Add support for follow request notifications

This commit is contained in:
eugenijm 2020-04-25 07:04:39 +03:00 committed by Shpuld Shpuldson
parent 4d1a674634
commit ab3c0e8512
11 changed files with 131 additions and 23 deletions

View file

@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed ### Changed
- Emoji autocomplete will match any part of the word and not just start, for example :drool will now helpfully suggest :blobcatdrool: and :blobcatdroolreach: - 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 ## [2.0.2] - 2020-04-08
### Fixed ### Fixed
- Favorite/Repeat avatars not showing up on private instances/non-public posts - Favorite/Repeat avatars not showing up on private instances/non-public posts

View file

@ -2,6 +2,7 @@ import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.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 { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -32,6 +33,21 @@ const Notification = {
}, },
toggleMute () { toggleMute () {
this.unmuted = !this.unmuted 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: { computed: {
@ -57,6 +73,9 @@ const Notification = {
}, },
needMute () { needMute () {
return this.user.muted return this.user.muted
},
isStatusNotification () {
return isStatusNotification(this.notification.type)
} }
} }
} }

View file

@ -74,6 +74,10 @@
<i class="fa icon-user-plus lit" /> <i class="fa icon-user-plus lit" />
<small>{{ $t('notifications.followed_you') }}</small> <small>{{ $t('notifications.followed_you') }}</small>
</span> </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'"> <span v-if="notification.type === 'move'">
<i class="fa icon-arrow-curved lit" /> <i class="fa icon-arrow-curved lit" />
<small>{{ $t('notifications.migrated_to') }}</small> <small>{{ $t('notifications.migrated_to') }}</small>
@ -87,18 +91,7 @@
</span> </span>
</div> </div>
<div <div
v-if="notification.type === 'follow' || notification.type === 'move'" v-if="isStatusNotification"
class="timeago"
>
<span class="faint">
<Timeago
:time="notification.created_at"
:auto-update="240"
/>
</span>
</div>
<div
v-else
class="timeago" class="timeago"
> >
<router-link <router-link
@ -112,6 +105,17 @@
/> />
</router-link> </router-link>
</div> </div>
<div
v-else
class="timeago"
>
<span class="faint">
<Timeago
:time="notification.created_at"
:auto-update="240"
/>
</span>
</div>
<a <a
v-if="needMute" v-if="needMute"
href="#" href="#"
@ -119,12 +123,30 @@
><i class="button-icon icon-eye-off" /></a> ><i class="button-icon icon-eye-off" /></a>
</span> </span>
<div <div
v-if="notification.type === 'follow'" v-if="notification.type === 'follow' || notification.type === 'follow_request'"
class="follow-text" class="follow-text"
> >
<router-link :to="userProfileLink"> <router-link
:to="userProfileLink"
class="follow-name"
>
@{{ notification.from_profile.screen_name }} @{{ notification.from_profile.screen_name }}
</router-link> </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>
<div <div
v-else-if="notification.type === 'move'" v-else-if="notification.type === 'move'"

View file

