Merge branch 'issue-436-mastoapi-notifications' into 'develop'

#436: integrate mastoAPI notifications

Closes #436

See merge request pleroma/pleroma-fe!678
This commit is contained in:
HJ 2019-04-10 17:28:42 +00:00
commit 84505e01f5
15 changed files with 205 additions and 6790 deletions

View file

@ -28,21 +28,21 @@ const Notification = {
},
computed: {
userClass () {
return highlightClass(this.notification.action.user)
return highlightClass(this.notification.from_profile)
},
userStyle () {
const highlight = this.$store.state.config.highlight
const user = this.notification.action.user
const user = this.notification.from_profile
return highlightStyle(highlight[user.screen_name])
},
userInStore () {
return this.$store.getters.findUser(this.notification.action.user.id)
return this.$store.getters.findUser(this.notification.from_profile.id)
},
user () {
if (this.userInStore) {
return this.userInStore
}
return {}
return this.notification.from_profile
}
}
}

View file

@ -1,15 +1,20 @@
<template>
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
<status
v-if="notification.type === 'mention'"
:compact="true"
:statusoid="notification.status"
>
</status>
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]" v-else>
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
<a class='avatar-container' :href="notification.from_profile.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.from_profile.profile_image_url_original" />
</a>
<div class='notification-right'>
<UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded" />
<span class="notification-details">
<div class="name-and-action">
<span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
<span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
<span class="username" v-if="!!notification.from_profile.name_html" :title="'@'+notification.from_profile.screen_name" v-html="notification.from_profile.name_html"></span>
<span class="username" v-else :title="'@'+notification.from_profile.screen_name">{{ notification.from_profile.name }}</span>
<span v-if="notification.type === 'like'">
<i class="fa icon-star lit"></i>
<small>{{$t('notifications.favorited_you')}}</small>
@ -23,19 +28,24 @@
<small>{{$t('notifications.followed_you')}}</small>
</span>
</div>
<div class="timeago">
<div class="timeago" v-if="notification.type === 'follow'">
<span class="faint">
<timeago :since="notification.created_at" :auto-update="240"></timeago>
</span>
</div>
<div class="timeago" v-else>
<router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
<timeago :since="notification.action.created_at" :auto-update="240"></timeago>
<timeago :since="notification.created_at" :auto-update="240"></timeago>
</router-link>
</div>
</span>
<div class="follow-text" v-if="notification.type === 'follow'">
<router-link :to="userProfileLink(notification.action.user)">
@{{notification.action.user.screen_name}}
<router-link :to="userProfileLink(notification.from_profile)">
@{{notification.from_profile.screen_name}}
</router-link>
</div>
<template v-else>
<status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
<status class="faint" :compact="true" :statusoid="notification.action" :noHeading="true"></status>
</template>
</div>
</div>

View file

