diff options
Diffstat (limited to 'src/modules/statuses.js')
| -rw-r--r-- | src/modules/statuses.js | 225 |
1 files changed, 165 insertions, 60 deletions
diff --git a/src/modules/statuses.js b/src/modules/statuses.js index bd52f161..8cdd4e28 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -15,6 +15,7 @@ const emptyTl = () => ({ followers: [], friends: [], viewing: 'statuses', + userId: 0, flushMarker: 0 }) @@ -22,16 +23,25 @@ export const defaultState = { allStatuses: [], allStatusesObject: {}, maxId: 0, - notifications: [], + notifications: { + desktopNotificationSilence: true, + maxId: 0, + minId: Number.POSITIVE_INFINITY, + data: [], + error: false, + brokenFavorites: {} + }, favorites: new Set(), error: false, timelines: { mentions: emptyTl(), public: emptyTl(), user: emptyTl(), + own: emptyTl(), publicAndExternal: emptyTl(), friends: emptyTl(), - tag: emptyTl() + tag: emptyTl(), + dms: emptyTl() } } @@ -45,7 +55,7 @@ export const prepareStatus = (status) => { if (status.nsfw === undefined) { status.nsfw = isNsfw(status) if (status.retweeted_status) { - status.retweeted_status.nsfw = status.nsfw + status.nsfw = status.retweeted_status.nsfw } } @@ -58,6 +68,15 @@ export const prepareStatus = (status) => { return status } +const visibleNotificationTypes = (rootState) => { + return [ + rootState.config.notificationVisibility.likes && 'like', + rootState.config.notificationVisibility.mentions && 'mention', + rootState.config.notificationVisibility.repeats && 'repeat', + rootState.config.notificationVisibility.follows && 'follow' + ].filter(_ => _) +} + export const statusType = (status) => { if (status.is_post_verb) { return 'status' @@ -76,8 +95,7 @@ export const statusType = (status) => { return 'deletion' } - // TODO change to status.activity_type === 'follow' when gs supports it - if (status.text.match(/started following/)) { + if (status.text.match(/started following/) || status.activity_type === 'follow') { return 'follow' } @@ -113,7 +131,7 @@ const sortTimeline = (timeline) => { return timeline } -const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false }) => { +const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId }) => { // Sanity check if (!isArray(statuses)) { return false @@ -130,15 +148,24 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us timelineObject.maxId = maxNew } + // This makes sure that user timeline won't get data meant for other + // user. I.e. opening different user profiles makes request which could + // return data late after user already viewing different user profile + if (timeline === 'user' && timelineObject.userId !== userId) { + return + } + const addStatus = (status, showImmediately, addToTimeline = true) => { const result = mergeOrAdd(allStatuses, allStatusesObject, status) status = result.item - if (result.new) { - if (statusType(status) === 'retweet' && status.retweeted_status.user.id === user.id) { - addNotification({ type: 'repeat', status: status.retweeted_status, action: status }) - } + const brokenFavorites = state.notifications.brokenFavorites[status.id] || [] + brokenFavorites.forEach((fav) => { + fav.status = status + }) + delete state.notifications.brokenFavorites[status.id] + if (result.new) { // We are mentioned in a post if (statusType(status) === 'status' && find(status.attentions, { id: user.id })) { const mentions = state.timelines.mentions @@ -150,10 +177,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us sortTimeline(mentions) } - // Don't add notification for self-mention - if (status.user.id !== user.id) { - addNotification({ type: 'mention', status, action: status }) - } + } + if (status.visibility === 'direct') { + const dms = state.timelines.dms + + mergeOrAdd(dms.statuses, dms.statusesObject, status) + dms.newStatusCount += 1 + + sortTimeline(dms) } } @@ -176,45 +207,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us return status } - const addNotification = ({type, status, action}) => { - // Only add a new notification if we don't have one for the same action - if (!find(state.notifications, (oldNotification) => oldNotification.action.id === action.id)) { - state.notifications.push({ type, status, action, seen: false }) - - if ('Notification' in window && window.Notification.permission === 'granted') { - const title = action.user.name - const result = {} - result.icon = action.user.profile_image_url - result.body = action.text // there's a problem that it doesn't put a space before links tho - - // 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/')) { - result.image = action.attachments[0].url - } - - let notification = new window.Notification(title, result) - - // Chrome is known for not closing notifications automatically - // according to MDN, anyway. - setTimeout(notification.close.bind(notification), 5000) - } - } - } - - const favoriteStatus = (favorite) => { + const favoriteStatus = (favorite, counter) => { const status = find(allStatuses, { id: toInteger(favorite.in_reply_to_status_id) }) if (status) { - status.fave_num += 1 - // This is our favorite, so the relevant bit. if (favorite.user.id === user.id) { status.favorited = true - } - - // Add a notification if the user's status is favorited - if (status.user.id === user.id) { - addNotification({type: 'favorite', status, action: favorite}) + } else { + status.fave_num += 1 } } return status @@ -248,18 +248,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us }, 'favorite': (favorite) => { // Only update if this is a new favorite. + // Ignore our own favorites because we get info about likes as response to like request if (!state.favorites.has(favorite.id)) { state.favorites.add(favorite.id) favoriteStatus(favorite) } }, - 'follow': (status) => { - let re = new RegExp(`started following ${user.name} \\(${user.statusnet_profile_url}\\)`) - let repleroma = new RegExp(`started following ${user.screen_name}$`) - if (status.text.match(re) || status.text.match(repleroma)) { - addNotification({ type: 'follow', status: status, action: status }) - } - }, 'deletion': (deletion) => { const uri = deletion.uri @@ -269,7 +263,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us return } - remove(state.notifications, ({action: {id}}) => id === status.id) + remove(state.notifications.data, ({action: {id}}) => id === status.id) remove(allStatuses, { uri }) if (timeline) { @@ -298,8 +292,68 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } } +const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => { + const allStatuses = state.allStatuses + const allStatusesObject = state.allStatusesObject + each(notifications, (notification) => { + const result = mergeOrAdd(allStatuses, allStatusesObject, notification.notice) + const action = result.item + // Only add a new notification if we don't have one for the same action + if (!find(state.notifications.data, (oldNotification) => oldNotification.action.id === action.id)) { + state.notifications.maxId = Math.max(notification.id, state.notifications.maxId) + state.notifications.minId = Math.min(notification.id, state.notifications.minId) + + const fresh = !notification.is_seen + const status = notification.ntype === 'like' + ? find(allStatuses, { id: action.in_reply_to_status_id }) + : action + + const result = { + type: notification.ntype, + status, + action, + seen: !fresh + } + + if (notification.ntype === 'like' && !status) { + let broken = state.notifications.brokenFavorites[action.in_reply_to_status_id] + if (broken) { + broken.push(result) + } else { + dispatch('fetchOldPost', { postId: action.in_reply_to_status_id }) + broken = [ result ] + state.notifications.brokenFavorites[action.in_reply_to_status_id] = broken + } + } + + state.notifications.data.push(result) + + if ('Notification' in window && window.Notification.permission === 'granted') { + const title = action.user.name + const result = {} + result.icon = action.user.profile_image_url + result.body = action.text // there's a problem that it doesn't put a space before links tho + + // 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/')) { + result.image = action.attachments[0].url + } + + if (fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) { + let notification = new window.Notification(title, result) + // Chrome is known for not closing notifications automatically + // according to MDN, anyway. + setTimeout(notification.close.bind(notification), 5000) + } + } + } + }) +} + export const mutations = { addNewStatuses, + addNewNotifications, showNewStatuses (state, { timeline }) { const oldTimeline = (state.timelines[timeline]) @@ -316,6 +370,11 @@ export const mutations = { const newStatus = state.allStatusesObject[status.id] newStatus.favorited = value }, + setFavoritedConfirm (state, { status }) { + const newStatus = state.allStatusesObject[status.id] + newStatus.favorited = status.favorited + newStatus.fave_num = status.fave_num + }, setRetweeted (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] newStatus.repeated = value @@ -334,6 +393,12 @@ export const mutations = { setError (state, { value }) { state.error = value }, + setNotificationsError (state, { value }) { + state.notifications.error = value + }, + setNotificationsSilence (state, { value }) { + state.notifications.desktopNotificationSilence = value + }, setProfileView (state, { v }) { // load followers / friends only when needed state.timelines['user'].viewing = v @@ -344,8 +409,8 @@ export const mutations = { addFollowers (state, { followers }) { state.timelines['user'].followers = followers }, - markNotificationsAsSeen (state, notifications) { - each(notifications, (notification) => { + markNotificationsAsSeen (state) { + each(state.notifications.data, (notification) => { notification.seen = true }) }, @@ -357,12 +422,21 @@ export const mutations = { const statuses = { state: defaultState, actions: { - addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false }) { - commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser }) + 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 }) }, setError ({ rootState, commit }, { value }) { commit('setError', { value }) }, + setNotificationsError ({ rootState, commit }, { value }) { + commit('setNotificationsError', { value }) + }, + setNotificationsSilence ({ rootState, commit }, { value }) { + commit('setNotificationsSilence', { value }) + }, addFriends ({ rootState, commit }, { friends }) { commit('addFriends', { friends }) }, @@ -377,19 +451,50 @@ const statuses = { // Optimistic favoriting... commit('setFavorited', { status, value: true }) apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials }) + .then(response => { + if (response.ok) { + return response.json() + } else { + return {} + } + }) + .then(status => { + commit('setFavoritedConfirm', { status }) + }) }, unfavorite ({ rootState, commit }, status) { // Optimistic favoriting... commit('setFavorited', { status, value: false }) apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials }) + .then(response => { + if (response.ok) { + return response.json() + } else { + return {} + } + }) + .then(status => { + commit('setFavoritedConfirm', { status }) + }) }, retweet ({ rootState, commit }, status) { // Optimistic retweeting... commit('setRetweeted', { status, value: true }) apiService.retweet({ id: status.id, credentials: rootState.users.currentUser.credentials }) }, + unretweet ({ rootState, commit }, status) { + commit('setRetweeted', { status, value: false }) + apiService.unretweet({ id: status.id, credentials: rootState.users.currentUser.credentials }) + }, queueFlush ({ rootState, commit }, { timeline, id }) { commit('queueFlush', { timeline, id }) + }, + markNotificationsAsSeen ({ rootState, commit }) { + commit('markNotificationsAsSeen') + apiService.markNotificationsAsSeen({ + id: rootState.statuses.notifications.maxId, + credentials: rootState.users.currentUser.credentials + }) } }, mutations |
