aboutsummaryrefslogtreecommitdiff
path: root/src/modules
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules')
-rw-r--r--src/modules/api.js29
-rw-r--r--src/modules/chats.js234
-rw-r--r--src/modules/config.js20
-rw-r--r--src/modules/instance.js77
-rw-r--r--src/modules/interface.js77
-rw-r--r--src/modules/media_viewer.js2
-rw-r--r--src/modules/statuses.js127
-rw-r--r--src/modules/users.js30
8 files changed, 483 insertions, 113 deletions
diff --git a/src/modules/api.js b/src/modules/api.js
index 748570e5..5e213f0d 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -1,4 +1,6 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
+import { WSConnectionStatus } from '../services/api/api.service.js'
+import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
import { Socket } from 'phoenix'
const api = {
@@ -7,6 +9,7 @@ const api = {
fetchers: {},
socket: null,
mastoUserSocket: null,
+ mastoUserSocketStatus: null,
followRequests: []
},
mutations: {
@@ -28,6 +31,9 @@ const api = {
},
setFollowRequests (state, value) {
state.followRequests = value
+ },
+ setMastoUserSocketStatus (state, value) {
+ state.mastoUserSocketStatus = value
}
},
actions: {
@@ -47,7 +53,7 @@ const api = {
startMastoUserSocket (store) {
return new Promise((resolve, reject) => {
try {
- const { state, dispatch, rootState } = store
+ const { state, commit, dispatch, rootState } = store
const timelineData = rootState.statuses.timelines.friends
state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
state.mastoUserSocket.addEventListener(
@@ -66,11 +72,23 @@ const api = {
showImmediately: timelineData.visibleStatuses.length === 0,
timeline: 'friends'
})
+ } else if (message.event === 'pleroma:chat_update') {
+ dispatch('addChatMessages', {
+ chatId: message.chatUpdate.id,
+ messages: [message.chatUpdate.lastMessage]
+ })
+ dispatch('updateChat', { chat: message.chatUpdate })
+ maybeShowChatNotification(store, message.chatUpdate)
}
}
)
+ state.mastoUserSocket.addEventListener('open', () => {
+ commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
+ })
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
console.error('Error in MastoAPI websocket:', error)
+ commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
+ dispatch('clearOpenedChats')
})
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
const ignoreCodes = new Set([
@@ -84,8 +102,11 @@ const api = {
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
+ dispatch('startFetchingChats')
dispatch('restartMastoUserSocket')
}
+ commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
+ dispatch('clearOpenedChats')
})
resolve()
} catch (e) {
@@ -99,12 +120,13 @@ const api = {
return dispatch('startMastoUserSocket').then(() => {
dispatch('stopFetchingTimeline', { timeline: 'friends' })
dispatch('stopFetchingNotifications')
+ dispatch('stopFetchingChats')
})
},
stopMastoUserSocket ({ state, dispatch }) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
- console.log(state.mastoUserSocket)
+ dispatch('startFetchingChats')
state.mastoUserSocket.close()
},
@@ -138,9 +160,6 @@ const api = {
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
},
- fetchAndUpdateNotifications (store) {
- store.state.backendInteractor.fetchAndUpdateNotifications({ store })
- },
// Follow requests
startFetchingFollowRequests (store) {
diff --git a/src/modules/chats.js b/src/modules/chats.js
new file mode 100644
index 00000000..c7609018
--- /dev/null
+++ b/src/modules/chats.js
@@ -0,0 +1,234 @@
+import Vue from 'vue'
+import { find, omitBy, orderBy, sumBy } from 'lodash'
+import chatService from '../services/chat_service/chat_service.js'
+import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
+import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
+
+const emptyChatList = () => ({
+ data: [],
+ idStore: {}
+})
+
+const defaultState = {
+ chatList: emptyChatList(),
+ chatListFetcher: null,
+ openedChats: {},
+ openedChatMessageServices: {},
+ fetcher: undefined,
+ currentChatId: null
+}
+
+const getChatById = (state, id) => {
+ return find(state.chatList.data, { id })
+}
+
+const sortedChatList = (state) => {
+ return orderBy(state.chatList.data, ['updated_at'], ['desc'])
+}
+
+const unreadChatCount = (state) => {
+ return sumBy(state.chatList.data, 'unread')
+}
+
+const chats = {
+ state: { ...defaultState },
+ getters: {
+ currentChat: state => state.openedChats[state.currentChatId],
+ currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId],
+ findOpenedChatByRecipientId: state => recipientId => find(state.openedChats, c => c.account.id === recipientId),
+ sortedChatList,
+ unreadChatCount
+ },
+ actions: {
+ // Chat list
+ startFetchingChats ({ dispatch, commit }) {
+ const fetcher = () => {
+ dispatch('fetchChats', { latest: true })
+ }
+ fetcher()
+ commit('setChatListFetcher', {
+ fetcher: () => setInterval(() => { fetcher() }, 5000)
+ })
+ },
+ stopFetchingChats ({ commit }) {
+ commit('setChatListFetcher', { fetcher: undefined })
+ },
+ fetchChats ({ dispatch, rootState, commit }, params = {}) {
+ return rootState.api.backendInteractor.chats()
+ .then(({ chats }) => {
+ dispatch('addNewChats', { chats })
+ return chats
+ })
+ },
+ addNewChats (store, { chats }) {
+ const { commit, dispatch, rootGetters } = store
+ const newChatMessageSideEffects = (chat) => {
+ maybeShowChatNotification(store, chat)
+ }
+ commit('addNewChats', { dispatch, chats, rootGetters, newChatMessageSideEffects })
+ },
+ updateChat ({ commit }, { chat }) {
+ commit('updateChat', { chat })
+ },
+
+ // Opened Chats
+ startFetchingCurrentChat ({ commit, dispatch }, { fetcher }) {
+ dispatch('setCurrentChatFetcher', { fetcher })
+ },
+ setCurrentChatFetcher ({ rootState, commit }, { fetcher }) {
+ commit('setCurrentChatFetcher', { fetcher })
+ },
+ addOpenedChat ({ rootState, commit, dispatch }, { chat }) {
+ commit('addOpenedChat', { dispatch, chat: parseChat(chat) })
+ dispatch('addNewUsers', [chat.account])
+ },
+ addChatMessages ({ commit }, value) {
+ commit('addChatMessages', { commit, ...value })
+ },
+ resetChatNewMessageCount ({ commit }, value) {
+ commit('resetChatNewMessageCount', value)
+ },
+ clearCurrentChat ({ rootState, commit, dispatch }, value) {
+ commit('setCurrentChatId', { chatId: undefined })
+ commit('setCurrentChatFetcher', { fetcher: undefined })
+ },
+ readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
+ dispatch('resetChatNewMessageCount')
+ commit('readChat', { id })
+ rootState.api.backendInteractor.readChat({ id, lastReadId })
+ },
+ deleteChatMessage ({ rootState, commit }, value) {
+ rootState.api.backendInteractor.deleteChatMessage(value)
+ commit('deleteChatMessage', { commit, ...value })
+ },
+ resetChats ({ commit, dispatch }) {
+ dispatch('clearCurrentChat')
+ commit('resetChats', { commit })
+ },
+ clearOpenedChats ({ rootState, commit, dispatch, rootGetters }) {
+ commit('clearOpenedChats', { commit })
+ }
+ },
+ mutations: {
+ setChatListFetcher (state, { commit, fetcher }) {
+ const prevFetcher = state.chatListFetcher
+ if (prevFetcher) {
+ clearInterval(prevFetcher)
+ }
+ state.chatListFetcher = fetcher && fetcher()
+ },
+ setCurrentChatFetcher (state, { fetcher }) {
+ const prevFetcher = state.fetcher
+ if (prevFetcher) {
+ clearInterval(prevFetcher)
+ }
+ state.fetcher = fetcher && fetcher()
+ },
+ addOpenedChat (state, { _dispatch, chat }) {
+ state.currentChatId = chat.id
+ Vue.set(state.openedChats, chat.id, chat)
+
+ if (!state.openedChatMessageServices[chat.id]) {
+ Vue.set(state.openedChatMessageServices, chat.id, chatService.empty(chat.id))
+ }
+ },
+ setCurrentChatId (state, { chatId }) {
+ state.currentChatId = chatId
+ },
+ addNewChats (state, { chats, newChatMessageSideEffects }) {
+ chats.forEach((updatedChat) => {
+ const chat = getChatById(state, updatedChat.id)
+
+ if (chat) {
+ const isNewMessage = (chat.lastMessage && chat.lastMessage.id) !== (updatedChat.lastMessage && updatedChat.lastMessage.id)
+ chat.lastMessage = updatedChat.lastMessage
+ chat.unread = updatedChat.unread
+ if (isNewMessage && chat.unread) {
+ newChatMessageSideEffects(updatedChat)
+ }
+ } else {
+ state.chatList.data.push(updatedChat)
+ Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+ }
+ })
+ },
+ updateChat (state, { _dispatch, chat: updatedChat, _rootGetters }) {
+ const chat = getChatById(state, updatedChat.id)
+ if (chat) {
+ chat.lastMessage = updatedChat.lastMessage
+ chat.unread = updatedChat.unread
+ chat.updated_at = updatedChat.updated_at
+ }
+ if (!chat) { state.chatList.data.unshift(updatedChat) }
+ Vue.set(state.chatList.idStore, updatedChat.id, updatedChat)
+ },
+ deleteChat (state, { _dispatch, id, _rootGetters }) {
+ state.chats.data = state.chats.data.filter(conversation =>
+ conversation.last_status.id !== id
+ )
+ state.chats.idStore = omitBy(state.chats.idStore, conversation => conversation.last_status.id === id)
+ },
+ resetChats (state, { commit }) {
+ state.chatList = emptyChatList()
+ state.currentChatId = null
+ commit('setChatListFetcher', { fetcher: undefined })
+ for (const chatId in state.openedChats) {
+ chatService.clear(state.openedChatMessageServices[chatId])
+ Vue.delete(state.openedChats, chatId)
+ Vue.delete(state.openedChatMessageServices, chatId)
+ }
+ },
+ setChatsLoading (state, { value }) {
+ state.chats.loading = value
+ },
+ addChatMessages (state, { commit, chatId, messages }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) })
+ commit('refreshLastMessage', { chatId })
+ }
+ },
+ refreshLastMessage (state, { chatId }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ const chat = getChatById(state, chatId)
+ if (chat) {
+ chat.lastMessage = chatMessageService.lastMessage
+ if (chatMessageService.lastMessage) {
+ chat.updated_at = chatMessageService.lastMessage.created_at
+ }
+ }
+ }
+ },
+ deleteChatMessage (state, { commit, chatId, messageId }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ chatService.deleteMessage(chatMessageService, messageId)
+ commit('refreshLastMessage', { chatId })
+ }
+ },
+ resetChatNewMessageCount (state, _value) {
+ const chatMessageService = state.openedChatMessageServices[state.currentChatId]
+ chatService.resetNewMessageCount(chatMessageService)
+ },
+ // Used when a connection loss occurs
+ clearOpenedChats (state) {
+ const currentChatId = state.currentChatId
+ for (const chatId in state.openedChats) {
+ if (currentChatId !== chatId) {
+ chatService.clear(state.openedChatMessageServices[chatId])
+ Vue.delete(state.openedChats, chatId)
+ Vue.delete(state.openedChatMessageServices, chatId)
+ }
+ }
+ },
+ readChat (state, { id }) {
+ const chat = getChatById(state, id)
+ if (chat) {
+ chat.unread = 0
+ }
+ }
+ }
+}
+
+export default chats
diff --git a/src/modules/config.js b/src/modules/config.js
index 8f4638f5..409d77a4 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,8 +1,19 @@
import { set, delete as del } from 'vue'
import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
+import messages from '../i18n/messages'
const browserLocale = (window.navigator.language || 'en').split('-')[0]
+/* TODO this is a bit messy.
+ * We need to declare settings with their types and also deal with
+ * instance-default settings in some way, hopefully try to avoid copy-pasta
+ * in general.
+ */
+export const multiChoiceProperties = [
+ 'postContentType',
+ 'subjectLineBehavior'
+]
+
export const defaultState = {
colors: {},
theme: undefined,
@@ -20,9 +31,7 @@ export const defaultState = {
preloadImage: true,
loopVideo: true,
loopVideoSilentOnly: true,
- autoLoad: true,
streaming: false,
- hoverPreview: true,
emojiReactionsOnTimeline: true,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
@@ -35,7 +44,8 @@ export const defaultState = {
repeats: true,
moves: true,
emojiReactions: false,
- followRequest: true
+ followRequest: true,
+ chatMention: true
},
webPushNotifications: false,
muteWords: [],
@@ -105,6 +115,10 @@ const config = {
case 'customTheme':
case 'customThemeSource':
applyTheme(value)
+ break
+ case 'interfaceLanguage':
+ messages.setLanguage(this.getters.i18n, value)
+ break
}
}
}
diff --git a/src/modules/instance.js b/src/modules/instance.js
index ffece311..3fe3bbf3 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -1,55 +1,63 @@
import { set } from 'vue'
import { getPreset, applyTheme } from '../services/style_setter/style_setter.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
+import apiService from '../services/api/api.service.js'
import { instanceDefaultProperties } from './config.js'
const defaultState = {
- // Stuff from static/config.json and apiConfig
+ // Stuff from apiConfig
name: 'Pleroma FE',
registrationOpen: true,
- safeDM: true,
- textlimit: 5000,
server: 'http://localhost:4040/',
- theme: 'pleroma-dark',
+ textlimit: 5000,
themeData: undefined,
- background: '/static/aurora_borealis.jpg',
- logo: '/static/logo.png',
- logoMask: true,
- logoMargin: '.2em',
- redirectRootNoLogin: '/main/all',
- redirectRootLogin: '/main/friends',
- showInstanceSpecificPanel: false,
+ vapidPublicKey: undefined,
+
+ // Stuff from static/config.json
alwaysShowSubjectInput: true,
- hideMutedPosts: false,
+ defaultAvatar: '/images/avi.png',
+ defaultBanner: '/images/banner.png',
+ background: '/static/aurora_borealis.jpg',
collapseMessageWithSubject: false,
- hidePostStats: false,
- hideUserStats: false,
- hideFilteredStatuses: false,
disableChat: false,
- scopeCopy: true,
- subjectLineBehavior: 'email',
- postContentType: 'text/plain',
+ greentext: false,
+ hideFilteredStatuses: false,
+ hideMutedPosts: false,
+ hidePostStats: false,
hideSitename: false,
+ hideUserStats: false,
+ loginMethod: 'password',
+ logo: '/static/logo.png',
+ logoMargin: '.2em',
+ logoMask: true,
+ minimalScopesMode: false,
nsfwCensorImage: undefined,
- vapidPublicKey: undefined,
- noAttachmentLinks: false,
+ postContentType: 'text/plain',
+ redirectRootLogin: '/main/friends',
+ redirectRootNoLogin: '/main/all',
+ scopeCopy: true,
showFeaturesPanel: true,
- minimalScopesMode: false,
- greentext: false,
+ showInstanceSpecificPanel: false,
+ sidebarRight: false,
+ subjectLineBehavior: 'email',
+ theme: 'pleroma-dark',
// Nasty stuff
- pleromaBackend: true,
- emoji: [],
- emojiFetched: false,
customEmoji: [],
customEmojiFetched: false,
- restrictedNicknames: [],
+ emoji: [],
+ emojiFetched: false,
+ pleromaBackend: true,
postFormats: [],
+ restrictedNicknames: [],
+ safeDM: true,
+ knownDomains: [],
// Feature-set, apparently, not everything here is reported...
- mediaProxyAvailable: false,
chatAvailable: false,
+ pleromaChatMessagesAvailable: false,
gopherAvailable: false,
+ mediaProxyAvailable: false,
suggestionsEnabled: false,
suggestionsWeb: '',
@@ -77,6 +85,9 @@ const instance = {
if (typeof value !== 'undefined') {
set(state, name, value)
}
+ },
+ setKnownDomains (state, domains) {
+ state.knownDomains = domains
}
},
getters: {
@@ -179,6 +190,18 @@ const instance = {
state.emojiFetched = true
dispatch('getStaticEmoji')
}
+ },
+
+ async getKnownDomains ({ commit, rootState }) {
+ try {
+ const result = await apiService.fetchKnownDomains({
+ credentials: rootState.users.currentUser.credentials
+ })
+ commit('setKnownDomains', result)
+ } catch (e) {
+ console.warn("Can't load known domains")
+ console.warn(e)
+ }
}
}
}
diff --git a/src/modules/interface.js b/src/modules/interface.js
index 5b2762e5..748d3025 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -1,6 +1,8 @@
import { set, delete as del } from 'vue'
const defaultState = {
+ settingsModalState: 'hidden',
+ settingsModalLoaded: false,
settings: {
currentSaveStateNotice: null,
noticeClearTimeout: null,
@@ -12,7 +14,10 @@ const defaultState = {
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
)
},
- mobileLayout: false
+ mobileLayout: false,
+ globalNotices: [],
+ layoutHeight: 0,
+ lastTimeline: null
}
const interfaceMod = {
@@ -35,6 +40,39 @@ const interfaceMod = {
},
setMobileLayout (state, value) {
state.mobileLayout = value
+ },
+ closeSettingsModal (state) {
+ state.settingsModalState = 'hidden'
+ },
+ togglePeekSettingsModal (state) {
+ switch (state.settingsModalState) {
+ case 'minimized':
+ state.settingsModalState = 'visible'
+ return
+ case 'visible':
+ state.settingsModalState = 'minimized'
+ return
+ default:
+ throw new Error('Illegal minimization state of settings modal')
+ }
+ },
+ openSettingsModal (state) {
+ state.settingsModalState = 'visible'
+ if (!state.settingsModalLoaded) {
+ state.settingsModalLoaded = true
+ }
+ },
+ pushGlobalNotice (state, notice) {
+ state.globalNotices.push(notice)
+ },
+ removeGlobalNotice (state, notice) {
+ state.globalNotices = state.globalNotices.filter(n => n !== notice)
+ },
+ setLayoutHeight (state, value) {
+ state.layoutHeight = value
+ },
+ setLastTimeline (state, value) {
+ state.lastTimeline = value
}
},
actions: {
@@ -49,6 +87,43 @@ const interfaceMod = {
},
setMobileLayout ({ commit }, value) {
commit('setMobileLayout', value)
+ },
+ closeSettingsModal ({ commit }) {
+ commit('closeSettingsModal')
+ },
+ openSettingsModal ({ commit }) {
+ commit('openSettingsModal')
+ },
+ togglePeekSettingsModal ({ commit }) {
+ commit('togglePeekSettingsModal')
+ },
+ pushGlobalNotice (
+ { commit, dispatch },
+ {
+ messageKey,
+ messageArgs = {},
+ level = 'error',
+ timeout = 0
+ }) {
+ const notice = {
+ messageKey,
+ messageArgs,
+ level
+ }
+ if (timeout) {
+ setTimeout(() => dispatch('removeGlobalNotice', notice), timeout)
+ }
+ commit('pushGlobalNotice', notice)
+ return notice
+ },
+ removeGlobalNotice ({ commit }, notice) {
+ commit('removeGlobalNotice', notice)
+ },
+ setLayoutHeight ({ commit }, value) {
+ commit('setLayoutHeight', value)
+ },
+ setLastTimeline ({ commit }, value) {
+ commit('setLastTimeline', value)
}
}
}
diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js
index a24b408d..721c25e6 100644
--- a/src/modules/media_viewer.js
+++ b/src/modules/media_viewer.js
@@ -22,7 +22,7 @@ const mediaViewer = {
setMedia ({ commit }, attachments) {
const media = attachments.filter(attachment => {
const type = fileTypeService.fileType(attachment.mimetype)
- return type === 'image' || type === 'video'
+ return type === 'image' || type === 'video' || type === 'audio'
})
commit('setMedia', media)
},
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index cd8c1dba..e108b2a7 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -13,9 +13,8 @@ import {
omitBy
} from 'lodash'
import { set } from 'vue'
-import { isStatusNotification } from '../services/notification_utils/notification_utils.js'
+import { isStatusNotification, maybeShowNotification } from '../services/notification_utils/notification_utils.js'
import apiService from '../services/api/api.service.js'
-// import parse from '../services/status_parser/status_parser.js'
const emptyTl = (userId = 0) => ({
statuses: [],
@@ -62,7 +61,8 @@ export const defaultState = () => ({
publicAndExternal: emptyTl(),
friends: emptyTl(),
tag: emptyTl(),
- dms: emptyTl()
+ dms: emptyTl(),
+ bookmarks: emptyTl()
}
})
@@ -76,17 +76,6 @@ 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',
- rootState.config.notificationVisibility.moves && 'move',
- rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions'
- ].filter(_ => _)
-}
-
const mergeOrAdd = (arr, obj, item) => {
const oldItem = obj[item.id]
@@ -163,8 +152,7 @@ const removeStatusFromGlobalStorage = (state, status) => {
}
}
-const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {},
- noIdUpdate = false, userId }) => {
+const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => {
// Sanity check
if (!isArray(statuses)) {
return false
@@ -173,8 +161,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const allStatuses = state.allStatuses
const timelineObject = state.timelines[timeline]
- const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
- const minNew = statuses.length > 0 ? minBy(statuses, 'id').id : 0
+ // Mismatch between API pagination and our internal minId/maxId tracking systems:
+ // pagination.maxId is the oldest of the returned statuses when fetching older,
+ // and pagination.minId is the newest when fetching newer. The names come directly
+ // from the arguments they're supposed to be passed as for the next fetch.
+ const minNew = pagination.maxId || (statuses.length > 0 ? minBy(statuses, 'id').id : 0)
+ const maxNew = pagination.minId || (statuses.length > 0 ? maxBy(statuses, 'id').id : 0)
+
const newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0
const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0
@@ -315,12 +308,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
})
// Keep the visible statuses sorted
- if (timeline) {
+ if (timeline && !(timeline === 'bookmarks')) {
sortTimeline(timelineObject)
}
}
-const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
+const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => {
each(notifications, (notification) => {
if (isStatusNotification(notification.type)) {
notification.action = addStatusToGlobalStorage(state, notification.action).item
@@ -343,51 +336,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
state.notifications.data.push(notification)
state.notifications.idStore[notification.id] = notification
- if ('Notification' in window && window.Notification.permission === 'granted') {
- const notifObj = {}
- 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
- case 'move':
- i18nString = 'migrated_to'
- break
- case 'follow_request':
- i18nString = 'follow_request'
- break
- }
-
- if (notification.type === 'pleroma:emoji_reaction') {
- notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji])
- } else if (i18nString) {
- notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
- } else if (isStatusNotification(notification.type)) {
- notifObj.body = notification.status.text
- }
-
- // Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
- 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)) {
- let desktopNotification = new window.Notification(title, notifObj)
- // Chrome is known for not closing notifications automatically
- // according to MDN, anyway.
- setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
- }
- }
+ newNotificationSideEffects(notification)
} else if (notification.seen) {
state.notifications.idStore[notification.id].seen = true
}
@@ -487,9 +436,17 @@ export const mutations = {
newStatus.rebloggedBy.push(user)
}
},
+ setBookmarked (state, { status, value }) {
+ const newStatus = state.allStatusesObject[status.id]
+ newStatus.bookmarked = value
+ },
+ setBookmarkedConfirm (state, { status }) {
+ const newStatus = state.allStatusesObject[status.id]
+ newStatus.bookmarked = status.bookmarked
+ },
setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
- newStatus.deleted = true
+ if (newStatus) newStatus.deleted = true
},
setManyDeleted (state, condition) {
Object.values(state.allStatusesObject).forEach(status => {
@@ -532,6 +489,9 @@ export const mutations = {
dismissNotification (state, { id }) {
state.notifications.data = state.notifications.data.filter(n => n.id !== id)
},
+ dismissNotifications (state, { finder }) {
+ state.notifications.data = state.notifications.data.filter(n => finder)
+ },
updateNotification (state, { id, updater }) {
const notification = find(state.notifications.data, n => n.id === id)
notification && updater(notification)
@@ -539,6 +499,11 @@ export const mutations = {
queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = id
},
+ queueFlushAll (state) {
+ Object.keys(state.timelines).forEach((timeline) => {
+ state.timelines[timeline].flushMarker = state.timelines[timeline].maxId
+ })
+ },
addRepeats (state, { id, rebloggedByUsers, currentUser }) {
const newStatus = state.allStatusesObject[id]
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
@@ -609,11 +574,16 @@ export const mutations = {
const statuses = {
state: defaultState(),
actions: {
- addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
- commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
+ addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
+ commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
},
- addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
- commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
+ addNewNotifications (store, { notifications, older }) {
+ const { commit, dispatch, rootGetters } = store
+
+ const newNotificationSideEffects = (notification) => {
+ maybeShowNotification(store, notification)
+ }
+ commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects })
},
setError ({ rootState, commit }, { value }) {
commit('setError', { value })
@@ -685,9 +655,26 @@ const statuses = {
rootState.api.backendInteractor.unretweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
},
+ bookmark ({ rootState, commit }, status) {
+ commit('setBookmarked', { status, value: true })
+ rootState.api.backendInteractor.bookmarkStatus({ id: status.id })
+ .then(status => {
+ commit('setBookmarkedConfirm', { status })
+ })
+ },
+ unbookmark ({ rootState, commit }, status) {
+ commit('setBookmarked', { status, value: false })
+ rootState.api.backendInteractor.unbookmarkStatus({ id: status.id })
+ .then(status => {
+ commit('setBookmarkedConfirm', { status })
+ })
+ },
queueFlush ({ rootState, commit }, { timeline, id }) {
commit('queueFlush', { timeline, id })
},
+ queueFlushAll ({ rootState, commit }) {
+ commit('queueFlushAll')
+ },
markNotificationsAsSeen ({ rootState, commit }) {
commit('markNotificationsAsSeen')
apiService.markNotificationsAsSeen({
diff --git a/src/modules/users.js b/src/modules/users.js
index 1d1b415c..9245db5c 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,6 +1,6 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import oauthApi from '../services/new_api/oauth.js'
-import { compact, map, each, merge, last, concat, uniq } from 'lodash'
+import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
import { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
@@ -10,7 +10,7 @@ export const mergeOrAdd = (arr, obj, item) => {
const oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
- merge(oldItem, item)
+ mergeWith(oldItem, item, mergeArrayLength)
return { item: oldItem, new: false }
} else {
// This is a new item, prepare it
@@ -23,6 +23,13 @@ export const mergeOrAdd = (arr, obj, item) => {
}
}
+const mergeArrayLength = (oldValue, newValue) => {
+ if (isArray(oldValue) && isArray(newValue)) {
+ oldValue.length = newValue.length
+ return mergeWith(oldValue, newValue, mergeArrayLength)
+ }
+}
+
const getNotificationPermission = () => {
const Notification = window.Notification
@@ -116,7 +123,7 @@ export const mutations = {
},
setCurrentUser (state, user) {
state.lastLoginName = user.screen_name
- state.currentUser = merge(state.currentUser || {}, user)
+ state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
},
clearCurrentUser (state) {
state.currentUser = false
@@ -259,6 +266,11 @@ const users = {
mutations,
getters,
actions: {
+ fetchUserIfMissing (store, id) {
+ if (!store.getters.findUser(id)) {
+ store.dispatch('fetchUser', id)
+ }
+ },
fetchUser (store, id) {
return store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => {
@@ -428,10 +440,10 @@ const users = {
store.commit('setUserForNotification', notification)
})
},
- searchUsers (store, query) {
- return store.rootState.api.backendInteractor.searchUsers({ query })
+ searchUsers ({ rootState, commit }, { query }) {
+ return rootState.api.backendInteractor.searchUsers({ query })
.then((users) => {
- store.commit('addNewUsers', users)
+ commit('addNewUsers', users)
return users
})
},
@@ -486,6 +498,8 @@ const users = {
store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
+ store.dispatch('resetChats')
+ store.dispatch('setLastTimeline', 'public-timeline')
})
},
loginUser (store, accessToken) {
@@ -525,6 +539,9 @@ const users = {
// Start fetching notifications
store.dispatch('startFetchingNotifications')
+
+ // Start fetching chats
+ store.dispatch('startFetchingChats')
}
if (store.getters.mergedConfig.useStreamingApi) {
@@ -532,6 +549,7 @@ const users = {
console.error('Failed initializing MastoAPI Streaming socket', error)
startPolling()
}).then(() => {
+ store.dispatch('fetchChats', { latest: true })
setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
})
} else {