logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: a0f780c4550b77d4574e0de8932a2dff288784a3
parent 8e049297ff6a125298ba9d06a3a4a563b231360f
Author: Shpuld Shpludson <shp@cock.li>
Date:   Sat,  2 May 2020 14:17:27 +0000

Merge branch 'rc/2.0.3' into 'master'

Update MASTER with 2.0.3 for real

See merge request pleroma/pleroma-fe!1099

Diffstat:

MCHANGELOG.md14+++++++++++++-
Msrc/components/emoji_input/suggestor.js24++++++++++++++++++------
Msrc/components/follow_request_card/follow_request_card.js22+++++++++++++++++++++-
Msrc/components/notification/notification.js22++++++++++++++++++++++
Msrc/components/notification/notification.vue46++++++++++++++++++++++++++++++++++------------
Msrc/components/notifications/notifications.scss34++++++++++++++++++++++++++++++++++
Msrc/components/status/status.js35+++++++++++++++--------------------
Msrc/i18n/en.json5++++-
Msrc/modules/config.js3++-
Msrc/modules/statuses.js37+++++++++++++++++++++++++++++++++++--
Msrc/services/api/api.service.js25++++++++++++++++++++-----
Msrc/services/entity_normalizer/entity_normalizer.service.js5++---
Msrc/services/notification_utils/notification_utils.js7++++++-
Mstatic/fontello.json15++++++++++++++-
14 files changed, 240 insertions(+), 54 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -2,8 +2,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - ## [Unreleased] +### Changed +- Removed the use of with_move parameters when fetching notifications + +## [2.0.3] - 2020-05-02 +### Fixed +- Show more/less works correctly with auto-collapsed subjects and long posts +- RTL characters won't look messed up in notifications + +### Changed +- Emoji autocomplete will match any part of the word and not just start, for example :drool will now helpfully suggest :blobcatdrool: and :blobcatdroolreach: + +### Add +- Follow request notification support ## [2.0.2] - 2020-04-08 ### Fixed diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js @@ -29,17 +29,29 @@ export default data => input => { export const suggestEmoji = emojis => input => { const noPrefix = input.toLowerCase().substr(1) return emojis - .filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix)) + .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix)) .sort((a, b) => { let aScore = 0 let bScore = 0 - // Make custom emojis a priority - aScore += a.imageUrl ? 10 : 0 - bScore += b.imageUrl ? 10 : 0 + // An exact match always wins + aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0 + bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0 - // Sort alphabetically - const alphabetically = a.displayText > b.displayText ? 1 : -1 + // Prioritize custom emoji a lot + aScore += a.imageUrl ? 100 : 0 + bScore += b.imageUrl ? 100 : 0 + + // Prioritize prefix matches somewhat + aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0 + bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0 + + // Sort by length + aScore -= a.displayText.length + bScore -= b.displayText.length + + // Break ties alphabetically + const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5 return bScore - aScore + alphabetically }) diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js @@ -1,4 +1,5 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue' +import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js' const FollowRequestCard = { props: ['user'], @@ -6,13 +7,32 @@ const FollowRequestCard = { BasicUserCard }, methods: { + findFollowRequestNotificationId () { + const notif = notificationsFromStore(this.$store).find( + (notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request' + ) + return notif && notif.id + }, approveUser () { this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.dispatch('removeFollowRequest', this.user) + + const notifId = this.findFollowRequestNotificationId() + this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId }) + this.$store.dispatch('updateNotification', { + id: notifId, + updater: notification => { + notification.type = 'follow' + } + }) }, denyUser () { + const notifId = this.findFollowRequestNotificationId() this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) - this.$store.dispatch('removeFollowRequest', this.user) + .then(() => { + this.$store.dispatch('dismissNotificationLocal', { id: notifId }) + this.$store.dispatch('removeFollowRequest', this.user) + }) } } } diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js @@ -2,6 +2,7 @@ import Status from '../status/status.vue' import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' +import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -32,6 +33,24 @@ const Notification = { }, toggleMute () { this.unmuted = !this.unmuted + }, + approveUser () { + this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) + this.$store.dispatch('removeFollowRequest', this.user) + this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id }) + this.$store.dispatch('updateNotification', { + id: this.notification.id, + updater: notification => { + notification.type = 'follow' + } + }) + }, + denyUser () { + this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) + .then(() => { + this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id }) + this.$store.dispatch('removeFollowRequest', this.user) + }) } }, computed: { @@ -57,6 +76,9 @@ const Notification = { }, needMute () { return this.user.muted + }, + isStatusNotification () { + return isStatusNotification(this.notification.type) } } } diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -47,7 +47,7 @@ <span class="notification-details"> <div class="name-and-action"> <!-- eslint-disable vue/no-v-html --> - <span + <bdi v-if="!!notification.from_profile.name_html" class="username" :title="'@'+notification.from_profile.screen_name" @@ -74,6 +74,10 @@ <i class="fa icon-user-plus lit" /> <small>{{ $t('notifications.followed_you') }}</small> </span> + <span v-if="notification.type === 'follow_request'"> + <i class="fa icon-user lit" /> + <small>{{ $t('notifications.follow_request') }}</small> + </span> <span v-if="notification.type === 'move'"> <i class="fa icon-arrow-curved lit" /> <small>{{ $t('notifications.migrated_to') }}</small> @@ -87,30 +91,30 @@ </span> </div> <div - v-if="notification.type === 'follow' || notification.type === 'move'" + v-if="isStatusNotification" class="timeago" > - <span class="faint"> + <router-link + v-if="notification.status" + :to="{ name: 'conversation', params: { id: notification.status.id } }" + class="faint-link" + > <Timeago :time="notification.created_at" :auto-update="240" /> - </span> + </router-link> </div> <div v-else class="timeago" > - <router-link - v-if="notification.status" - :to="{ name: 'conversation', params: { id: notification.status.id } }" - class="faint-link" - > + <span class="faint"> <Timeago :time="notification.created_at" :auto-update="240" /> - </router-link> + </span> </div> <a v-if="needMute" @@ -119,12 +123,30 @@ ><i class="button-icon icon-eye-off" /></a> </span> <div - v-if="notification.type === 'follow'" + v-if="notification.type === 'follow' || notification.type === 'follow_request'" class="follow-text" > - <router-link :to="userProfileLink"> + <router-link + :to="userProfileLink" + class="follow-name" + > @{{ notification.from_profile.screen_name }} </router-link> + <div + v-if="notification.type === 'follow_request'" + style="white-space: nowrap;" + > + <i + class="icon-ok button-icon follow-request-accept" + :title="$t('tool_tip.accept_follow_request')" + @click="approveUser()" + /> + <i + class="icon-cancel button-icon follow-request-reject" + :title="$t('tool_tip.reject_follow_request')" + @click="denyUser()" + /> + </div> </div> <div v-else-if="notification.type === 'move'" diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss @@ -79,9 +79,38 @@ } } + .follow-request-accept { + cursor: pointer; + + &:hover { + color: $fallback--text; + color: var(--text, $fallback--text); + } + } + + .follow-request-reject { + cursor: pointer; + + &:hover { + color: $fallback--cRed; + color: var(--cRed, $fallback--cRed); + } + } + + .follow-text, .move-text { padding: 0.5em 0; overflow-wrap: break-word; + display: flex; + justify-content: space-between; + + .follow-name { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } .status-el { @@ -143,6 +172,11 @@ color: var(--cGreen, $fallback--cGreen); } + .icon-user.lit { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + } + .icon-user-plus.lit { color: $fallback--cBlue; color: var(--cBlue, $fallback--cBlue); diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -188,23 +188,22 @@ const Status = { } return this.status.attentions.length > 0 }, + + // 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) + }, + mightHideBecauseTall () { + return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault) + }, hideSubjectStatus () { - if (this.tallStatus && !this.localCollapseSubjectDefault) { - return false - } - return !this.expandingSubject && this.status.summary + return this.mightHideBecauseSubject && !this.expandingSubject }, hideTallStatus () { - if (this.status.summary && this.localCollapseSubjectDefault) { - return false - } - if (this.showingTall) { - return false - } - return this.tallStatus + return this.mightHideBecauseTall && !this.showingTall }, showingMore () { - return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject) + return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject) }, nsfwClickthrough () { if (!this.status.nsfw) { @@ -408,14 +407,10 @@ const Status = { this.userExpanded = !this.userExpanded }, toggleShowMore () { - if (this.showingTall) { - this.showingTall = false - } else if (this.expandingSubject && this.status.summary) { - this.expandingSubject = false - } else if (this.hideTallStatus) { - this.showingTall = true - } else if (this.hideSubjectStatus && this.status.summary) { - this.expandingSubject = true + if (this.mightHideBecauseTall) { + this.showingTall = !this.showingTall + } else if (this.mightHideBecauseSubject) { + this.expandingSubject = !this.expandingSubject } }, generateUserProfileLink (id, name) { diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -124,6 +124,7 @@ "broken_favorite": "Unknown status, searching for it...", "favorited_you": "favorited your status", "followed_you": "followed you", + "follow_request": "wants to follow you", "load_older": "Load older notifications", "notifications": "Notifications", "read": "Read!", @@ -697,7 +698,9 @@ "reply": "Reply", "favorite": "Favorite", "add_reaction": "Add Reaction", - "user_settings": "User Settings" + "user_settings": "User Settings", + "accept_follow_request": "Accept follow request", + "reject_follow_request": "Reject follow request" }, "upload":{ "error": { diff --git a/src/modules/config.js b/src/modules/config.js @@ -34,7 +34,8 @@ export const defaultState = { likes: true, repeats: true, moves: true, - emojiReactions: false + emojiReactions: false, + followRequest: true }, webPushNotifications: false, muteWords: [], diff --git a/src/modules/statuses.js b/src/modules/statuses.js @@ -13,6 +13,7 @@ import { omitBy } from 'lodash' import { set } from 'vue' +import { isStatusNotification } from '../services/notification_utils/notification_utils.js' import apiService from '../services/api/api.service.js' // import parse from '../services/status_parser/status_parser.js' @@ -321,7 +322,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => { each(notifications, (notification) => { - if (notification.type !== 'follow' && notification.type !== 'move') { + if (isStatusNotification(notification.type)) { notification.action = addStatusToGlobalStorage(state, notification.action).item notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item } @@ -361,13 +362,16 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot case 'move': i18nString = 'migrated_to' break + case 'follow_request': + i18nString = 'follow_request' + break } if (notification.type === 'pleroma:emoji_reaction') { notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji]) } else if (i18nString) { notifObj.body = rootGetters.i18n.t('notifications.' + i18nString) - } else { + } else if (isStatusNotification(notification.type)) { notifObj.body = notification.status.text } @@ -521,6 +525,17 @@ export const mutations = { notification.seen = true }) }, + markSingleNotificationAsSeen (state, { id }) { + const notification = find(state.notifications.data, n => n.id === id) + if (notification) notification.seen = true + }, + dismissNotification (state, { id }) { + state.notifications.data = state.notifications.data.filter(n => n.id !== id) + }, + updateNotification (state, { id, updater }) { + const notification = find(state.notifications.data, n => n.id === id) + notification && updater(notification) + }, queueFlush (state, { timeline, id }) { state.timelines[timeline].flushMarker = id }, @@ -680,6 +695,24 @@ const statuses = { credentials: rootState.users.currentUser.credentials }) }, + markSingleNotificationAsSeen ({ rootState, commit }, { id }) { + commit('markSingleNotificationAsSeen', { id }) + apiService.markNotificationsAsSeen({ + single: true, + id, + credentials: rootState.users.currentUser.credentials + }) + }, + dismissNotificationLocal ({ rootState, commit }, { id }) { + commit('dismissNotification', { id }) + }, + dismissNotification ({ rootState, commit }, { id }) { + commit('dismissNotification', { id }) + rootState.api.backendInteractor.dismissNotification({ id }) + }, + updateNotification ({ rootState, commit }, { id, updater }) { + commit('updateNotification', { id, updater }) + }, fetchFavsAndRepeats ({ rootState, commit }, id) { Promise.all([ rootState.api.backendInteractor.fetchFavoritedByUsers({ id }), diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js @@ -4,7 +4,6 @@ import 'whatwg-fetch' import { RegistrationError, StatusCodeError } from '../errors/errors' /* eslint-env browser */ -const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json' const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import' const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' @@ -17,6 +16,7 @@ const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate' const ADMIN_USERS_URL = '/api/pleroma/admin/users' const SUGGESTIONS_URL = '/api/v1/suggestions' const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' +const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read' const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa' const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes' @@ -29,6 +29,7 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' const MASTODON_REGISTRATION_URL = '/api/v1/accounts' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' +const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss` const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite` const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite` const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog` @@ -540,7 +541,7 @@ const fetchTimeline = ({ params.push(['with_move', withMove]) } - params.push(['count', 20]) + params.push(['limit', 20]) params.push(['with_muted', withMuted]) const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') @@ -844,12 +845,16 @@ const suggestions = ({ credentials }) => { }).then((data) => data.json()) } -const markNotificationsAsSeen = ({ id, credentials }) => { +const markNotificationsAsSeen = ({ id, credentials, single = false }) => { const body = new FormData() - body.append('latest_id', id) + if (single) { + body.append('id', id) + } else { + body.append('max_id', id) + } - return fetch(QVITTER_USER_NOTIFICATIONS_READ_URL, { + return fetch(NOTIFICATION_READ_URL, { body, headers: authHeaders(credentials), method: 'POST' @@ -1010,6 +1015,15 @@ const unmuteDomain = ({ domain, credentials }) => { }) } +const dismissNotification = ({ credentials, id }) => { + return promisedRequest({ + url: MASTODON_DISMISS_NOTIFICATION_URL(id), + method: 'POST', + payload: { id }, + credentials + }) +} + export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => { return Object.entries({ ...(credentials @@ -1165,6 +1179,7 @@ const apiService = { denyUser, suggestions, markNotificationsAsSeen, + dismissNotification, vote, fetchPoll, fetchFavoritedByUsers, diff --git 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 { isStatusNotification } from '../notification_utils/notification_utils.js' const qvitterStatusType = (status) => { if (status.is_post_verb) { @@ -346,9 +347,7 @@ export const parseNotification = (data) => { if (masto) { output.type = mastoDict[data.type] || data.type output.seen = data.pleroma.is_seen - output.status = output.type === 'follow' || output.type === 'move' - ? null - : parseStatus(data.status) + output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null output.action = output.status // TODO: Refactor, this is unneeded output.target = output.type !== 'move' ? null diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js @@ -1,4 +1,4 @@ -import { filter, sortBy } from 'lodash' +import { filter, sortBy, includes } from 'lodash' export const notificationsFromStore = store => store.state.statuses.notifications.data @@ -7,10 +7,15 @@ export const visibleTypes = store => ([ store.state.config.notificationVisibility.mentions && 'mention', store.state.config.notificationVisibility.repeats && 'repeat', store.state.config.notificationVisibility.follows && 'follow', + store.state.config.notificationVisibility.followRequest && 'follow_request', store.state.config.notificationVisibility.moves && 'move', store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction' ].filter(_ => _)) +const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction'] + +export const isStatusNotification = (type) => includes(statusNotifications, type) + const sortById = (a, b) => { const seqA = Number(a.id) const seqB = Number(b.id) diff --git a/static/fontello.json b/static/fontello.json @@ -345,6 +345,18 @@ "css": "link", "code": 59427, "src": "fontawesome" + }, + { + "uid": "8b80d36d4ef43889db10bc1f0dc9a862", + "css": "user", + "code": 59428, + "src": "fontawesome" + }, + { + "uid": "12f4ece88e46abd864e40b35e05b11cd", + "css": "ok", + "code": 59431, + "src": "fontawesome" } ] -} +} +\ No newline at end of file