aboutsummaryrefslogtreecommitdiff
path: root/src/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/services')
-rw-r--r--src/services/api/api.service.js237
-rw-r--r--src/services/desktop_notification_utils/desktop_notification_utils.js39
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js7
-rw-r--r--src/services/favicon_service/favicon_service.js5
-rw-r--r--src/services/file_type/file_type.service.js18
-rw-r--r--src/services/html_converter/utility.service.js4
-rw-r--r--src/services/locale/locale.service.js1
-rw-r--r--src/services/matcher/matcher.service.js7
-rw-r--r--src/services/notification_utils/notification_utils.js87
-rw-r--r--src/services/notifications_fetcher/notifications_fetcher.service.js14
-rw-r--r--src/services/random_seed/random_seed.service.js3
-rw-r--r--src/services/status_poster/status_poster.service.js2
-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}`))
}