From a79bad5cdb896fd965e544df3ba8d0a87b3db458 Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Fri, 19 Jun 2020 12:46:48 +0200 Subject: [PATCH 01/26] StatusContent: Better separate subject from status content. --- .../status_content/status_content.js | 6 ----- .../status_content/status_content.vue | 22 +++++++++++-------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js index c0a71e8f..cfee77a3 100644 --- a/src/components/status_content/status_content.js +++ b/src/components/status_content/status_content.js @@ -142,12 +142,6 @@ const StatusContent = { return html } }, - contentHtml () { - if (!this.status.summary_html) { - return this.postBodyHtml - } - return this.status.summary_html + '<br />' + this.postBodyHtml - }, ...mapGetters(['mergedConfig']), ...mapState({ betterShadow: state => state.interface.browserSupport.cssFilter, diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index efc2485e..df980a71 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -31,7 +31,7 @@ <div class="status-content media-body" @click.prevent="linkClicked" - v-html="contentHtml" + v-html="postBodyHtml" /> <a v-if="showingLongSubject" @@ -52,17 +52,17 @@ href="#" @click.prevent="toggleShowMore" >{{ $t("general.show_more") }}</a> + <div + v-if="status.summary_html" + class="status-content media-body summary" + @click.prevent="linkClicked" + v-html="status.summary_html" + /> <div v-if="!hideSubjectStatus" class="status-content media-body" @click.prevent="linkClicked" - v-html="contentHtml" - /> - <div - v-else - class="status-content media-body" - @click.prevent="linkClicked" - v-html="status.summary_html" + v-html="postBodyHtml" /> <a v-if="hideSubjectStatus" @@ -181,6 +181,10 @@ $status-margin: 0.75em; line-height: 1.4em; white-space: pre-wrap; + &.summary { + font-weight: bold; + } + blockquote { margin: 0.2em 0 0.2em 2em; font-style: italic; @@ -226,7 +230,7 @@ $status-margin: 0.75em; .greentext { color: $fallback--cGreen; - color: var(--postGreentext, $fallback--cGreen); + color: var(--cGreen, $fallback--cGreen); } .timeline :not(.panel-disabled) > { From 4da0a0c0bfcfaccd58def8d2170b952c1d15106a Mon Sep 17 00:00:00 2001 From: lain <lain@soykaf.club> Date: Fri, 19 Jun 2020 12:49:42 +0200 Subject: [PATCH 02/26] StatusContent: Fix greentext. --- src/components/status_content/status_content.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index df980a71..431661eb 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -230,7 +230,7 @@ $status-margin: 0.75em; .greentext { color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--postGreentext, $fallback--cGreen); } .timeline :not(.panel-disabled) > { From 44edb730c1e2298a00be0c1a139f80a1335ad7cf Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Fri, 26 Jun 2020 14:07:39 +0300 Subject: [PATCH 03/26] rip restyle subject, fix some issues with long subject --- .../status_content/status_content.js | 2 +- .../status_content/status_content.vue | 133 +++++++++++------- 2 files changed, 83 insertions(+), 52 deletions(-) diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js index cfee77a3..66501b3e 100644 --- a/src/components/status_content/status_content.js +++ b/src/components/status_content/status_content.js @@ -44,7 +44,7 @@ const StatusContent = { return lengthScore > 20 }, longSubject () { - return this.status.summary.length > 900 + return this.status.summary.length > 240 }, // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status. mightHideBecauseSubject () { diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index 431661eb..5698b4c0 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -3,16 +3,55 @@ <div class="status-body"> <slot name="header" /> <div - v-if="longSubject" + v-if="status.summary_html" + class="summary-wrapper" + :class="{ 'tall-subject': (longSubject && !showingLongSubject) }" + > + <div + class="media-body summary" + @click.prevent="linkClicked" + v-html="status.summary_html" + /> + <a + v-if="longSubject && showingLongSubject" + href="#" + class="tall-subject-hider" + @click.prevent="showingLongSubject=false" + >{{ $t("general.show_less") }}</a> + <a + v-else-if="longSubject" + class="tall-subject-hider" + :class="{ 'tall-subject-hider_focused': focused }" + href="#" + @click.prevent="showingLongSubject=true" + > + {{ $t("general.show_more") }} + </a> + </div> + <div + :class="{'tall-status': hideTallStatus}" class="status-content-wrapper" - :class="{ 'tall-status': !showingLongSubject }" > <a - v-if="!showingLongSubject" + v-if="hideTallStatus" class="tall-status-hider" :class="{ 'tall-status-hider_focused': focused }" href="#" - @click.prevent="showingLongSubject=true" + @click.prevent="toggleShowMore" + > + {{ $t("general.show_more") }} + </a> + <div + v-if="!hideSubjectStatus" + class="status-content media-body" + @click.prevent="linkClicked" + v-html="postBodyHtml" + /> + <a + v-if="hideSubjectStatus" + href="#" + class="cw-status-hider" + @click.prevent="toggleShowMore" > {{ $t("general.show_more") }} <span @@ -28,48 +67,6 @@ class="icon-link" /> </a> - <div - class="status-content media-body" - @click.prevent="linkClicked" - v-html="postBodyHtml" - /> - <a - v-if="showingLongSubject" - href="#" - class="status-unhider" - @click.prevent="showingLongSubject=false" - >{{ $t("general.show_less") }}</a> - </div> - <div - v-else - :class="{'tall-status': hideTallStatus}" - class="status-content-wrapper" - > - <a - v-if="hideTallStatus" - class="tall-status-hider" - :class="{ 'tall-status-hider_focused': focused }" - href="#" - @click.prevent="toggleShowMore" - >{{ $t("general.show_more") }}</a> - <div - v-if="status.summary_html" - class="status-content media-body summary" - @click.prevent="linkClicked" - v-html="status.summary_html" - /> - <div - v-if="!hideSubjectStatus" - class="status-content media-body" - @click.prevent="linkClicked" - v-html="postBodyHtml" - /> - <a - v-if="hideSubjectStatus" - href="#" - class="cw-status-hider" - @click.prevent="toggleShowMore" - >{{ $t("general.show_more") }}</a> <a v-if="showingMore" href="#" @@ -129,6 +126,12 @@ $status-margin: 0.75em; flex: 1; min-width: 0; + .status-content-wrapper { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + } + .tall-status { position: relative; height: 220px; @@ -136,7 +139,7 @@ $status-margin: 0.75em; overflow-y: hidden; z-index: 1; .status-content { - height: 100%; + min-height: 0; mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, linear-gradient(to top, white, white); /* Autoprefixed seem to ignore this one, and also syntax is different */ @@ -176,15 +179,43 @@ $status-margin: 0.75em; } } + .summary-wrapper { + margin-bottom: 0.5em; + border-style: solid; + border-width: 0 0 1px 0; + border-color: var(--border, $fallback--border); + flex-grow: 0; + } + + .summary { + font-style: italic; + padding-bottom: 0.5em; + } + + .tall-subject { + position: relative; + .summary { + max-height: 2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + .tall-subject-hider { + display: inline-block; + word-break: break-all; + // position: absolute; + width: 100%; + text-align: center; + padding-bottom: 0.5em; + } + .status-content { font-family: var(--postFont, sans-serif); line-height: 1.4em; white-space: pre-wrap; - &.summary { - font-weight: bold; - } - blockquote { margin: 0.2em 0 0.2em 2em; font-style: italic; From 8c3106c588f964a8e26c69784d5808ad1f72625c Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Fri, 26 Jun 2020 18:20:32 +0300 Subject: [PATCH 04/26] Change the show/hide strings about, remove subjected status toggle when 'collapse' option not used --- src/components/status_content/status_content.js | 4 ++-- src/components/status_content/status_content.vue | 10 ++++++---- src/i18n/en.json | 6 +++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js index 66501b3e..09ea3a20 100644 --- a/src/components/status_content/status_content.js +++ b/src/components/status_content/status_content.js @@ -48,10 +48,10 @@ const StatusContent = { }, // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status. mightHideBecauseSubject () { - return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault) + return !!this.status.summary && this.localCollapseSubjectDefault }, mightHideBecauseTall () { - return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault) + return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault) }, hideSubjectStatus () { return this.mightHideBecauseSubject && !this.expandingSubject diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index 5698b4c0..3460c2fa 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -17,7 +17,7 @@ href="#" class="tall-subject-hider" @click.prevent="showingLongSubject=false" - >{{ $t("general.show_less") }}</a> + >{{ $t("status.hide_full_subject") }}</a> <a v-else-if="longSubject" class="tall-subject-hider" @@ -25,7 +25,7 @@ href="#" @click.prevent="showingLongSubject=true" > - {{ $t("general.show_more") }} + {{ $t("status.show_full_subject") }} </a> </div> <div @@ -53,7 +53,7 @@ class="cw-status-hider" @click.prevent="toggleShowMore" > - {{ $t("general.show_more") }} + {{ $t("status.show_content") }} <span v-if="hasImageAttachments" class="icon-picture" @@ -72,7 +72,9 @@ href="#" class="status-unhider" @click.prevent="toggleShowMore" - >{{ $t("general.show_less") }}</a> + > + {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }} + </a> </div> <div v-if="status.poll && status.poll.options"> diff --git a/src/i18n/en.json b/src/i18n/en.json index eefe10e5..ede8c3d8 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -629,7 +629,11 @@ "status_unavailable": "Status unavailable", "copy_link": "Copy link to status", "thread_muted": "Thread muted", - "thread_muted_and_words": ", has words:" + "thread_muted_and_words": ", has words:", + "show_full_subject": "Show full subject", + "hide_full_subject": "Hide full subject", + "show_content": "Show content", + "hide_content": "Hide content" }, "user_card": { "approve": "Approve", From 195e83d0c8ab660a1dd6b29137295c2a6a36dca9 Mon Sep 17 00:00:00 2001 From: Fristi <fristi@subcon.town> Date: Sat, 27 Jun 2020 15:04:52 +0000 Subject: [PATCH 05/26] Translated using Weblate (Dutch) Currently translated at 100.0% (626 of 626 strings) Translation: Pleroma/Pleroma-FE Translate-URL: https://translate.pleroma.social/projects/pleroma/pleroma-fe/nl/ --- src/i18n/nl.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 15ce5cbe..bf270f87 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -476,7 +476,14 @@ "backend_version": "Backend Versie", "title": "Versie" }, - "mutes_and_blocks": "Negeringen en Blokkades" + "mutes_and_blocks": "Negeringen en Blokkades", + "profile_fields": { + "value": "Inhoud", + "name": "Label", + "add_field": "Veld Toevoegen", + "label": "Profiel metadata" + }, + "bot": "Dit is een bot account" }, "timeline": { "collapse": "Inklappen", From 6529f9fa34c5e4e4786f76f63068d70c771d868e Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Tue, 30 Jun 2020 15:04:16 +0300 Subject: [PATCH 06/26] add strikethrough when parent isn't visible --- src/components/status/status.vue | 11 ++++++++++- .../entity_normalizer/entity_normalizer.service.js | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 7ec29b28..c537358b 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -208,7 +208,12 @@ @click.prevent="gotoOriginal(status.in_reply_to_status_id)" > <i class="button-icon icon-reply" /> - <span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span> + <span + class="faint-link reply-to-text" + :class="{ 'strikethrough': !status.parent_visible }" + > + {{ $t('status.reply_to') }} + </span> </a> </StatusPopover> <span @@ -526,6 +531,10 @@ $status-margin: 0.75em; margin: 0 0.4em 0 0.2em; } + .strikethrough { + text-decoration: line-through; + } + .replies-separator { margin-left: 0.4em; } diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 3bdb92f3..10111045 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -248,6 +248,7 @@ export const parseStatus = (data) => { output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct output.thread_muted = pleroma.thread_muted output.emoji_reactions = pleroma.emoji_reactions + output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible } else { output.text = data.content output.summary = data.spoiler_text From ee1364a16770792e2a1040a0ea0b8f1693b5da52 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Tue, 30 Jun 2020 15:15:27 +0300 Subject: [PATCH 07/26] add no-statusId support for status popover --- src/components/status/status.vue | 2 +- src/components/status_popover/status_popover.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index c537358b..8237be6c 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -197,7 +197,7 @@ > <StatusPopover v-if="!isPreview" - :status-id="status.in_reply_to_status_id" + :status-id="status.parent_visible && status.in_reply_to_status_id" class="reply-to-popover" style="min-width: 0" > diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js index 159132a9..51e7680c 100644 --- a/src/components/status_popover/status_popover.js +++ b/src/components/status_popover/status_popover.js @@ -22,6 +22,10 @@ const StatusPopover = { methods: { enter () { if (!this.status) { + if (!this.statusId) { + this.error = true + return + } this.$store.dispatch('fetchStatus', this.statusId) .then(data => (this.error = false)) .catch(e => (this.error = true)) From 3a79918b89fa41f8676e7f19862e6e29abd4ea14 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Tue, 30 Jun 2020 15:23:47 +0300 Subject: [PATCH 08/26] update changelog for reply-to strikethrough --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 887588f3..e98b2728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Autocomplete domains from list of known instances - 'Bot' settings option and badge - Added profile meta data fields that can be set in profile settings +- When a post is a reply to an unavailable post, the 'Reply to'-text has a strike-through style ### Changed - Registration page no longer requires email if the server is configured not to require it From ea09bbecf8b7715a1242a104b6233a7c3b5ac588 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Tue, 30 Jun 2020 17:02:38 +0300 Subject: [PATCH 09/26] Make use of backend reply filtering --- CHANGELOG.md | 1 + .../settings_modal/tabs/filtering_tab.js | 3 ++ src/components/status/status.js | 33 +------------------ src/components/timeline/timeline.js | 6 ++-- src/components/timeline/timeline.vue | 4 +-- src/modules/statuses.js | 8 +++++ src/services/api/api.service.js | 6 +++- .../timeline_fetcher.service.js | 4 ++- 8 files changed, 27 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 887588f3..d978d362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Subject field now appears disabled when posting - Fix status ellipsis menu being cut off in notifications column - Fixed autocomplete sometimes not returning the right user when there's already some results +- Reply filtering options in Settings -> Filtering now work again using filtering on server ## [2.0.3] - 2020-05-02 ### Fixed diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index 224a7f47..3b2df556 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -37,6 +37,9 @@ const FilteringTab = { }) }, deep: true + }, + replyVisibility () { + this.$store.dispatch('queueFlushAll') } } } diff --git a/src/components/status/status.js b/src/components/status/status.js index 73382521..ad0b72a9 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -141,7 +141,7 @@ const Status = { return this.mergedConfig.hideFilteredStatuses }, hideStatus () { - return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses) + return this.deleted || (this.muted && this.hideFilteredStatuses) }, isFocused () { // retweet or root of an expanded conversation @@ -164,37 +164,6 @@ const Status = { return user && user.screen_name } }, - hideReply () { - if (this.mergedConfig.replyVisibility === 'all') { - return false - } - if (this.inConversation || !this.isReply) { - return false - } - if (this.status.user.id === this.currentUser.id) { - return false - } - if (this.status.type === 'retweet') { - return false - } - const checkFollowing = this.mergedConfig.replyVisibility === 'following' - for (var i = 0; i < this.status.attentions.length; ++i) { - if (this.status.user.id === this.status.attentions[i].id) { - continue - } - // There's zero guarantee of this working. If we happen to have that user and their - // relationship in store then it will work, but there's kinda little chance of having - // them for people you're not following. - const relationship = this.$store.state.users.relationships[this.status.attentions[i].id] - if (checkFollowing && relationship && relationship.following) { - return false - } - if (this.status.attentions[i].id === this.currentUser.id) { - return false - } - } - return this.status.attentions.length > 0 - }, replySubject () { if (!this.status.summary) return '' const decodedSummary = unescape(this.status.summary) diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 9a53acd6..3a244c83 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -45,6 +45,10 @@ const Timeline = { newStatusCount () { return this.timeline.newStatusCount }, + showLoadButton () { + if (this.timelineError || this.errorData) return false + return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0 + }, newStatusCountStr () { if (this.timeline.flushMarker !== 0) { return '' @@ -112,8 +116,6 @@ const Timeline = { if (e.key === '.') this.showNewStatuses() }, showNewStatuses () { - if (this.newStatusCount === 0) return - if (this.timeline.flushMarker !== 0) { this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true }) this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 }) diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 9777bd0c..bd8389b9 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -19,14 +19,14 @@ {{ errorData.statusText }} </div> <button - v-if="timeline.newStatusCount > 0 && !timelineError && !errorData" + v-else-if="showLoadButton" class="loadmore-button" @click.prevent="showNewStatuses" > {{ $t('timeline.show_new') }}{{ newStatusCountStr }} </button> <div - v-if="!timeline.newStatusCount > 0 && !timelineError && !errorData" + v-else class="loadmore-text faint" @click.prevent > diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 073b15f1..4d3f8031 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -515,6 +515,11 @@ export const mutations = { queueFlush (state, { timeline, id }) { state.timelines[timeline].flushMarker = id }, + queueFlushAll (state) { + Object.keys(state.timelines).forEach((timeline) => { + state.timelines[timeline].flushMarker = state.timelines[timeline].maxId + }) + }, addRepeats (state, { id, rebloggedByUsers, currentUser }) { const newStatus = state.allStatusesObject[id] newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _) @@ -664,6 +669,9 @@ const statuses = { queueFlush ({ rootState, commit }, { timeline, id }) { commit('queueFlush', { timeline, id }) }, + queueFlushAll ({ rootState, commit }) { + commit('queueFlushAll') + }, markNotificationsAsSeen ({ rootState, commit }) { commit('markNotificationsAsSeen') apiService.markNotificationsAsSeen({ diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index dfffc291..7e5e9645 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -498,7 +498,8 @@ const fetchTimeline = ({ until = false, userId = false, tag = false, - withMuted = false + withMuted = false, + replyVisibility = 'all' }) => { const timelineUrls = { public: MASTODON_PUBLIC_TIMELINE, @@ -541,6 +542,9 @@ const fetchTimeline = ({ if (timeline !== 'favorites') { params.push(['with_muted', withMuted]) } + if (replyVisibility !== 'all') { + params.push(['reply_visibility', replyVisibility]) + } params.push(['limit', 20]) diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index c6b28ad5..30fb26bd 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -30,7 +30,8 @@ const fetchAndUpdate = ({ const rootState = store.rootState || store.state const { getters } = store const timelineData = rootState.statuses.timelines[camelCase(timeline)] - const hideMutedPosts = getters.mergedConfig.hideMutedPosts + const { hideMutedPosts, replyVisibility } = getters.mergedConfig + const loggedIn = !!rootState.users.currentUser if (older) { args['until'] = until || timelineData.minId @@ -41,6 +42,7 @@ const fetchAndUpdate = ({ args['userId'] = userId args['tag'] = tag args['withMuted'] = !hideMutedPosts + if (loggedIn) args['replyVisibility'] = replyVisibility const numStatusesBeforeFetch = timelineData.statuses.length From 38d8526660df4ca664c9ea50a660868be2cb5e49 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Tue, 30 Jun 2020 17:37:36 +0300 Subject: [PATCH 10/26] change Show New text to Reload when flushing --- src/components/timeline/timeline.js | 6 +++--- src/components/timeline/timeline.vue | 2 +- src/i18n/en.json | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 3a244c83..d6519f4a 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -49,11 +49,11 @@ const Timeline = { if (this.timelineError || this.errorData) return false return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0 }, - newStatusCountStr () { + loadButtonString () { if (this.timeline.flushMarker !== 0) { - return '' + return this.$t('timeline.reload') } else { - return ` (${this.newStatusCount})` + return `${this.$t('timeline.show_new')} (${this.newStatusCount})` } }, classes () { diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index bd8389b9..111c0976 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -23,7 +23,7 @@ class="loadmore-button" @click.prevent="showNewStatuses" > - {{ $t('timeline.show_new') }}{{ newStatusCountStr }} + {{ loadButtonString }} </button> <div v-else diff --git a/src/i18n/en.json b/src/i18n/en.json index e6af4266..59f69e57 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -617,6 +617,7 @@ "no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated", "repeated": "repeated", "show_new": "Show new", + "reload": "Reload", "up_to_date": "Up-to-date", "no_more_statuses": "No more statuses", "no_statuses": "No statuses" From 62d0bc47b333135f31abea90b4f5a3c28e608733 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Wed, 1 Jul 2020 14:15:04 +0300 Subject: [PATCH 11/26] remove unnecessary fetchAndUpdate, change notifications fetcher to not double fetch --- src/components/notifications/notifications.js | 5 ----- src/modules/api.js | 3 --- .../backend_interactor_service.js | 4 ---- .../notifications_fetcher/notifications_fetcher.service.js | 7 +++++-- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 26ffbab6..e999a18e 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -56,11 +56,6 @@ const Notifications = { components: { Notification }, - created () { - const { dispatch } = this.$store - - dispatch('fetchAndUpdateNotifications') - }, watch: { unseenCount (count) { if (count > 0) { diff --git a/src/modules/api.js b/src/modules/api.js index 748570e5..04ef6ab4 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -138,9 +138,6 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: 'notifications', fetcher }) }, - fetchAndUpdateNotifications (store) { - store.state.backendInteractor.fetchAndUpdateNotifications({ store }) - }, // Follow requests startFetchingFollowRequests (store) { diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index e1c32860..45e6bd0e 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -12,10 +12,6 @@ const backendInteractorService = credentials => ({ return notificationsFetcher.startFetching({ store, credentials }) }, - fetchAndUpdateNotifications ({ store }) { - return notificationsFetcher.fetchAndUpdate({ store, 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 64499a1b..581931f5 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -31,8 +31,11 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => { const notifications = timelineData.data const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id) if (readNotifsIds.length) { - args['since'] = Math.max(...readNotifsIds) - fetchNotifications({ store, args, older }) + const possibleMax = Math.max(...readNotifsIds) + if (possibleMax !== timelineData.maxId) { + args['since'] = possibleMax + fetchNotifications({ store, args, older }) + } } return result From a3e370e9f816e3c98028bf82e8ac020b8e294466 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Wed, 1 Jul 2020 15:19:45 +0300 Subject: [PATCH 12/26] add initial fetching back in a more streamlined way --- src/components/notifications/notifications.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index e999a18e..30187072 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -27,6 +27,11 @@ const Notifications = { seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT } }, + created () { + const store = this.$store + const credentials = store.state.users.currentUser.credentials + notificationsFetcher.fetchAndUpdate({ store: this.$store, credentials }) + }, computed: { mainClass () { return this.minimalMode ? '' : 'panel panel-default' From ca997f45e8b2e6c8df72833d774bad7266af76bf Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Wed, 1 Jul 2020 15:56:45 +0300 Subject: [PATCH 13/26] allow overscrolling enough to not have FAB block interactables --- src/App.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/App.scss b/src/App.scss index f2972eda..6597b6f4 100644 --- a/src/App.scss +++ b/src/App.scss @@ -858,6 +858,10 @@ nav { display: block; margin-right: 0.8em; } + + .main { + margin-bottom: 7em; + } } .select-multiple { From 3ebd4e4429a9680046daeac2ed8753471365be9e Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Wed, 1 Jul 2020 17:55:42 +0300 Subject: [PATCH 14/26] document the 'mark-as-read-detection' system --- .../notifications_fetcher.service.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 581931f5..c2552480 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -27,17 +27,18 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => { } const result = fetchNotifications({ store, args, older }) - // load unread notifications repeatedly to provide consistency between browser tabs + // If there's any unread notifications, try fetch notifications since + // the newest read notification to check if any of the unread notifs + // have changed their 'seen' state (marked as read in another session), so + // we can update the state in this session to mark them as read as well. + // The normal maxId-check does not tell if older notifications have changed const notifications = timelineData.data const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id) - if (readNotifsIds.length) { - const possibleMax = Math.max(...readNotifsIds) - if (possibleMax !== timelineData.maxId) { - args['since'] = possibleMax - fetchNotifications({ store, args, older }) - } + const numUnseenNotifs = notifications.length - readNotifsIds.length + if (numUnseenNotifs > 0) { + args['since'] = Math.max(...readNotifsIds) + fetchNotifications({ store, args, older }) } - return result } } From d30b0b28c9371e56ffe54b5a8b56087718221c1d Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Wed, 1 Jul 2020 19:15:28 +0300 Subject: [PATCH 15/26] catch localforage error and let the application work, add an alert for user to dismiss --- src/App.js | 6 ++++++ src/App.scss | 9 +++++++++ src/App.vue | 10 ++++++++++ src/i18n/en.json | 3 +++ src/main.js | 13 +++++++++++-- src/modules/instance.js | 2 +- src/modules/interface.js | 7 +++++++ 7 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/App.js b/src/App.js index 040138c9..da66fe21 100644 --- a/src/App.js +++ b/src/App.js @@ -107,6 +107,9 @@ export default { return { 'order': this.$store.state.instance.sidebarRight ? 99 : 0 } + }, + showStorageError () { + return this.$store.state.interface.storageError === 'show' } }, methods: { @@ -129,6 +132,9 @@ export default { if (changed) { this.$store.dispatch('setMobileLayout', mobileLayout) } + }, + hideStorageError () { + this.$store.dispatch('setStorageError', 'hide') } } } diff --git a/src/App.scss b/src/App.scss index f2972eda..db447f1c 100644 --- a/src/App.scss +++ b/src/App.scss @@ -806,6 +806,15 @@ nav { } } +.storage-error-notice { + text-align: center; + i { + cursor: pointer; + color: $fallback--text; + color: var(--alertErrorText, $fallback--text); + } +} + .button-icon { font-size: 1.2em; } diff --git a/src/App.vue b/src/App.vue index 7b9ad3dc..23991eac 100644 --- a/src/App.vue +++ b/src/App.vue @@ -101,6 +101,16 @@ </div> </div> <div class="main"> + <div + v-if="showStorageError" + class="alert error storage-error-notice" + > + {{ $t("errors.storage_unavailable") }} + <i + class="icon-cancel" + @click="hideStorageError" + /> + </div> <div v-if="!currentUser" class="login-hint panel panel-default" diff --git a/src/i18n/en.json b/src/i18n/en.json index 59f69e57..4856008f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -163,6 +163,9 @@ "load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.", "load_all": "Loading all {emojiAmount} emoji" }, + "errors": { + "storage_unavailable": "Pleroma could not access browser storage. You may encounter issues and your login or your local settings won't be saved. Try enabling cookies." + }, "interactions": { "favs_repeats": "Repeats and Favorites", "follows": "New follows", diff --git a/src/main.js b/src/main.js index 9a201e4f..bb2c8cd3 100644 --- a/src/main.js +++ b/src/main.js @@ -62,7 +62,16 @@ const persistedStateOptions = { }; (async () => { - const persistedState = await createPersistedState(persistedStateOptions) + console.log('before perse state') + let persistedState + let storageError = 'none' + try { + persistedState = await createPersistedState(persistedStateOptions) + } catch (e) { + console.error(e) + storageError = 'show' + persistedState = _ => _ + } const store = new Vuex.Store({ modules: { i18n: { @@ -89,7 +98,7 @@ const persistedStateOptions = { strict: false // Socket modifies itself, let's ignore this for now. // strict: process.env.NODE_ENV !== 'production' }) - + store.dispatch('setStorageError', storageError) afterStoreSetup({ store, i18n }) })() diff --git a/src/modules/instance.js b/src/modules/instance.js index ec5f4e54..cc884317 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -8,7 +8,7 @@ const defaultState = { // Stuff from apiConfig name: 'Pleroma FE', registrationOpen: true, - server: 'http://localhost:4040/', + server: 'http://lain.com:4040', textlimit: 5000, themeData: undefined, vapidPublicKey: undefined, diff --git a/src/modules/interface.js b/src/modules/interface.js index eeebd65e..4b5b5b5d 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -8,6 +8,7 @@ const defaultState = { noticeClearTimeout: null, notificationPermission: null }, + storageError: 'none', browserSupport: { cssFilter: window.CSS && window.CSS.supports && ( window.CSS.supports('filter', 'drop-shadow(0 0)') || @@ -58,6 +59,9 @@ const interfaceMod = { if (!state.settingsModalLoaded) { state.settingsModalLoaded = true } + }, + setStorageError (state, value) { + state.storageError = value } }, actions: { @@ -81,6 +85,9 @@ const interfaceMod = { }, togglePeekSettingsModal ({ commit }) { commit('togglePeekSettingsModal') + }, + setStorageError ({ commit }, value) { + commit('setStorageError', value) } } } From 43b7a5d9b387e005a47debb5962c76c773cd862c Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Wed, 1 Jul 2020 19:22:39 +0300 Subject: [PATCH 16/26] update the message and changelog --- CHANGELOG.md | 1 + src/i18n/en.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d978d362..b57e9afb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix status ellipsis menu being cut off in notifications column - Fixed autocomplete sometimes not returning the right user when there's already some results - Reply filtering options in Settings -> Filtering now work again using filtering on server +- Don't show just blank-screen when cookies are disabled ## [2.0.3] - 2020-05-02 ### Fixed diff --git a/src/i18n/en.json b/src/i18n/en.json index 4856008f..0b34ae07 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -164,7 +164,7 @@ "load_all": "Loading all {emojiAmount} emoji" }, "errors": { - "storage_unavailable": "Pleroma could not access browser storage. You may encounter issues and your login or your local settings won't be saved. 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": { "favs_repeats": "Repeats and Favorites", From 15d492ace4db5243330741a007daa9929e221f64 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Wed, 1 Jul 2020 19:24:17 +0300 Subject: [PATCH 17/26] revert accidental change in instance.js --- src/modules/instance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/instance.js b/src/modules/instance.js index cc884317..ec5f4e54 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -8,7 +8,7 @@ const defaultState = { // Stuff from apiConfig name: 'Pleroma FE', registrationOpen: true, - server: 'http://lain.com:4040', + server: 'http://localhost:4040/', textlimit: 5000, themeData: undefined, vapidPublicKey: undefined, From 0997e5ff668a57d58002ec646f698c5503f66c35 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Wed, 1 Jul 2020 19:25:31 +0300 Subject: [PATCH 18/26] remove accidental log --- src/main.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.js b/src/main.js index bb2c8cd3..a7294ea0 100644 --- a/src/main.js +++ b/src/main.js @@ -62,7 +62,6 @@ const persistedStateOptions = { }; (async () => { - console.log('before perse state') let persistedState let storageError = 'none' try { From 1293bec77e5137acf64d3536c286f8ba3df284f4 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Thu, 2 Jul 2020 10:40:41 +0300 Subject: [PATCH 19/26] change storage error one-off into a global notice system --- src/App.js | 10 +-- src/App.scss | 9 --- src/App.vue | 11 +-- .../global_notice_list/global_notice_list.js | 15 ++++ .../global_notice_list/global_notice_list.vue | 77 +++++++++++++++++++ src/main.js | 16 ++-- src/modules/interface.js | 33 ++++++-- src/services/theme_data/pleromafe.js | 36 ++++++++- 8 files changed, 167 insertions(+), 40 deletions(-) create mode 100644 src/components/global_notice_list/global_notice_list.js create mode 100644 src/components/global_notice_list/global_notice_list.vue diff --git a/src/App.js b/src/App.js index da66fe21..92c4e2f5 100644 --- a/src/App.js +++ b/src/App.js @@ -13,6 +13,7 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil import MobileNav from './components/mobile_nav/mobile_nav.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' import PostStatusModal from './components/post_status_modal/post_status_modal.vue' +import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' import { windowWidth } from './services/window_utils/window_utils' export default { @@ -32,7 +33,8 @@ export default { MobileNav, SettingsModal, UserReportingModal, - PostStatusModal + PostStatusModal, + GlobalNoticeList }, data: () => ({ mobileActivePanel: 'timeline', @@ -107,9 +109,6 @@ export default { return { 'order': this.$store.state.instance.sidebarRight ? 99 : 0 } - }, - showStorageError () { - return this.$store.state.interface.storageError === 'show' } }, methods: { @@ -132,9 +131,6 @@ export default { if (changed) { this.$store.dispatch('setMobileLayout', mobileLayout) } - }, - hideStorageError () { - this.$store.dispatch('setStorageError', 'hide') } } } diff --git a/src/App.scss b/src/App.scss index db447f1c..f2972eda 100644 --- a/src/App.scss +++ b/src/App.scss @@ -806,15 +806,6 @@ nav { } } -.storage-error-notice { - text-align: center; - i { - cursor: pointer; - color: $fallback--text; - color: var(--alertErrorText, $fallback--text); - } -} - .button-icon { font-size: 1.2em; } diff --git a/src/App.vue b/src/App.vue index 23991eac..03b632ec 100644 --- a/src/App.vue +++ b/src/App.vue @@ -101,16 +101,6 @@ </div> </div> <div class="main"> - <div - v-if="showStorageError" - class="alert error storage-error-notice" - > - {{ $t("errors.storage_unavailable") }} - <i - class="icon-cancel" - @click="hideStorageError" - /> - </div> <div v-if="!currentUser" class="login-hint panel panel-default" @@ -138,6 +128,7 @@ <PostStatusModal /> <SettingsModal /> <portal-target name="modal" /> + <GlobalNoticeList /> </div> </template> diff --git a/src/components/global_notice_list/global_notice_list.js b/src/components/global_notice_list/global_notice_list.js new file mode 100644 index 00000000..3af29c23 --- /dev/null +++ b/src/components/global_notice_list/global_notice_list.js @@ -0,0 +1,15 @@ + +const GlobalNoticeList = { + computed: { + notices () { + return this.$store.state.interface.globalNotices + } + }, + methods: { + closeNotice (notice) { + this.$store.dispatch('removeGlobalNotice', notice) + } + } +} + +export default GlobalNoticeList diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue new file mode 100644 index 00000000..0e4285cc --- /dev/null +++ b/src/components/global_notice_list/global_notice_list.vue @@ -0,0 +1,77 @@ +<template> + <div class="global-notice-list"> + <div + v-for="(notice, index) in notices" + :key="index" + class="alert global-notice" + :class="{ ['global-' + notice.level]: true }" + > + <div class="notice-message"> + {{ $t(notice.messageKey, notice.messageArgs) }} + </div> + <i + class="button-icon icon-cancel" + @click="closeNotice(notice)" + /> + </div> + </div> +</template> + +<script src="./global_notice_list.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.global-notice-list { + position: fixed; + top: 50px; + width: 100%; + pointer-events: none; + z-index: 1001; + display: flex; + flex-direction: column; + align-items: center; + + .global-notice { + pointer-events: auto; + text-align: center; + width: 40em; + max-width: calc(100% - 3em); + display: flex; + padding-left: 1.5em; + line-height: 2em; + .notice-message { + flex: 1 1 100%; + } + i { + flex: 0 0; + width: 1.5em; + cursor: pointer; + } + } + + .global-error { + background-color: var(--alertPopupError, $fallback--cRed); + color: var(--alertPopupErrorText, $fallback--text); + i { + color: var(--alertPopupErrorText, $fallback--text); + } + } + + .global-warning { + background-color: var(--alertPopupWarning, $fallback--cOrange); + color: var(--alertPopupWarningText, $fallback--text); + i { + color: var(--alertPopupWarningText, $fallback--text); + } + } + + .global-info { + background-color: var(--alertPopupNeutral, $fallback--fg); + color: var(--alertPopupNeutralText, $fallback--text); + i { + color: var(--alertPopupNeutralText, $fallback--text); + } + } +} +</style> diff --git a/src/main.js b/src/main.js index a7294ea0..5bddc76e 100644 --- a/src/main.js +++ b/src/main.js @@ -62,14 +62,14 @@ const persistedStateOptions = { }; (async () => { - let persistedState - let storageError = 'none' + let storageError = false + const plugins = [pushNotifications] try { - persistedState = await createPersistedState(persistedStateOptions) + const persistedState = await createPersistedState(persistedStateOptions) + plugins.push(persistedState) } catch (e) { console.error(e) - storageError = 'show' - persistedState = _ => _ + storageError = true } const store = new Vuex.Store({ modules: { @@ -93,11 +93,13 @@ const persistedStateOptions = { polls: pollsModule, postStatus: postStatusModule }, - plugins: [persistedState, pushNotifications], + plugins, strict: false // Socket modifies itself, let's ignore this for now. // strict: process.env.NODE_ENV !== 'production' }) - store.dispatch('setStorageError', storageError) + if (storageError) { + store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' }) + } afterStoreSetup({ store, i18n }) })() diff --git a/src/modules/interface.js b/src/modules/interface.js index 4b5b5b5d..338ef651 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -8,14 +8,14 @@ const defaultState = { noticeClearTimeout: null, notificationPermission: null }, - storageError: 'none', browserSupport: { cssFilter: window.CSS && window.CSS.supports && ( window.CSS.supports('filter', 'drop-shadow(0 0)') || window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)') ) }, - mobileLayout: false + mobileLayout: false, + globalNotices: [] } const interfaceMod = { @@ -60,8 +60,11 @@ const interfaceMod = { state.settingsModalLoaded = true } }, - setStorageError (state, value) { - state.storageError = value + pushGlobalNotice (state, notice) { + state.globalNotices.push(notice) + }, + removeGlobalNotice (state, notice) { + state.globalNotices = state.globalNotices.filter(n => n !== notice) } }, actions: { @@ -86,8 +89,26 @@ const interfaceMod = { togglePeekSettingsModal ({ commit }) { commit('togglePeekSettingsModal') }, - setStorageError ({ commit }, value) { - commit('setStorageError', value) + pushGlobalNotice ( + { commit, dispatch }, + { + messageKey, + messageArgs = {}, + level = 'error', + timeout = 0 + }) { + const notice = { + messageKey, + messageArgs, + level + } + if (timeout) { + setTimeout(() => dispatch('removeGlobalNotice', notice), timeout) + } + commit('pushGlobalNotice', notice) + }, + removeGlobalNotice ({ commit }, notice) { + commit('removeGlobalNotice', notice) } } } diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js index b577cfab..83c878ed 100644 --- a/src/services/theme_data/pleromafe.js +++ b/src/services/theme_data/pleromafe.js @@ -34,7 +34,8 @@ export const DEFAULT_OPACITY = { alert: 0.5, input: 0.5, faint: 0.5, - underlay: 0.15 + underlay: 0.15, + alertPopup: 0.85 } /** SUBJECT TO CHANGE IN THE FUTURE, this is all beta @@ -627,6 +628,39 @@ export const SLOT_INHERITANCE = { textColor: true }, + alertPopupError: { + depends: ['alertError'], + opacity: 'alertPopup' + }, + alertPopupErrorText: { + depends: ['alertErrorText'], + layer: 'popover', + variant: 'alertPopupError', + textColor: true + }, + + alertPopupWarning: { + depends: ['alertWarning'], + opacity: 'alertPopup' + }, + alertPopupWarningText: { + depends: ['alertWarningText'], + layer: 'popover', + variant: 'alertPopupWarning', + textColor: true + }, + + alertPopupNeutral: { + depends: ['alertNeutral'], + opacity: 'alertPopup' + }, + alertPopupNeutralText: { + depends: ['alertNeutralText'], + layer: 'popover', + variant: 'alertPopupNeutral', + textColor: true + }, + badgeNotification: '--cRed', badgeNotificationText: { depends: ['text', 'badgeNotification'], From 685ab4f33ee57e1dc4997b58314e5c0d38581e9d Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Thu, 2 Jul 2020 10:46:43 +0300 Subject: [PATCH 20/26] make the addNotice dispatch return the notice --- src/modules/interface.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/interface.js b/src/modules/interface.js index 338ef651..e31630fc 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -106,6 +106,7 @@ const interfaceMod = { setTimeout(() => dispatch('removeGlobalNotice', notice), timeout) } commit('pushGlobalNotice', notice) + return notice }, removeGlobalNotice ({ commit }, notice) { commit('removeGlobalNotice', notice) From f0668c9ff8b985cd5f537fbb0d1083480cbfb065 Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Thu, 2 Jul 2020 12:19:33 +0300 Subject: [PATCH 21/26] add follow request users to store --- .../follow_request_fetcher/follow_request_fetcher.service.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js index 786740b7..93fac9bc 100644 --- a/src/services/follow_request_fetcher/follow_request_fetcher.service.js +++ b/src/services/follow_request_fetcher/follow_request_fetcher.service.js @@ -4,6 +4,7 @@ const fetchAndUpdate = ({ store, credentials }) => { return apiService.fetchFollowRequests({ credentials }) .then((requests) => { store.commit('setFollowRequests', requests) + store.commit('addNewUsers', requests) }, () => {}) .catch(() => {}) } From 9cac5d94dd78ea331254307871e876da7e266a6f Mon Sep 17 00:00:00 2001 From: Shpuld Shpuldson <shp@cock.li> Date: Thu, 2 Jul 2020 15:17:58 +0300 Subject: [PATCH 22/26] change alert popup alpha --- src/services/theme_data/pleromafe.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js index 83c878ed..6b25cd6f 100644 --- a/src/services/theme_data/pleromafe.js +++ b/src/services/theme_data/pleromafe.js @@ -35,7 +35,7 @@ export const DEFAULT_OPACITY = { input: 0.5, faint: 0.5, underlay: 0.15, - alertPopup: 0.85 + alertPopup: 0.95 } /** SUBJECT TO CHANGE IN THE FUTURE, this is all beta From 08b593746faf54ac41df25bb2cc5a1f72ae1a9b5 Mon Sep 17 00:00:00 2001 From: Ilja <pleroma@spectraltheorem.be> Date: Fri, 3 Jul 2020 10:17:42 +0000 Subject: [PATCH 23/26] FE part of BE issue 1586 provide index md * I added an index.md which will be the landing page for the docs. It has an explanation of Pleroma-FE from the user point of view * See also BE MR: https://git.pleroma.social/pleroma/pleroma/-/merge_requests/2669 * And issue: https://git.pleroma.social/pleroma/pleroma/-/issues/1586 --- docs/USER_GUIDE.md | 2 -- docs/index.md | 8 ++++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 docs/index.md diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index f417f33d..241ad331 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -8,8 +8,6 @@ > > --Catbag -Pleroma-FE user interface is modeled after Qvitter which is modeled after older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options. - ## Posting, reading, basic functions. After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..8764f9ab --- /dev/null +++ b/docs/index.md @@ -0,0 +1,8 @@ +# Introduction to Pleroma-FE +## What is Pleroma-FE? + +Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface is modeled after Qvitter which is modeled after an older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options. + +## How can I use it? + +If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./USER_GUIDE.md). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md). From de291e2e33f1d9e04b27ed30ba3b012d73178e63 Mon Sep 17 00:00:00 2001 From: Eugenij <eugenijm@protonmail.com> Date: Fri, 3 Jul 2020 19:45:49 +0000 Subject: [PATCH 24/26] Add bookmarks Co-authored-by: jared <jaredrmain@gmail.com> --- CHANGELOG.md | 1 + package.json | 1 + src/boot/routes.js | 2 + .../bookmark_timeline/bookmark_timeline.js | 17 ++++++++ .../bookmark_timeline/bookmark_timeline.vue | 9 ++++ src/components/extra_buttons/extra_buttons.js | 10 +++++ .../extra_buttons/extra_buttons.vue | 16 +++++++ src/components/nav_panel/nav_panel.vue | 5 +++ src/components/side_drawer/side_drawer.vue | 8 ++++ src/components/timeline/timeline.js | 2 +- src/i18n/en.json | 6 ++- src/i18n/ru.json | 7 ++- src/modules/statuses.js | 43 +++++++++++++++---- src/services/api/api.service.js | 34 +++++++++++++-- .../entity_normalizer.service.js | 16 +++++++ .../notifications_fetcher.service.js | 2 +- .../timeline_fetcher.service.js | 17 +++++--- static/fontello.json | 12 ++++++ .../entity_normalizer.spec.js | 22 +++++++++- yarn.lock | 7 +++ 20 files changed, 213 insertions(+), 24 deletions(-) create mode 100644 src/components/bookmark_timeline/bookmark_timeline.js create mode 100644 src/components/bookmark_timeline/bookmark_timeline.vue mode change 100755 => 100644 static/fontello.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d978d362..2595af1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Ability to change user's email - About page - Added remote user redirect +- Bookmarks ### Changed - changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes ### Fixed diff --git a/package.json b/package.json index c0665f6e..96231171 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "cropperjs": "^1.4.3", "diff": "^3.0.1", "escape-html": "^1.0.3", + "parse-link-header": "^1.0.1", "localforage": "^1.5.0", "phoenix": "^1.3.0", "portal-vue": "^2.1.4", diff --git a/src/boot/routes.js b/src/boot/routes.js index d98a3b50..f63d8adf 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -2,6 +2,7 @@ import PublicTimeline from 'components/public_timeline/public_timeline.vue' import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue' import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue' import TagTimeline from 'components/tag_timeline/tag_timeline.vue' +import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue' import ConversationPage from 'components/conversation-page/conversation-page.vue' import Interactions from 'components/interactions/interactions.vue' import DMs from 'components/dm_timeline/dm_timeline.vue' @@ -40,6 +41,7 @@ export default (store) => { { name: 'public-timeline', path: '/main/public', component: PublicTimeline }, { name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute }, { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, + { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'remote-user-profile-acct', path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)', diff --git a/src/components/bookmark_timeline/bookmark_timeline.js b/src/components/bookmark_timeline/bookmark_timeline.js new file mode 100644 index 00000000..64b69e5d --- /dev/null +++ b/src/components/bookmark_timeline/bookmark_timeline.js @@ -0,0 +1,17 @@ +import Timeline from '../timeline/timeline.vue' + +const Bookmarks = { + computed: { + timeline () { + return this.$store.state.statuses.timelines.bookmarks + } + }, + components: { + Timeline + }, + destroyed () { + this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) + } +} + +export default Bookmarks diff --git a/src/components/bookmark_timeline/bookmark_timeline.vue b/src/components/bookmark_timeline/bookmark_timeline.vue new file mode 100644 index 00000000..8da6884b --- /dev/null +++ b/src/components/bookmark_timeline/bookmark_timeline.vue @@ -0,0 +1,9 @@ +<template> + <Timeline + :title="$t('nav.bookmarks')" + :timeline="timeline" + :timeline-name="'bookmarks'" + /> +</template> + +<script src="./bookmark_timeline.js"></script> diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index e4b19d01..5e0c36bb 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -34,6 +34,16 @@ const ExtraButtons = { navigator.clipboard.writeText(this.statusLink) .then(() => this.$emit('onSuccess')) .catch(err => this.$emit('onError', err.error.error)) + }, + bookmarkStatus () { + this.$store.dispatch('bookmark', { id: this.status.id }) + .then(() => this.$emit('onSuccess')) + .catch(err => this.$emit('onError', err.error.error)) + }, + unbookmarkStatus () { + this.$store.dispatch('unbookmark', { id: this.status.id }) + .then(() => this.$emit('onSuccess')) + .catch(err => this.$emit('onError', err.error.error)) } }, computed: { diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index 68db6fd8..7a4e8642 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -40,6 +40,22 @@ > <i class="icon-pin" /><span>{{ $t("status.unpin") }}</span> </button> + <button + v-if="!status.bookmarked" + class="dropdown-item dropdown-item-icon" + @click.prevent="bookmarkStatus" + @click="close" + > + <i class="icon-bookmark-empty" /><span>{{ $t("status.bookmark") }}</span> + </button> + <button + v-if="status.bookmarked" + class="dropdown-item dropdown-item-icon" + @click.prevent="unbookmarkStatus" + @click="close" + > + <i class="icon-bookmark" /><span>{{ $t("status.unbookmark") }}</span> + </button> <button v-if="canDelete" class="dropdown-item dropdown-item-icon" diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 8cd04dc7..f164b2b0 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -17,6 +17,11 @@ <i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }} </router-link> </li> + <li v-if="currentUser"> + <router-link :to="{ name: 'bookmarks'}"> + <i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }} + </router-link> + </li> <li v-if="currentUser && currentUser.locked"> <router-link :to="{ name: 'friend-requests' }"> <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }} diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index f253742d..0ac53b34 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -65,6 +65,14 @@ <i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }} </router-link> </li> + <li + v-if="currentUser" + @click="toggleDrawer" + > + <router-link :to="{ name: 'bookmarks'}"> + <i class="button-icon icon-bookmark" /> {{ $t("nav.bookmarks") }} + </router-link> + </li> <li v-if="currentUser && currentUser.locked" @click="toggleDrawer" diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index d6519f4a..bac73022 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -137,7 +137,7 @@ const Timeline = { showImmediately: true, userId: this.userId, tag: this.tag - }).then(statuses => { + }).then(({ statuses }) => { store.commit('setLoading', { timeline: this.timelineName, value: false }) if (statuses && statuses.length === 0) { this.bottomedOut = true diff --git a/src/i18n/en.json b/src/i18n/en.json index 59f69e57..2e48c473 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -120,6 +120,7 @@ "public_tl": "Public Timeline", "timeline": "Timeline", "twkn": "The Whole Known Network", + "bookmarks": "Bookmarks", "user_search": "User Search", "search": "Search", "who_to_follow": "Who to follow", @@ -629,6 +630,8 @@ "pin": "Pin on profile", "unpin": "Unpin from profile", "pinned": "Pinned", + "bookmark": "Bookmark", + "unbookmark": "Unbookmark", "delete_confirm": "Do you really want to delete this status?", "reply_to": "Reply to", "replies_list": "Replies:", @@ -724,7 +727,8 @@ "add_reaction": "Add Reaction", "user_settings": "User Settings", "accept_follow_request": "Accept follow request", - "reject_follow_request": "Reject follow request" + "reject_follow_request": "Reject follow request", + "bookmark": "Bookmark" }, "upload": { "error": { diff --git a/src/i18n/ru.json b/src/i18n/ru.json index aa78db26..08f05d18 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -45,7 +45,8 @@ "timeline": "Лента", "twkn": "Федеративная лента", "search": "Поиск", - "friend_requests": "Запросы на чтение" + "friend_requests": "Запросы на чтение", + "bookmarks": "Закладки" }, "notifications": { "broken_favorite": "Неизвестный статус, ищем...", @@ -366,6 +367,10 @@ "show_new": "Показать новые", "up_to_date": "Обновлено" }, + "status": { + "bookmark": "В закладки", + "unbookmark": "Удалить из закладок" + }, "user_card": { "block": "Заблокировать", "blocked": "Заблокирован", diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 4d3f8031..7fbf685c 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -62,7 +62,8 @@ export const defaultState = () => ({ publicAndExternal: emptyTl(), friends: emptyTl(), tag: emptyTl(), - dms: emptyTl() + dms: emptyTl(), + bookmarks: emptyTl() } }) @@ -163,8 +164,7 @@ const removeStatusFromGlobalStorage = (state, status) => { } } -const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, - noIdUpdate = false, userId }) => { +const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => { // Sanity check if (!isArray(statuses)) { return false @@ -173,8 +173,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us const allStatuses = state.allStatuses const timelineObject = state.timelines[timeline] - const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0 - const minNew = statuses.length > 0 ? minBy(statuses, 'id').id : 0 + // Mismatch between API pagination and our internal minId/maxId tracking systems: + // pagination.maxId is the oldest of the returned statuses when fetching older, + // and pagination.minId is the newest when fetching newer. The names come directly + // from the arguments they're supposed to be passed as for the next fetch. + const minNew = pagination.maxId || (statuses.length > 0 ? minBy(statuses, 'id').id : 0) + const maxNew = pagination.minId || (statuses.length > 0 ? maxBy(statuses, 'id').id : 0) + const newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0 const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0 @@ -315,7 +320,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us }) // Keep the visible statuses sorted - if (timeline) { + if (timeline && !(timeline === 'bookmarks')) { sortTimeline(timelineObject) } } @@ -463,6 +468,14 @@ export const mutations = { newStatus.rebloggedBy.push(user) } }, + setBookmarked (state, { status, value }) { + const newStatus = state.allStatusesObject[status.id] + newStatus.bookmarked = value + }, + setBookmarkedConfirm (state, { status }) { + const newStatus = state.allStatusesObject[status.id] + newStatus.bookmarked = status.bookmarked + }, setDeleted (state, { status }) { const newStatus = state.allStatusesObject[status.id] newStatus.deleted = true @@ -590,8 +603,8 @@ export const mutations = { const statuses = { state: defaultState(), actions: { - addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) { - commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId }) + addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) { + commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination }) }, addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) { commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters }) @@ -666,6 +679,20 @@ const statuses = { rootState.api.backendInteractor.unretweet({ id: status.id }) .then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser })) }, + bookmark ({ rootState, commit }, status) { + commit('setBookmarked', { status, value: true }) + rootState.api.backendInteractor.bookmarkStatus({ id: status.id }) + .then(status => { + commit('setBookmarkedConfirm', { status }) + }) + }, + unbookmark ({ rootState, commit }, status) { + commit('setBookmarked', { status, value: false }) + rootState.api.backendInteractor.unbookmarkStatus({ id: status.id }) + .then(status => { + commit('setBookmarkedConfirm', { status }) + }) + }, queueFlush ({ rootState, commit }, { timeline, id }) { commit('queueFlush', { timeline, id }) }, diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 7e5e9645..c9ec88b7 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,5 +1,5 @@ import { each, map, concat, last, get } from 'lodash' -import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js' import { RegistrationError, StatusCodeError } from '../errors/errors' /* eslint-env browser */ @@ -50,6 +50,7 @@ const MASTODON_USER_URL = '/api/v1/accounts' const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses` const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}` +const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks' const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/' const MASTODON_USER_MUTES_URL = '/api/v1/mutes/' const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block` @@ -58,6 +59,8 @@ const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute` const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute` const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe` const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe` +const MASTODON_BOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/bookmark` +const MASTODON_UNBOOKMARK_STATUS_URL = id => `/api/v1/statuses/${id}/unbookmark` const MASTODON_POST_STATUS_URL = '/api/v1/statuses' const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes` @@ -510,7 +513,8 @@ const fetchTimeline = ({ user: MASTODON_USER_TIMELINE_URL, media: MASTODON_USER_TIMELINE_URL, favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, - tag: MASTODON_TAG_TIMELINE_URL + tag: MASTODON_TAG_TIMELINE_URL, + bookmarks: MASTODON_BOOKMARK_TIMELINE_URL } const isNotifications = timeline === 'notifications' const params = [] @@ -539,7 +543,7 @@ const fetchTimeline = ({ if (timeline === 'public' || timeline === 'publicAndExternal') { params.push(['only_media', false]) } - if (timeline !== 'favorites') { + if (timeline !== 'favorites' && timeline !== 'bookmarks') { params.push(['with_muted', withMuted]) } if (replyVisibility !== 'all') { @@ -552,16 +556,20 @@ const fetchTimeline = ({ url += `?${queryString}` let status = '' let statusText = '' + let pagination = {} return fetch(url, { headers: authHeaders(credentials) }) .then((data) => { status = data.status statusText = data.statusText + pagination = parseLinkHeaderPagination(data.headers.get('Link'), { + flakeId: timeline !== 'bookmarks' && timeline !== 'notifications' + }) return data }) .then((data) => data.json()) .then((data) => { if (!data.error) { - return data.map(isNotifications ? parseNotification : parseStatus) + return { data: data.map(isNotifications ? parseNotification : parseStatus), pagination } } else { data.status = status data.statusText = statusText @@ -612,6 +620,22 @@ const unretweet = ({ id, credentials }) => { .then((data) => parseStatus(data)) } +const bookmarkStatus = ({ id, credentials }) => { + return promisedRequest({ + url: MASTODON_BOOKMARK_STATUS_URL(id), + headers: authHeaders(credentials), + method: 'POST' + }) +} + +const unbookmarkStatus = ({ id, credentials }) => { + return promisedRequest({ + url: MASTODON_UNBOOKMARK_STATUS_URL(id), + headers: authHeaders(credentials), + method: 'POST' + }) +} + const postStatus = ({ credentials, status, @@ -1150,6 +1174,8 @@ const apiService = { unfavorite, retweet, unretweet, + bookmarkStatus, + unbookmarkStatus, postStatus, deleteStatus, uploadMedia, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 3bdb92f3..0d27dc97 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -1,4 +1,5 @@ import escape from 'escape-html' +import parseLinkHeader from 'parse-link-header' import { isStatusNotification } from '../notification_utils/notification_utils.js' const qvitterStatusType = (status) => { @@ -232,6 +233,8 @@ export const parseStatus = (data) => { output.repeated = data.reblogged output.repeat_num = data.reblogs_count + output.bookmarked = data.bookmarked + output.type = data.reblog ? 'retweet' : 'status' output.nsfw = data.sensitive @@ -381,3 +384,16 @@ const isNsfw = (status) => { const nsfwRegex = /#nsfw/i return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex) } + +export const parseLinkHeaderPagination = (linkHeader, opts = {}) => { + const flakeId = opts.flakeId + const parsedLinkHeader = parseLinkHeader(linkHeader) + if (!parsedLinkHeader) return + const maxId = parsedLinkHeader.next.max_id + const minId = parsedLinkHeader.prev.min_id + + return { + maxId: flakeId ? maxId : parseInt(maxId, 10), + minId: flakeId ? minId : parseInt(minId, 10) + } +} diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 64499a1b..4644e449 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -41,7 +41,7 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => { const fetchNotifications = ({ store, args, older }) => { return apiService.fetchTimeline(args) - .then((notifications) => { + .then(({ data: notifications }) => { update({ store, notifications, older }) return notifications }, () => store.dispatch('setNotificationsError', { value: true })) diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 30fb26bd..214294eb 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -2,7 +2,7 @@ import { camelCase } from 'lodash' import apiService from '../api/api.service.js' -const update = ({ store, statuses, timeline, showImmediately, userId }) => { +const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => { const ccTimeline = camelCase(timeline) store.dispatch('setError', { value: false }) @@ -12,7 +12,8 @@ const update = ({ store, statuses, timeline, showImmediately, userId }) => { timeline: ccTimeline, userId, statuses, - showImmediately + showImmediately, + pagination }) } @@ -47,16 +48,18 @@ const fetchAndUpdate = ({ const numStatusesBeforeFetch = timelineData.statuses.length return apiService.fetchTimeline(args) - .then((statuses) => { - if (statuses.error) { - store.dispatch('setErrorData', { value: statuses }) + .then(response => { + if (response.error) { + store.dispatch('setErrorData', { value: response }) return } + + const { data: statuses, pagination } = response if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) { store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId }) } - update({ store, statuses, timeline, showImmediately, userId }) - return statuses + update({ store, statuses, timeline, showImmediately, userId, pagination }) + return { statuses, pagination } }, () => store.dispatch('setError', { value: true })) } diff --git a/static/fontello.json b/static/fontello.json old mode 100755 new mode 100644 index ac3f0a18..6083c0bf --- a/static/fontello.json +++ b/static/fontello.json @@ -375,6 +375,18 @@ "css": "download", "code": 59429, "src": "fontawesome" + }, + { + "uid": "f04a5d24e9e659145b966739c4fde82a", + "css": "bookmark", + "code": 59430, + "src": "fontawesome" + }, + { + "uid": "2f5ef6f6b7aaebc56458ab4e865beff5", + "css": "bookmark-empty", + "code": 61591, + "src": "fontawesome" } ] } \ No newline at end of file diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js index ccb57942..e1f7a958 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -1,4 +1,4 @@ -import { parseStatus, parseUser, parseNotification, addEmojis } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseUser, parseNotification, addEmojis, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' import mastoapidata from '../../../../fixtures/mastoapi.json' import qvitterapidata from '../../../../fixtures/statuses.json' @@ -383,4 +383,24 @@ describe('API Entities normalizer', () => { expect(result).to.include('title=\':[a-z] {|}*:\'') }) }) + + describe('Link header pagination', () => { + it('Parses min and max ids as integers', () => { + const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"' + const result = parseLinkHeaderPagination(linkHeader) + expect(result).to.eql({ + 'maxId': 861676, + 'minId': 861741 + }) + }) + + it('Parses min and max ids as flakes', () => { + const linkHeader = '<http://example.com/api/v1/timelines/home?max_id=9waQx5IIS48qVue2Ai>; rel="next", <http://example.com/api/v1/timelines/home?min_id=9wi61nIPnfn674xgie>; rel="prev"' + const result = parseLinkHeaderPagination(linkHeader, { flakeId: true }) + expect(result).to.eql({ + 'maxId': '9waQx5IIS48qVue2Ai', + 'minId': '9wi61nIPnfn674xgie' + }) + }) + }) }) diff --git a/yarn.lock b/yarn.lock index f05b00b1..09316863 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5751,6 +5751,13 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" +parse-link-header@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7" + integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc= + dependencies: + xtend "~4.0.1" + parseqs@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" From 9b40cf43d80f61d2afdaed757e4f873bf2dfd0a4 Mon Sep 17 00:00:00 2001 From: kPherox <admin@mail.kr-kp.com> Date: Sat, 4 Jul 2020 18:42:15 +0900 Subject: [PATCH 25/26] fix height for emoji panel of settings modal --- src/components/settings_modal/settings_modal.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss index 833ff89a..0da4d9a8 100644 --- a/src/components/settings_modal/settings_modal.scss +++ b/src/components/settings_modal/settings_modal.scss @@ -30,7 +30,7 @@ height: 100vh; } - .panel-body { + >.panel-body { height: 100%; overflow-y: hidden; From 9178908c1ea8d0ad99b4a631abb9594edc3765bf Mon Sep 17 00:00:00 2001 From: Shpuld Shpludson <shp@cock.li> Date: Sun, 5 Jul 2020 06:54:12 +0000 Subject: [PATCH 26/26] Apply suggestion to src/components/notifications/notifications.js --- src/components/notifications/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 30187072..d8a327b0 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -30,7 +30,7 @@ const Notifications = { created () { const store = this.$store const credentials = store.state.users.currentUser.credentials - notificationsFetcher.fetchAndUpdate({ store: this.$store, credentials }) + notificationsFetcher.fetchAndUpdate({ store, credentials }) }, computed: { mainClass () {