commit: c67e9daf068c5a7eafaa7ce6a6418c8916a4f118
parent: af3e69743e3192898f185fbc867defa1d155a4d4
Author: Shpuld Shpludson <shp@cock.li>
Date: Fri, 1 May 2020 20:24:25 +0000
Merge branch 'follow-request-notification' into 'develop'
Add support for follow request notifications
Closes #823 and #822
See merge request pleroma/pleroma-fe!1093
Diffstat:
11 files changed, 129 insertions(+), 20 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### 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
- Favorite/Repeat avatars not showing up on private instances/non-public posts
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,21 @@ 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('updateNotification', {
+ id: this.notification.id,
+ updater: notification => {
+ notification.type = 'follow'
+ }
+ })
+ },
+ denyUser () {
+ this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
+ this.$store.dispatch('removeFollowRequest', this.user)
+ this.$store.dispatch('dismissNotification', { id: this.notification.id })
}
},
computed: {
@@ -57,6 +73,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
@@ -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 add-reaction-button"
+ :title="$t('tool_tip.accept_follow_request')"
+ @click="approveUser()"
+ />
+ <i
+ class="icon-cancel button-icon add-reaction-button"
+ :title="$t('tool_tip.accept_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
@@ -82,6 +82,16 @@
.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 +153,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/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,13 @@ export const mutations = {
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 +691,13 @@ const statuses = {
credentials: rootState.users.currentUser.credentials
})
},
+ dismissNotification ({ rootState, commit }, { id }) {
+ rootState.api.backendInteractor.dismissNotification({ id })
+ .then(() => commit('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
@@ -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`
@@ -1006,6 +1007,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
@@ -1161,6 +1171,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