@ -82,6 +82,16 @@
.follow-text, .move-text { .follow-text, .move-text {
padding: 0.5em 0; padding: 0.5em 0;
overflow-wrap: break-word; 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 { .status-el {
@ -143,6 +153,11 @@
color: var(--cGreen, $fallback--cGreen); color: var(--cGreen, $fallback--cGreen);
} }
.icon-user.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.icon-user-plus.lit { .icon-user-plus.lit {
color: $fallback--cBlue; color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue); color: var(--cBlue, $fallback--cBlue);

View file

@ -124,6 +124,7 @@
"broken_favorite": "Unknown status, searching for it...", "broken_favorite": "Unknown status, searching for it...",
"favorited_you": "favorited your status", "favorited_you": "favorited your status",
"followed_you": "followed you", "followed_you": "followed you",
"follow_request": "wants to follow you",
"load_older": "Load older notifications", "load_older": "Load older notifications",
"notifications": "Notifications", "notifications": "Notifications",
"read": "Read!", "read": "Read!",
@ -697,7 +698,9 @@
"reply": "Reply", "reply": "Reply",
"favorite": "Favorite", "favorite": "Favorite",
"add_reaction": "Add Reaction", "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":{ "upload":{
"error": { "error": {

View file

@ -34,7 +34,8 @@ export const defaultState = {
likes: true, likes: true,
repeats: true, repeats: true,
moves: true, moves: true,
emojiReactions: false emojiReactions: false,
followRequest: true
}, },
webPushNotifications: false, webPushNotifications: false,
muteWords: [], muteWords: [],

View file

@ -13,6 +13,7 @@ import {
omitBy omitBy
} from 'lodash' } from 'lodash'
import { set } from 'vue' import { set } from 'vue'
import { isStatusNotification } from '../services/notification_utils/notification_utils.js'
import apiService from '../services/api/api.service.js' import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.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 }) => { const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
each(notifications, (notification) => { each(notifications, (notification) => {
if (notification.type !== 'follow' && notification.type !== 'move') { if (isStatusNotification(notification.type)) {
notification.action = addStatusToGlobalStorage(state, notification.action).item notification.action = addStatusToGlobalStorage(state, notification.action).item
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
} }
@ -361,13 +362,16 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
case 'move': case 'move':
i18nString = 'migrated_to' i18nString = 'migrated_to'
break break
case 'follow_request':
i18nString = 'follow_request'
break
} }
if (notification.type === 'pleroma:emoji_reaction') { if (notification.type === 'pleroma:emoji_reaction') {
notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji]) notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji])
} else if (i18nString) { } else if (i18nString) {
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString) notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
} else { } else if (isStatusNotification(notification.type)) {
notifObj.body = notification.status.text notifObj.body = notification.status.text
} }
@ -521,6 +525,13 @@ export const mutations = {
notification.seen = true 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 }) { queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = id state.timelines[timeline].flushMarker = id
}, },
@ -680,6 +691,13 @@ const statuses = {
credentials: rootState.users.currentUser.credentials 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) { fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([ Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers({ id }), rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),

View file

@ -29,6 +29,7 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts' const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' 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_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite` const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog` const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
@ -1010,6 +1011,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 = {} }) => { export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({ return Object.entries({
...(credentials ...(credentials
@ -1165,6 +1175,7 @@ const apiService = {
denyUser, denyUser,
suggestions, suggestions,
markNotificationsAsSeen, markNotificationsAsSeen,
dismissNotification,
vote, vote,
fetchPoll, fetchPoll,
fetchFavoritedByUsers, fetchFavoritedByUsers,

View file

@ -1,4 +1,5 @@
import escape from 'escape-html' import escape from 'escape-html'
import { isStatusNotification } from '../notification_utils/notification_utils.js'
const qvitterStatusType = (status) => { const qvitterStatusType = (status) => {
if (status.is_post_verb) { if (status.is_post_verb) {
@ -346,9 +347,7 @@ export const parseNotification = (data) => {
if (masto) { if (masto) {
output.type = mastoDict[data.type] || data.type output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen output.seen = data.pleroma.is_seen
output.status = output.type === 'follow' || output.type === 'move' output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
? null
: parseStatus(data.status)
output.action = output.status // TODO: Refactor, this is unneeded output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move' output.target = output.type !== 'move'
? null ? null

View file

@ -1,4 +1,4 @@
import { filter, sortBy } from 'lodash' import { filter, sortBy, includes } from 'lodash'
export const notificationsFromStore = store => store.state.statuses.notifications.data 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.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat', store.state.config.notificationVisibility.repeats && 'repeat',
store.state.config.notificationVisibility.follows && 'follow', store.state.config.notificationVisibility.follows && 'follow',
store.state.config.notificationVisibility.followRequest && 'follow_request',
store.state.config.notificationVisibility.moves && 'move', store.state.config.notificationVisibility.moves && 'move',
store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction' store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
].filter(_ => _)) ].filter(_ => _))
const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
export const isStatusNotification = (type) => includes(statusNotifications, type)
const sortById = (a, b) => { const sortById = (a, b) => {
const seqA = Number(a.id) const seqA = Number(a.id)
const seqB = Number(b.id) const seqB = Number(b.id)

View file

@ -345,6 +345,18 @@
"css": "link", "css": "link",
"code": 59427, "code": 59427,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "8b80d36d4ef43889db10bc1f0dc9a862",
"css": "user",
"code": 59428,
"src": "fontawesome"
},
{
"uid": "12f4ece88e46abd864e40b35e05b11cd",
"css": "ok",
"code": 59431,
"src": "fontawesome"
} }
] ]
} }