Merge branch 'issue-436-mastoapi-notifications' into shigusegubu
* issue-436-mastoapi-notifications: #436 - apply patch and clean up #436 - update notification #436 - apply patch #436 - keep original naming #436 - keep original naming #436: sync notification with timeline #436: implement is_seen logic #436: clean up based on comment #436: update test #436: add is_local for statuses #436: remove clear & dismiss UI updates #436: update unit testing #436: fix notification order, follow type link issue, duplicate key #436: update is_local #436: update entity_normalizer #436: add dismiss button, disable is_seen part #436: integrate mastoAPI notifications
This commit is contained in:
commit
93755d4858
14 changed files with 207 additions and 82 deletions
|
|
@ -25,11 +25,11 @@ 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])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
|
||||
<UserCard :user="notification.from_profile" :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>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ const Notifications = {
|
|||
},
|
||||
methods: {
|
||||
markAsSeen () {
|
||||
this.$store.dispatch('markNotificationsAsSeen', this.visibleNotifications)
|
||||
this.$store.dispatch('markNotificationsAsSeen')
|
||||
},
|
||||
fetchOlderNotifications () {
|
||||
const store = this.$store
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const defaultState = () => ({
|
|||
allStatusesObject: {},
|
||||
maxId: 0,
|
||||
notifications: {
|
||||
desktopNotificationSilence: true,
|
||||
desktopNotificationSilence: false,
|
||||
maxId: 0,
|
||||
minId: Number.POSITIVE_INFINITY,
|
||||
data: [],
|
||||
|
|
@ -271,12 +271,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) => {
|
||||
notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
|
||||
notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
|
||||
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)) {
|
||||
|
|
@ -292,15 +294,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)) {
|
||||
|
|
@ -413,8 +432,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 })
|
||||
|
|
|
|||
|
|
@ -122,8 +122,10 @@ export const mutations = {
|
|||
status.user = state.usersObject[status.user.id]
|
||||
},
|
||||
setUserForNotification (state, notification) {
|
||||
notification.action.user = state.usersObject[notification.action.user.id]
|
||||
notification.from_profile = state.usersObject[notification.action.user.id]
|
||||
if (notification.type !== 'follow') {
|
||||
notification.action.user = 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]
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -19,6 +18,7 @@ const DENY_USER_URL = '/api/pleroma/friendships/deny'
|
|||
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`
|
||||
|
|
@ -358,7 +358,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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -69,7 +69,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
|
||||
|
|
@ -182,8 +182,8 @@ export const parseStatus = (data) => {
|
|||
// Missing!! fix in UI?
|
||||
// output.in_reply_to_screen_name = ???
|
||||
|
||||
// Not exactly the same but works
|
||||
output.statusnet_conversation_id = data.id
|
||||
// It breaks the conversation when combined with notification
|
||||
// output.statusnet_conversation_id = data.id
|
||||
|
||||
if (output.type === 'retweet') {
|
||||
output.retweeted_status = parseStatus(data.reblog)
|
||||
|
|
@ -192,8 +192,7 @@ export const parseStatus = (data) => {
|
|||
output.summary = data.spoiler_text
|
||||
output.summary_html = addEmojis(data.spoiler_text, data.emojis)
|
||||
output.external_url = data.url
|
||||
|
||||
// output.is_local = ??? missing
|
||||
output.is_local = data.pleroma.local
|
||||
} else {
|
||||
output.favorited = data.favorited
|
||||
output.fave_num = data.fave_num
|
||||
|
|
@ -221,7 +220,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') {
|
||||
|
|
@ -272,9 +270,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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,29 +11,35 @@ 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})
|
||||
update({ store, notifications, older })
|
||||
return notifications
|
||||
}, () => store.dispatch('setNotificationsError', { value: true }))
|
||||
.catch(() => store.dispatch('setNotificationsError', { value: true }))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue