logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://anongit.hacktivis.me/git/pleroma-fe.git/

api.js (12160B)


  1. import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
  2. import { WSConnectionStatus } from '../services/api/api.service.js'
  3. import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
  4. import { Socket } from 'phoenix'
  5. import { useShoutStore } from 'src/stores/shout.js'
  6. import { useInterfaceStore } from 'src/stores/interface.js'
  7. const retryTimeout = (multiplier) => 1000 * multiplier
  8. const api = {
  9. state: {
  10. retryMultiplier: 1,
  11. backendInteractor: backendInteractorService(),
  12. fetchers: {},
  13. socket: null,
  14. mastoUserSocket: null,
  15. mastoUserSocketStatus: null,
  16. followRequests: []
  17. },
  18. getters: {
  19. followRequestCount: state => state.followRequests.length
  20. },
  21. mutations: {
  22. setBackendInteractor (state, backendInteractor) {
  23. state.backendInteractor = backendInteractor
  24. },
  25. addFetcher (state, { fetcherName, fetcher }) {
  26. state.fetchers[fetcherName] = fetcher
  27. },
  28. removeFetcher (state, { fetcherName }) {
  29. state.fetchers[fetcherName].stop()
  30. delete state.fetchers[fetcherName]
  31. },
  32. setWsToken (state, token) {
  33. state.wsToken = token
  34. },
  35. setSocket (state, socket) {
  36. state.socket = socket
  37. },
  38. setFollowRequests (state, value) {
  39. state.followRequests = value
  40. },
  41. setMastoUserSocketStatus (state, value) {
  42. state.mastoUserSocketStatus = value
  43. },
  44. incrementRetryMultiplier (state) {
  45. state.retryMultiplier = Math.max(++state.retryMultiplier, 3)
  46. },
  47. resetRetryMultiplier (state) {
  48. state.retryMultiplier = 1
  49. }
  50. },
  51. actions: {
  52. /**
  53. * Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets
  54. *
  55. * @param {Boolean} [initial] - whether this enabling happened at boot time or not
  56. */
  57. enableMastoSockets (store, initial) {
  58. const { state, dispatch, commit } = store
  59. // Do not initialize unless nonexistent or closed
  60. if (
  61. state.mastoUserSocket &&
  62. ![
  63. WebSocket.CLOSED,
  64. WebSocket.CLOSING
  65. ].includes(state.mastoUserSocket.getState())
  66. ) {
  67. return
  68. }
  69. if (initial) {
  70. commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING_INITIAL)
  71. } else {
  72. commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING)
  73. }
  74. return dispatch('startMastoUserSocket')
  75. },
  76. disableMastoSockets (store) {
  77. const { state, dispatch, commit } = store
  78. if (!state.mastoUserSocket) return
  79. commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED)
  80. return dispatch('stopMastoUserSocket')
  81. },
  82. // MastoAPI 'User' sockets
  83. startMastoUserSocket (store) {
  84. return new Promise((resolve, reject) => {
  85. try {
  86. const { state, commit, dispatch, rootState } = store
  87. const timelineData = rootState.statuses.timelines.friends
  88. state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
  89. state.mastoUserSocket.addEventListener('pleroma:authenticated', () => {
  90. state.mastoUserSocket.subscribe('user')
  91. })
  92. state.mastoUserSocket.addEventListener(
  93. 'message',
  94. ({ detail: message }) => {
  95. if (!message) return // pings
  96. if (message.event === 'notification') {
  97. dispatch('addNewNotifications', {
  98. notifications: [message.notification],
  99. older: false
  100. })
  101. } else if (message.event === 'update') {
  102. dispatch('addNewStatuses', {
  103. statuses: [message.status],
  104. userId: false,
  105. showImmediately: timelineData.visibleStatuses.length === 0,
  106. timeline: 'friends'
  107. })
  108. } else if (message.event === 'status.update') {
  109. dispatch('addNewStatuses', {
  110. statuses: [message.status],
  111. userId: false,
  112. showImmediately: message.status.id in timelineData.visibleStatusesObject,
  113. timeline: 'friends'
  114. })
  115. } else if (message.event === 'delete') {
  116. dispatch('deleteStatusById', message.id)
  117. } else if (message.event === 'pleroma:chat_update') {
  118. // The setTimeout wrapper is a temporary band-aid to avoid duplicates for the user's own messages when doing optimistic sending.
  119. // The cause of the duplicates is the WS event arriving earlier than the HTTP response.
  120. // This setTimeout wrapper can be removed once the commit `8e41baff` is in the stable Pleroma release.
  121. // (`8e41baff` adds the idempotency key to the chat message entity, which PleromaFE uses when it's available, and it makes this artificial delay unnecessary).
  122. setTimeout(() => {
  123. dispatch('addChatMessages', {
  124. chatId: message.chatUpdate.id,
  125. messages: [message.chatUpdate.lastMessage]
  126. })
  127. dispatch('updateChat', { chat: message.chatUpdate })
  128. maybeShowChatNotification(store, message.chatUpdate)
  129. }, 100)
  130. }
  131. }
  132. )
  133. state.mastoUserSocket.addEventListener('open', () => {
  134. // Do not show notification when we just opened up the page
  135. if (state.mastoUserSocketStatus !== WSConnectionStatus.STARTING_INITIAL) {
  136. useInterfaceStore().pushGlobalNotice({
  137. level: 'success',
  138. messageKey: 'timeline.socket_reconnected',
  139. timeout: 5000
  140. })
  141. }
  142. // Stop polling if we were errored or disabled
  143. if (new Set([
  144. WSConnectionStatus.ERROR,
  145. WSConnectionStatus.DISABLED
  146. ]).has(state.mastoUserSocketStatus)) {
  147. dispatch('stopFetchingTimeline', { timeline: 'friends' })
  148. dispatch('stopFetchingNotifications')
  149. dispatch('stopFetchingChats')
  150. }
  151. commit('resetRetryMultiplier')
  152. commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
  153. })
  154. state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
  155. console.error('Error in MastoAPI websocket:', error)
  156. // TODO is this needed?
  157. dispatch('clearOpenedChats')
  158. })
  159. state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
  160. const ignoreCodes = new Set([
  161. 1000, // Normal (intended) closure
  162. 1001 // Going away
  163. ])
  164. const { code } = closeEvent
  165. if (ignoreCodes.has(code)) {
  166. console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`)
  167. commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
  168. } else {
  169. console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
  170. setTimeout(() => {
  171. dispatch('startMastoUserSocket')
  172. }, retryTimeout(state.retryMultiplier))
  173. commit('incrementRetryMultiplier')
  174. if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) {
  175. dispatch('startFetchingTimeline', { timeline: 'friends' })
  176. dispatch('startFetchingNotifications')
  177. dispatch('startFetchingChats')
  178. useInterfaceStore().pushGlobalNotice({
  179. level: 'error',
  180. messageKey: 'timeline.socket_broke',
  181. messageArgs: [code],
  182. timeout: 5000
  183. })
  184. }
  185. commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
  186. }
  187. dispatch('clearOpenedChats')
  188. })
  189. resolve()
  190. } catch (e) {
  191. reject(e)
  192. }
  193. })
  194. },
  195. stopMastoUserSocket ({ state, dispatch }) {
  196. dispatch('startFetchingTimeline', { timeline: 'friends' })
  197. dispatch('startFetchingNotifications')
  198. dispatch('startFetchingChats')
  199. state.mastoUserSocket.close()
  200. },
  201. // Timelines
  202. startFetchingTimeline (store, {
  203. timeline = 'friends',
  204. tag = false,
  205. userId = false,
  206. listId = false,
  207. statusId = false,
  208. bookmarkFolderId = false
  209. }) {
  210. if (store.state.fetchers[timeline]) return
  211. const fetcher = store.state.backendInteractor.startFetchingTimeline({
  212. timeline, store, userId, listId, statusId, bookmarkFolderId, tag
  213. })
  214. store.commit('addFetcher', { fetcherName: timeline, fetcher })
  215. },
  216. stopFetchingTimeline (store, timeline) {
  217. const fetcher = store.state.fetchers[timeline]
  218. if (!fetcher) return
  219. store.commit('removeFetcher', { fetcherName: timeline, fetcher })
  220. },
  221. fetchTimeline (store, { timeline, ...rest }) {
  222. store.state.backendInteractor.fetchTimeline({
  223. store,
  224. timeline,
  225. ...rest
  226. })
  227. },
  228. // Notifications
  229. startFetchingNotifications (store) {
  230. if (store.state.fetchers.notifications) return
  231. const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
  232. store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
  233. },
  234. stopFetchingNotifications (store) {
  235. const fetcher = store.state.fetchers.notifications
  236. if (!fetcher) return
  237. store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
  238. },
  239. fetchNotifications (store, { ...rest }) {
  240. store.state.backendInteractor.fetchNotifications({
  241. store,
  242. ...rest
  243. })
  244. },
  245. // Follow requests
  246. startFetchingFollowRequests (store) {
  247. if (store.state.fetchers.followRequests) return
  248. const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
  249. store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
  250. },
  251. stopFetchingFollowRequests (store) {
  252. const fetcher = store.state.fetchers.followRequests
  253. if (!fetcher) return
  254. store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
  255. },
  256. removeFollowRequest (store, request) {
  257. const requests = store.state.followRequests.filter((it) => it !== request)
  258. store.commit('setFollowRequests', requests)
  259. },
  260. // Lists
  261. startFetchingLists (store) {
  262. if (store.state.fetchers.lists) return
  263. const fetcher = store.state.backendInteractor.startFetchingLists({ store })
  264. store.commit('addFetcher', { fetcherName: 'lists', fetcher })
  265. },
  266. stopFetchingLists (store) {
  267. const fetcher = store.state.fetchers.lists
  268. if (!fetcher) return
  269. store.commit('removeFetcher', { fetcherName: 'lists', fetcher })
  270. },
  271. // Bookmark folders
  272. startFetchingBookmarkFolders (store) {
  273. if (store.state.fetchers.bookmarkFolders) return
  274. const fetcher = store.state.backendInteractor.startFetchingBookmarkFolders({ store })
  275. store.commit('addFetcher', { fetcherName: 'bookmarkFolders', fetcher })
  276. },
  277. stopFetchingBookmarkFolders (store) {
  278. const fetcher = store.state.fetchers.bookmarkFolders
  279. if (!fetcher) return
  280. store.commit('removeFetcher', { fetcherName: 'bookmarkFolders', fetcher })
  281. },
  282. // Pleroma websocket
  283. setWsToken (store, token) {
  284. store.commit('setWsToken', token)
  285. },
  286. initializeSocket ({ commit, state, rootState }) {
  287. // Set up websocket connection
  288. const token = state.wsToken
  289. if (rootState.instance.shoutAvailable && typeof token !== 'undefined' && state.socket === null) {
  290. const socket = new Socket('/socket', { params: { token } })
  291. socket.connect()
  292. commit('setSocket', socket)
  293. useShoutStore().initializeShout(socket)
  294. }
  295. },
  296. disconnectFromSocket ({ commit, state }) {
  297. state.socket && state.socket.disconnect()
  298. commit('setSocket', null)
  299. }
  300. }
  301. }
  302. export default api