diff options
Diffstat (limited to 'src/services')
| -rw-r--r-- | src/services/api/api.service.js | 237 | ||||
| -rw-r--r-- | src/services/desktop_notification_utils/desktop_notification_utils.js | 39 | ||||
| -rw-r--r-- | src/services/entity_normalizer/entity_normalizer.service.js | 7 | ||||
| -rw-r--r-- | src/services/favicon_service/favicon_service.js | 5 | ||||
| -rw-r--r-- | src/services/file_type/file_type.service.js | 18 | ||||
| -rw-r--r-- | src/services/html_converter/utility.service.js | 4 | ||||
| -rw-r--r-- | src/services/locale/locale.service.js | 1 | ||||
| -rw-r--r-- | src/services/matcher/matcher.service.js | 7 | ||||
| -rw-r--r-- | src/services/notification_utils/notification_utils.js | 87 | ||||
| -rw-r--r-- | src/services/notifications_fetcher/notifications_fetcher.service.js | 14 | ||||
| -rw-r--r-- | src/services/random_seed/random_seed.service.js | 3 | ||||
| -rw-r--r-- | src/services/status_poster/status_poster.service.js | 2 | ||||
| -rw-r--r-- | src/services/sw/sw.js (renamed from src/services/push/push.js) | 55 |
13 files changed, 430 insertions, 49 deletions
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index e90723a1..a47c638c 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -107,6 +107,22 @@ const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` +const PLEROMA_SCROBBLES_URL = id => `/api/v1/pleroma/accounts/${id}/scrobbles` +const PLEROMA_USER_FAVORITES_TIMELINE_URL = id => `/api/v1/pleroma/accounts/${id}/favourites` + +const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config' +const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions' +const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends' +const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install' + +const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji' +const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import' +const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) => `/api/v1/pleroma/emoji/packs?page=${page}&page_size=${pageSize}` +const PLEROMA_EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}` +const PLEROMA_EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download' +const PLEROMA_EMOJI_PACKS_LS_REMOTE_URL = + (url, page, pageSize) => `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}` +const PLEROMA_EMOJI_UPDATE_FILE_URL = (name) => `/api/v1/pleroma/emoji/packs/files?name=${name}` const oldfetch = window.fetch @@ -665,6 +681,7 @@ const fetchTimeline = ({ timeline, credentials, since = false, + minId = false, until = false, userId = false, listId = false, @@ -683,6 +700,7 @@ const fetchTimeline = ({ media: MASTODON_USER_TIMELINE_URL, list: MASTODON_LIST_TIMELINE_URL, favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, + publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL, tag: MASTODON_TAG_TIMELINE_URL, bookmarks: MASTODON_BOOKMARK_TIMELINE_URL } @@ -691,6 +709,10 @@ const fetchTimeline = ({ let url = timelineUrls[timeline] + if (timeline === 'favorites' && userId) { + url = timelineUrls.publicFavorites(userId) + } + if (timeline === 'user' || timeline === 'media') { url = url(userId) } @@ -699,6 +721,9 @@ const fetchTimeline = ({ url = url(listId) } + if (minId) { + params.push(['min_id', minId]) + } if (since) { params.push(['since_id', since]) } @@ -822,6 +847,7 @@ const postStatus = ({ poll, mediaIds = [], inReplyToStatusId, + quoteId, contentType, preview, idempotencyKey @@ -854,6 +880,9 @@ const postStatus = ({ if (inReplyToStatusId) { form.append('in_reply_to_id', inReplyToStatusId) } + if (quoteId) { + form.append('quote_id', quoteId) + } if (preview) { form.append('preview', 'true') } @@ -1668,6 +1697,195 @@ const setReportState = ({ id, state, credentials }) => { }) } +// ADMIN STUFF // EXPERIMENTAL +const fetchInstanceDBConfig = ({ credentials }) => { + return fetch(PLEROMA_ADMIN_CONFIG_URL, { + headers: authHeaders(credentials) + }) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return { + error: response + } + } + }) +} + +const fetchInstanceConfigDescriptions = ({ credentials }) => { + return fetch(PLEROMA_ADMIN_DESCRIPTIONS_URL, { + headers: authHeaders(credentials) + }) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return { + error: response + } + } + }) +} + +const fetchAvailableFrontends = ({ credentials }) => { + return fetch(PLEROMA_ADMIN_FRONTENDS_URL, { + headers: authHeaders(credentials) + }) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return { + error: response + } + } + }) +} + +const pushInstanceDBConfig = ({ credentials, payload }) => { + return fetch(PLEROMA_ADMIN_CONFIG_URL, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...authHeaders(credentials) + }, + method: 'POST', + body: JSON.stringify(payload) + }) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return { + error: response + } + } + }) +} + +const installFrontend = ({ credentials, payload }) => { + return fetch(PLEROMA_ADMIN_FRONTENDS_INSTALL_URL, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...authHeaders(credentials) + }, + method: 'POST', + body: JSON.stringify(payload) + }) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return { + error: response + } + } + }) +} + +const fetchScrobbles = ({ accountId, limit = 1 }) => { + let url = PLEROMA_SCROBBLES_URL(accountId) + const params = [['limit', limit]] + const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') + url += `?${queryString}` + return fetch(url, {}) + .then((response) => { + if (response.ok) { + return response.json() + } else { + return { + error: response + } + } + }) +} + +const deleteEmojiPack = ({ name }) => { + return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'DELETE' }) +} + +const reloadEmoji = () => { + return fetch(PLEROMA_EMOJI_RELOAD_URL, { method: 'POST' }) +} + +const importEmojiFromFS = () => { + return fetch(PLEROMA_EMOJI_IMPORT_FS_URL) +} + +const createEmojiPack = ({ name }) => { + return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'POST' }) +} + +const listEmojiPacks = ({ page, pageSize }) => { + return fetch(PLEROMA_EMOJI_PACKS_URL(page, pageSize)) +} + +const listRemoteEmojiPacks = ({ instance, page, pageSize }) => { + if (!instance.startsWith('http')) { + instance = 'https://' + instance + } + + return fetch( + PLEROMA_EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize), + { + headers: { 'Content-Type': 'application/json' } + } + ) +} + +const downloadRemoteEmojiPack = ({ instance, packName, as }) => { + return fetch( + PLEROMA_EMOJI_PACKS_DL_REMOTE_URL, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: instance, name: packName, as + }) + } + ) +} + +const saveEmojiPackMetadata = ({ name, newData }) => { + return fetch( + PLEROMA_EMOJI_PACK_URL(name), + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ metadata: newData }) + } + ) +} + +const addNewEmojiFile = ({ packName, file, shortcode, filename }) => { + const data = new FormData() + if (filename.trim() !== '') { data.set('filename', filename) } + if (shortcode.trim() !== '') { data.set('shortcode', shortcode) } + data.set('file', file) + + return fetch( + PLEROMA_EMOJI_UPDATE_FILE_URL(packName), + { method: 'POST', body: data } + ) +} + +const updateEmojiFile = ({ packName, shortcode, newShortcode, newFilename, force }) => { + return fetch( + PLEROMA_EMOJI_UPDATE_FILE_URL(packName), + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ shortcode, new_shortcode: newShortcode, new_filename: newFilename, force }) + } + ) +} + +const deleteEmojiFile = ({ packName, shortcode }) => { + return fetch(`${PLEROMA_EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, { method: 'DELETE' }) +} + const apiService = { verifyCredentials, fetchTimeline, @@ -1781,7 +1999,24 @@ const apiService = { postAnnouncement, editAnnouncement, deleteAnnouncement, - adminFetchAnnouncements + fetchScrobbles, + adminFetchAnnouncements, + fetchInstanceDBConfig, + fetchInstanceConfigDescriptions, + fetchAvailableFrontends, + pushInstanceDBConfig, + installFrontend, + importEmojiFromFS, + reloadEmoji, + listEmojiPacks, + createEmojiPack, + deleteEmojiPack, + saveEmojiPackMetadata, + addNewEmojiFile, + updateEmojiFile, + deleteEmojiFile, + listRemoteEmojiPacks, + downloadRemoteEmojiPack } export default apiService diff --git a/src/services/desktop_notification_utils/desktop_notification_utils.js b/src/services/desktop_notification_utils/desktop_notification_utils.js index b84a1f75..80b8c6e0 100644 --- a/src/services/desktop_notification_utils/desktop_notification_utils.js +++ b/src/services/desktop_notification_utils/desktop_notification_utils.js @@ -1,9 +1,38 @@ +import { + showDesktopNotification as swDesktopNotification, + closeDesktopNotification as swCloseDesktopNotification, + isSWSupported +} from '../sw/sw.js' +const state = { failCreateNotif: false } + export const showDesktopNotification = (rootState, desktopNotificationOpts) => { if (!('Notification' in window && window.Notification.permission === 'granted')) return - if (rootState.statuses.notifications.desktopNotificationSilence) { return } + if (rootState.notifications.desktopNotificationSilence) { return } + + if (isSWSupported()) { + swDesktopNotification(desktopNotificationOpts) + } else if (!state.failCreateNotif) { + try { + const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts) + setTimeout(desktopNotification.close.bind(desktopNotification), 5000) + } catch { + state.failCreateNotif = true + } + } +} + +export const closeDesktopNotification = (rootState, { id }) => { + if (!('Notification' in window && window.Notification.permission === 'granted')) return + + if (isSWSupported()) { + swCloseDesktopNotification({ id }) + } +} + +export const closeAllDesktopNotifications = (rootState) => { + if (!('Notification' in window && window.Notification.permission === 'granted')) return - const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts) - // Chrome is known for not closing notifications automatically - // according to MDN, anyway. - setTimeout(desktopNotification.close.bind(desktopNotification), 5000) + if (isSWSupported()) { + swCloseDesktopNotification({}) + } } diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index adefc5a5..21e67b67 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -107,6 +107,7 @@ export const parseUser = (data) => { output.allow_following_move = data.pleroma.allow_following_move + output.hide_favorites = data.pleroma.hide_favorites output.hide_follows = data.pleroma.hide_follows output.hide_followers = data.pleroma.hide_followers output.hide_follows_count = data.pleroma.hide_follows_count @@ -165,6 +166,7 @@ export const parseUser = (data) => { output.show_role = data.source.pleroma.show_role output.discoverable = data.source.pleroma.discoverable output.show_birthday = data.pleroma.show_birthday + output.actor_type = data.source.pleroma.actor_type } } @@ -325,6 +327,10 @@ export const parseStatus = (data) => { output.thread_muted = pleroma.thread_muted output.emoji_reactions = pleroma.emoji_reactions output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible + output.quote = pleroma.quote ? parseStatus(pleroma.quote) : undefined + output.quote_id = pleroma.quote_id ? pleroma.quote_id : (output.quote ? output.quote.id : undefined) + output.quote_url = pleroma.quote_url + output.quote_visible = pleroma.quote_visible } else { output.text = data.content output.summary = data.spoiler_text @@ -435,7 +441,6 @@ export const parseNotification = (data) => { output.type = mastoDict[data.type] || data.type output.seen = data.pleroma.is_seen output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null - output.action = output.status // TODO: Refactor, this is unneeded output.target = output.type !== 'move' ? null : parseUser(data.target) diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js index 7e19629d..df603bb4 100644 --- a/src/services/favicon_service/favicon_service.js +++ b/src/services/favicon_service/favicon_service.js @@ -55,10 +55,13 @@ const createFaviconService = () => { }) } + const getOriginalFavicons = () => [...favicons] + return { initFaviconService, clearFaviconBadge, - drawFaviconBadge + drawFaviconBadge, + getOriginalFavicons } } diff --git a/src/services/file_type/file_type.service.js b/src/services/file_type/file_type.service.js index 5182ecd1..f7989ffe 100644 --- a/src/services/file_type/file_type.service.js +++ b/src/services/file_type/file_type.service.js @@ -1,7 +1,7 @@ // TODO this func might as well take the entire file and use its mimetype // or the entire service could be just mimetype service that only operates // on mimetypes and not files. Currently the naming is confusing. -const fileType = mimetype => { +export const fileType = mimetype => { if (mimetype.match(/flash/)) { return 'flash' } @@ -25,11 +25,25 @@ const fileType = mimetype => { return 'unknown' } -const fileMatchesSomeType = (types, file) => +export const fileTypeExt = url => { + if (url.match(/\.(a?png|jpe?g|gif|webp|avif)$/)) { + return 'image' + } + if (url.match(/\.(ogv|mp4|webm|mov)$/)) { + return 'video' + } + if (url.match(/\.(it|s3m|mod|umx|mp3|aac|m4a|flac|alac|ogg|oga|opus|wav|ape|midi?)$/)) { + return 'audio' + } + return 'unknown' +} + +export const fileMatchesSomeType = (types, file) => types.some(type => fileType(file.mimetype) === type) const fileTypeService = { fileType, + fileTypeExt, fileMatchesSomeType } diff --git a/src/services/html_converter/utility.service.js b/src/services/html_converter/utility.service.js index f1042971..f8e62dfe 100644 --- a/src/services/html_converter/utility.service.js +++ b/src/services/html_converter/utility.service.js @@ -5,7 +5,7 @@ * @return {String} - tagname, i.e. "div" */ export const getTagName = (tag) => { - const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag) + const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gis.exec(tag) return result && (result[1] || result[2]) } @@ -22,7 +22,7 @@ export const getAttrs = (tag, filter) => { .replace(new RegExp('^' + getTagName(tag)), '') .replace(/\/?$/, '') .trim() - const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi)) + const attrs = Array.from(innertag.matchAll(/([a-z]+[a-z0-9-]*)(?:=("[^"]+?"|'[^']+?'))?/gi)) .map(([trash, key, value]) => [key, value]) .map(([k, v]) => { if (!v) return [k, true] diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js index a4af8b90..24ed3cdb 100644 --- a/src/services/locale/locale.service.js +++ b/src/services/locale/locale.service.js @@ -19,6 +19,7 @@ const internalToBackendLocaleMulti = codes => { const getLanguageName = (code) => { const specialLanguageNames = { ja_easy: 'やさしいにほんご', + 'nan-TW': '臺語(閩南語)', zh: '简体中文', zh_Hant: '繁體中文' } diff --git a/src/services/matcher/matcher.service.js b/src/services/matcher/matcher.service.js index b6c4e909..54f02d31 100644 --- a/src/services/matcher/matcher.service.js +++ b/src/services/matcher/matcher.service.js @@ -14,8 +14,11 @@ export const mentionMatchesUrl = (attention, url) => { * @param {string} url */ export const extractTagFromUrl = (url) => { - const regex = /tag[s]*\/(\w+)$/g - const result = regex.exec(url) + const decoded = decodeURI(url) + // https://git.pleroma.social/pleroma/elixir-libraries/linkify/-/blob/master/lib/linkify/parser.ex + // https://www.pcre.org/original/doc/html/pcrepattern.html + const regex = /tag[s]*\/([\p{L}\p{N}_]*[\p{Alphabetic}_·\u{200c}][\p{L}\p{N}_·\p{M}\u{200c}]*)$/ug + const result = regex.exec(decoded) if (!result) { return false } diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 0f8b9b02..7b705e65 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -1,28 +1,36 @@ -import { filter, sortBy, includes } from 'lodash' import { muteWordHits } from '../status_parser/status_parser.js' import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js' -export const notificationsFromStore = store => store.state.statuses.notifications.data +import FaviconService from 'src/services/favicon_service/favicon_service.js' + +export const ACTIONABLE_NOTIFICATION_TYPES = new Set(['mention', 'pleroma:report', 'follow_request']) + +let cachedBadgeUrl = null + +export const notificationsFromStore = store => store.state.notifications.data export const visibleTypes = store => { - const rootState = store.rootState || store.state + // When called from within a module we need rootGetters to access wider scope + // however when called from a component (i.e. this.$store) we already have wider scope + const rootGetters = store.rootGetters || store.getters + const { notificationVisibility } = rootGetters.mergedConfig return ([ - rootState.config.notificationVisibility.likes && 'like', - rootState.config.notificationVisibility.mentions && 'mention', - rootState.config.notificationVisibility.repeats && 'repeat', - rootState.config.notificationVisibility.follows && 'follow', - rootState.config.notificationVisibility.followRequest && 'follow_request', - rootState.config.notificationVisibility.moves && 'move', - rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction', - rootState.config.notificationVisibility.reports && 'pleroma:report', - rootState.config.notificationVisibility.polls && 'poll' + notificationVisibility.likes && 'like', + notificationVisibility.mentions && 'mention', + notificationVisibility.repeats && 'repeat', + notificationVisibility.follows && 'follow', + notificationVisibility.followRequest && 'follow_request', + notificationVisibility.moves && 'move', + notificationVisibility.emojiReactions && 'pleroma:emoji_reaction', + notificationVisibility.reports && 'pleroma:report', + notificationVisibility.polls && 'poll' ].filter(_ => _)) } -const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction', 'poll'] +const statusNotifications = new Set(['like', 'mention', 'repeat', 'pleroma:emoji_reaction', 'poll']) -export const isStatusNotification = (type) => includes(statusNotifications, type) +export const isStatusNotification = (type) => statusNotifications.has(type) export const isValidNotification = (notification) => { if (isStatusNotification(notification.type) && !notification.status) { @@ -49,35 +57,57 @@ const sortById = (a, b) => { const isMutedNotification = (store, notification) => { if (!notification.status) return - return notification.status.muted || muteWordHits(notification.status, store.rootGetters.mergedConfig.muteWords).length > 0 + const rootGetters = store.rootGetters || store.getters + return notification.status.muted || muteWordHits(notification.status, rootGetters.mergedConfig.muteWords).length > 0 } export const maybeShowNotification = (store, notification) => { const rootState = store.rootState || store.state + const rootGetters = store.rootGetters || store.getters if (notification.seen) return if (!visibleTypes(store).includes(notification.type)) return if (notification.type === 'mention' && isMutedNotification(store, notification)) return - const notificationObject = prepareNotificationObject(notification, store.rootGetters.i18n) + const notificationObject = prepareNotificationObject(notification, rootGetters.i18n) showDesktopNotification(rootState, notificationObject) } export const filteredNotificationsFromStore = (store, types) => { // map is just to clone the array since sort mutates it and it causes some issues - let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById) - sortedNotifications = sortBy(sortedNotifications, 'seen') + const sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById) + // TODO implement sorting elsewhere and make it optional return sortedNotifications.filter( (notification) => (types || visibleTypes(store)).includes(notification.type) ) } -export const unseenNotificationsFromStore = store => - filter(filteredNotificationsFromStore(store), ({ seen }) => !seen) +export const unseenNotificationsFromStore = store => { + const rootGetters = store.rootGetters || store.getters + const ignoreInactionableSeen = rootGetters.mergedConfig.ignoreInactionableSeen + + return filteredNotificationsFromStore(store).filter(({ seen, type }) => { + if (!ignoreInactionableSeen) return !seen + if (seen) return false + return ACTIONABLE_NOTIFICATION_TYPES.has(type) + }) +} export const prepareNotificationObject = (notification, i18n) => { + if (cachedBadgeUrl === null) { + const favicons = FaviconService.getOriginalFavicons() + const favicon = favicons[favicons.length - 1] + if (!favicon) { + cachedBadgeUrl = 'about:blank' + } else { + cachedBadgeUrl = favicon.favimg.src + } + } + const notifObj = { - tag: notification.id + tag: notification.id, + type: notification.type, + badge: cachedBadgeUrl } const status = notification.status const title = notification.from_profile.name @@ -124,3 +154,18 @@ export const prepareNotificationObject = (notification, i18n) => { return notifObj } + +export const countExtraNotifications = (store) => { + const rootGetters = store.rootGetters || store.getters + const mergedConfig = rootGetters.mergedConfig + + if (!mergedConfig.showExtraNotifications) { + return 0 + } + + return [ + mergedConfig.showChatsInExtraNotifications ? rootGetters.unreadChatCount : 0, + mergedConfig.showAnnouncementsInExtraNotifications ? rootGetters.unreadAnnouncementCount : 0, + mergedConfig.showFollowRequestsInExtraNotifications ? rootGetters.followRequestCount : 0 + ].reduce((a, c) => a + c, 0) +} diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 6c247210..c91a86c8 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -21,7 +21,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { const args = { credentials } const { getters } = store const rootState = store.rootState || store.state - const timelineData = rootState.statuses.notifications + const timelineData = rootState.notifications const hideMutedPosts = getters.mergedConfig.hideMutedPosts args.includeTypes = mastoApiNotificationTypes @@ -49,10 +49,14 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { // The normal maxId-check does not tell if older notifications have changed const notifications = timelineData.data const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id) - const numUnseenNotifs = notifications.length - readNotifsIds.length - if (numUnseenNotifs > 0 && readNotifsIds.length > 0) { - args.since = Math.max(...readNotifsIds) - fetchNotifications({ store, args, older }) + const unreadNotifsIds = notifications.filter(n => !n.seen).map(n => n.id) + if (readNotifsIds.length > 0 && readNotifsIds.length > 0) { + const minId = Math.min(...unreadNotifsIds) // Oldest known unread notification + if (minId !== Infinity) { + args.since = false // Don't use since_id since it sorta conflicts with min_id + args.minId = minId - 1 // go beyond + fetchNotifications({ store, args, older }) + } } return result diff --git a/src/services/random_seed/random_seed.service.js b/src/services/random_seed/random_seed.service.js new file mode 100644 index 00000000..33f92e81 --- /dev/null +++ b/src/services/random_seed/random_seed.service.js @@ -0,0 +1,3 @@ +const genRandomSeed = () => `${Math.random()}`.replace('.', '-') + +export default genRandomSeed diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js index 1eb10bb6..aaef5a7a 100644 --- a/src/services/status_poster/status_poster.service.js +++ b/src/services/status_poster/status_poster.service.js @@ -10,6 +10,7 @@ const postStatus = ({ poll, media = [], inReplyToStatusId = undefined, + quoteId = undefined, contentType = 'text/plain', preview = false, idempotencyKey = '' @@ -24,6 +25,7 @@ const postStatus = ({ sensitive, mediaIds, inReplyToStatusId, + quoteId, contentType, poll, preview, diff --git a/src/services/push/push.js b/src/services/sw/sw.js index 1787ac36..554cc7b8 100644 --- a/src/services/push/push.js +++ b/src/services/sw/sw.js @@ -10,8 +10,12 @@ function urlBase64ToUint8Array (base64String) { return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0))) } +export function isSWSupported () { + return 'serviceWorker' in navigator +} + function isPushSupported () { - return 'serviceWorker' in navigator && 'PushManager' in window + return 'PushManager' in window } function getOrCreateServiceWorker () { @@ -24,7 +28,7 @@ function subscribePush (registration, isEnabled, vapidPublicKey) { if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found')) const subscribeOptions = { - userVisibleOnly: true, + userVisibleOnly: false, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) } return registration.pushManager.subscribe(subscribeOptions) @@ -39,7 +43,7 @@ function unsubscribePush (registration) { } function deleteSubscriptionFromBackEnd (token) { - return window.fetch('/api/v1/push/subscription/', { + return fetch('/api/v1/push/subscription/', { method: 'DELETE', headers: { 'Content-Type': 'application/json', @@ -78,6 +82,44 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility) return responseData }) } +export async function initServiceWorker (store) { + if (!isSWSupported()) return + await getOrCreateServiceWorker() + navigator.serviceWorker.addEventListener('message', (event) => { + const { dispatch } = store + const { type, ...rest } = event.data + + switch (type) { + case 'notificationClicked': + dispatch('notificationClicked', { id: rest.id }) + } + }) +} + +export async function showDesktopNotification (content) { + if (!isSWSupported) return + const { active: sw } = await window.navigator.serviceWorker.getRegistration() + if (!sw) return console.error('No serviceworker found!') + sw.postMessage({ type: 'desktopNotification', content }) +} + +export async function closeDesktopNotification ({ id }) { + if (!isSWSupported) return + const { active: sw } = await window.navigator.serviceWorker.getRegistration() + if (!sw) return console.error('No serviceworker found!') + if (id >= 0) { + sw.postMessage({ type: 'desktopNotificationClose', content: { id } }) + } else { + sw.postMessage({ type: 'desktopNotificationClose', content: { all: true } }) + } +} + +export async function updateFocus () { + if (!isSWSupported) return + const { active: sw } = await window.navigator.serviceWorker.getRegistration() + if (!sw) return console.error('No serviceworker found!') + sw.postMessage({ type: 'updateFocus' }) +} export function registerPushNotifications (isEnabled, vapidPublicKey, token, notificationVisibility) { if (isPushSupported()) { @@ -98,13 +140,8 @@ export function unregisterPushNotifications (token) { }) .then(([registration, unsubResult]) => { if (!unsubResult) { - console.warn('Push subscription cancellation wasn\'t successful, killing SW anyway...') + console.warn('Push subscription cancellation wasn\'t successful') } - return registration.unregister().then((result) => { - if (!result) { - console.warn('Failed to kill SW') - } - }) }) ]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`)) } |
