aboutsummaryrefslogtreecommitdiff
path: root/src/modules/statuses.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/statuses.js')
-rw-r--r--src/modules/statuses.js225
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