logo

pleroma-fe

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

api.js (11300B)


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