@ -49,7 +49,7 @@ const Notifications = {
},
methods: {
markAsSeen () {
this.$store.dispatch('markNotificationsAsSeen', this.visibleNotifications)
this.$store.dispatch('markNotificationsAsSeen')
},
fetchOlderNotifications () {
const store = this.$store

View file

@ -12,7 +12,7 @@
<button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button>
</div>
<div class="panel-body">
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
<div v-for="notification in visibleNotifications" :key="notification.id" class="notification" :class='{"unseen": !notification.seen}'>
<div class="notification-overlay"></div>
<notification :notification="notification"></notification>
</div>

View file

@ -59,6 +59,11 @@ const persistedStateOptions = {
const persistedState = await createPersistedState(persistedStateOptions)
const store = new Vuex.Store({
modules: {
i18n: {
getters: {
i18n: () => i18n
}
},
interface: interfaceModule,
instance: instanceModule,
statuses: statusesModule,

View file

@ -272,12 +272,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}
}
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => {
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
const allStatuses = state.allStatuses
const allStatusesObject = state.allStatusesObject
each(notifications, (notification) => {
if (notification.type !== 'follow') {
notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
}
// Only add a new notification if we don't have one for the same action
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
@ -293,15 +295,32 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
if ('Notification' in window && window.Notification.permission === 'granted') {
const notifObj = {}
const action = notification.action
const title = action.user.name
notifObj.icon = action.user.profile_image_url
notifObj.body = action.text // there's a problem that it doesn't put a space before links tho
const status = notification.status
const title = notification.from_profile.name
notifObj.icon = notification.from_profile.profile_image_url
let i18nString
switch (notification.type) {
case 'like':
i18nString = 'favorited_you'
break
case 'repeat':
i18nString = 'repeated_you'
break
case 'follow':
i18nString = 'followed_you'
break
}
if (i18nString) {
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
} else {
notifObj.body = notification.status.text
}
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
action.attachments[0].mimetype.startsWith('image/')) {
notifObj.image = action.attachments[0].url
if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
status.attachments[0].mimetype.startsWith('image/')) {
notifObj.image = status.attachments[0].url
}
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
@ -421,8 +440,8 @@ const statuses = {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
},
addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) {
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older })
addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
},
setError ({ rootState, commit }, { value }) {
commit('setError', { value })

View file

@ -144,8 +144,10 @@ export const mutations = {
status.user = state.usersObject[status.user.id]
},
setUserForNotification (state, notification) {
if (notification.type !== 'follow') {
notification.action.user = state.usersObject[notification.action.user.id]
notification.from_profile = state.usersObject[notification.action.user.id]
}
notification.from_profile = state.usersObject[notification.from_profile.id]
},
setColor (state, { user: { id }, highlighted }) {
const user = state.usersObject[id]

View file

@ -8,7 +8,6 @@ const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
const BANNER_UPDATE_URL = '/api/account/update_profile_banner.json'
const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json'
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
@ -23,6 +22,7 @@ const ADMIN_USER_URL = '/api/pleroma/admin/user'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
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`
@ -442,7 +442,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
friends: MASTODON_USER_HOME_TIMELINE_URL,
mentions: MENTIONS_URL,
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
notifications: QVITTER_USER_NOTIFICATIONS_URL,
notifications: MASTODON_USER_NOTIFICATIONS_URL,
'publicAndExternal': MASTODON_PUBLIC_TIMELINE,
user: MASTODON_USER_TIMELINE_URL,
media: MASTODON_USER_TIMELINE_URL,

View file

@ -39,7 +39,7 @@ export const parseUser = (data) => {
return output
}
// output.name = ??? missing
output.name = data.display_name
output.name_html = addEmojis(data.display_name, data.emojis)
// output.description = ??? missing
@ -74,7 +74,7 @@ export const parseUser = (data) => {
}
}
// Missing, trying to recover
// TODO: handle is_local
output.is_local = !output.screen_name.includes('@')
} else {
output.screen_name = data.screen_name
@ -239,7 +239,6 @@ export const parseStatus = (data) => {
output.in_reply_to_status_id = data.in_reply_to_status_id
output.in_reply_to_user_id = data.in_reply_to_user_id
output.in_reply_to_screen_name = data.in_reply_to_screen_name
output.statusnet_conversation_id = data.statusnet_conversation_id
if (output.type === 'retweet') {
@ -290,9 +289,11 @@ export const parseNotification = (data) => {
if (masto) {
output.type = mastoDict[data.type] || data.type
// output.seen = ??? missing
output.status = parseStatus(data.status)
output.action = output.status // not sure
output.seen = data.pleroma.is_seen
output.status = output.type === 'follow'
? null
: parseStatus(data.status)
output.action = output.status // TODO: Refactor, this is unneeded
output.from_profile = parseUser(data.account)
} else {
const parsedNotice = parseStatus(data.notice)

View file

@ -10,8 +10,8 @@ export const visibleTypes = store => ([
].filter(_ => _))
const sortById = (a, b) => {
const seqA = Number(a.action.id)
const seqB = Number(b.action.id)
const seqA = Number(a.id)
const seqB = Number(b.id)
const isSeqA = !Number.isNaN(seqA)
const isSeqB = !Number.isNaN(seqB)
if (isSeqA && isSeqB) {
@ -21,7 +21,7 @@ const sortById = (a, b) => {
} else if (!isSeqA && isSeqB) {
return -1
} else {
return a.action.id > b.action.id ? -1 : 1
return a.id > b.id ? -1 : 1
}
}

View file

@ -11,26 +11,32 @@ const fetchAndUpdate = ({store, credentials, older = false}) => {
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications
args['timeline'] = 'notifications'
if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) {
args['until'] = timelineData.minId
}
return fetchNotifications({ store, args, older })
} else {
// load unread notifications repeadedly to provide consistency between browser tabs
// fetch new notifications
if (timelineData.maxId !== Number.POSITIVE_INFINITY) {
args['since'] = timelineData.maxId
}
const result = fetchNotifications({ store, args, older })
// load unread notifications repeatedly to provide consistency between browser tabs
const notifications = timelineData.data
const unread = notifications.filter(n => !n.seen).map(n => n.id)
if (!unread.length) {
args['since'] = timelineData.maxId
} else {
args['since'] = Math.min(...unread) - 1
if (timelineData.maxId !== Math.max(...unread)) {
args['until'] = Math.max(...unread, args['since'] + 20)
if (unread.length) {
args['since'] = Math.min(...unread)
fetchNotifications({ store, args, older })
}
return result
}
}
args['timeline'] = 'notifications'
const fetchNotifications = ({ store, args, older }) => {
return apiService.fetchTimeline(args)
.then((notifications) => {
update({ store, notifications, older })

View file

@ -58,7 +58,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639",
"url": "https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -127,7 +130,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/6634d32b-96a8-4852-a3db-ac8730715779",
"url": "https://shigusegubu.club/objects/6634d32b-96a8-4852-a3db-ac8730715779",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -250,7 +256,10 @@
"tags": [],
"uri": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
"url": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
},
"reblogged": false,
"reblogs_count": 0,
@ -260,7 +269,10 @@
"tags": [],
"uri": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
"url": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -329,7 +341,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/0f963ca1-a263-41ca-a43c-b5d26d0a08e9",
"url": "https://shigusegubu.club/objects/0f963ca1-a263-41ca-a43c-b5d26d0a08e9",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -390,7 +405,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/3f809bd8-656f-4a29-81d4-80eed6916eb0",
"url": "https://shigusegubu.club/objects/3f809bd8-656f-4a29-81d4-80eed6916eb0",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -516,7 +534,10 @@
}],
"uri": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
"url": "https://social.super-niche.club/notice/2353002",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
},
"reblogged": false,
"reblogs_count": 0,
@ -529,7 +550,10 @@
}],
"uri": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
"url": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -657,7 +681,10 @@
"tags": [],
"uri": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
"url": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
},
"reblogged": false,
"reblogs_count": 0,
@ -667,7 +694,10 @@
"tags": [],
"uri": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
"url": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -733,7 +763,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/38b1bc44-15d8-40dd-b1aa-937e0ff4a86d",
"url": "https://shigusegubu.club/objects/38b1bc44-15d8-40dd-b1aa-937e0ff4a86d",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -794,7 +827,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/fbff5da4-a517-42a9-bca9-17cae8cf2542",
"url": "https://shigusegubu.club/objects/fbff5da4-a517-42a9-bca9-17cae8cf2542",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -850,7 +886,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/4007d659-27c6-4577-be10-fd134f5e4e7e",
"url": "https://shigusegubu.club/objects/4007d659-27c6-4577-be10-fd134f5e4e7e",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -906,7 +945,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/59912d51-1cc6-4dc7-828c-f167e6c8b391",
"url": "https://shigusegubu.club/objects/59912d51-1cc6-4dc7-828c-f167e6c8b391",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -962,7 +1004,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/62690bce-3f49-4047-9c8e-8941f2f79e10",
"url": "https://shigusegubu.club/objects/62690bce-3f49-4047-9c8e-8941f2f79e10",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -1023,7 +1068,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/818f3dd0-2ff8-4def-a170-e4d4c405f387",
"url": "https://shigusegubu.club/objects/818f3dd0-2ff8-4def-a170-e4d4c405f387",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -1084,7 +1132,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/0783a193-c097-488d-8944-47df9372cd6e",
"url": "https://shigusegubu.club/objects/0783a193-c097-488d-8944-47df9372cd6e",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -1145,7 +1196,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/145d5252-7b8e-467d-9f36-1db0818f452f",
"url": "https://shigusegubu.club/objects/145d5252-7b8e-467d-9f36-1db0818f452f",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -1252,7 +1306,10 @@
"tags": [],
"uri": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
"url": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
},
"reblogged": false,
"reblogs_count": 0,
@ -1262,7 +1319,10 @@
"tags": [],
"uri": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
"url": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -1323,7 +1383,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/d4eb7c46-02f9-4b1f-83af-926cefa21f33",
"url": "https://shigusegubu.club/objects/d4eb7c46-02f9-4b1f-83af-926cefa21f33",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -1446,7 +1509,10 @@
"tags": [],
"uri": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
"url": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
},
"reblogged": false,
"reblogs_count": 0,
@ -1456,7 +1522,10 @@
"tags": [],
"uri": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
"url": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -1517,7 +1586,10 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/f472f4ed-8b0b-492f-9d53-d69eda79629d",
"url": "https://shigusegubu.club/objects/f472f4ed-8b0b-492f-9d53-d69eda79629d",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}, {
"account": {
"acct": "hj",
@ -1578,5 +1650,8 @@
"tags": [],
"uri": "https://shigusegubu.club/objects/d6fb4fd2-1f6a-4446-a1a6-5edd34050096",
"url": "https://shigusegubu.club/objects/d6fb4fd2-1f6a-4446-a1a6-5edd34050096",
"visibility": "public"
"visibility": "public",
"pleroma": {
"local": true
}
}]

View file

@ -129,7 +129,10 @@ const makeMockStatusMasto = (overrides = {}) => {
tags: [],
uri: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
url: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
visibility: 'public'
visibility: 'public',
pleroma: {
local: true
}
}, overrides)
}

View file

@ -9,14 +9,17 @@ describe('NotificationUtils', () => {
notifications: {
data: [
{
id: 1,
action: { id: '1' },
type: 'like'
},
{
id: 2,
action: { id: '2' },
type: 'mention'
},
{
id: 3,
action: { id: '3' },
type: 'repeat'
}
@ -35,10 +38,12 @@ describe('NotificationUtils', () => {
const expected = [
{
action: { id: '3' },
id: 3,
type: 'repeat'
},
{
action: { id: '1' },
id: 1,
type: 'like'
}
]

6711
yarn.lock

File diff suppressed because it is too large Load diff