aboutsummaryrefslogtreecommitdiff
path: root/src/services
diff options
context:
space:
mode:
Diffstat (limited to 'src/services')
-rw-r--r--src/services/api/api.service.js26
-rw-r--r--src/services/chat_service/chat_service.js77
-rw-r--r--src/services/chat_utils/chat_utils.js23
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js39
-rw-r--r--src/services/favicon_service/favicon_service.js61
-rw-r--r--src/services/locale/locale.service.js12
-rw-r--r--src/services/notifications_fetcher/notifications_fetcher.service.js18
-rw-r--r--src/services/promise_interval/promise_interval.js9
-rw-r--r--src/services/style_setter/style_setter.js15
-rw-r--r--src/services/theme_data/pleromafe.js4
-rw-r--r--src/services/timeline_fetcher/timeline_fetcher.service.js18
11 files changed, 264 insertions, 38 deletions
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 1a3495d4..f4483149 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -129,7 +129,11 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
return reject(new StatusCodeError(response.status, json, { url, options }, response))
}
return resolve(json)
- }))
+ })
+ .catch((error) => {
+ return reject(new StatusCodeError(response.status, error, { url, options }, response))
+ })
+ )
})
}
@@ -158,7 +162,12 @@ const updateProfileImages = ({ credentials, avatar = null, banner = null, backgr
body: form
})
.then((data) => data.json())
- .then((data) => parseUser(data))
+ .then((data) => {
+ if (data.error) {
+ throw new Error(data.error)
+ }
+ return parseUser(data)
+ })
}
const updateProfile = ({ credentials, params }) => {
@@ -556,7 +565,7 @@ const fetchTimeline = ({
})
.then((data) => data.json())
.then((data) => {
- if (!data.error) {
+ if (!data.errors) {
return { data: data.map(isNotifications ? parseNotification : parseStatus), pagination }
} else {
data.status = status
@@ -1210,7 +1219,7 @@ const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
})
}
-const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
+const sendChatMessage = ({ id, content, mediaId = null, idempotencyKey, credentials }) => {
const payload = {
'content': content
}
@@ -1219,11 +1228,18 @@ const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
payload['media_id'] = mediaId
}
+ const headers = {}
+
+ if (idempotencyKey) {
+ headers['idempotency-key'] = idempotencyKey
+ }
+
return promisedRequest({
url: PLEROMA_CHAT_MESSAGES_URL(id),
method: 'POST',
payload: payload,
- credentials
+ credentials,
+ headers
})
}
diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js
index 95c69482..e653ebc1 100644
--- a/src/services/chat_service/chat_service.js
+++ b/src/services/chat_service/chat_service.js
@@ -3,9 +3,10 @@ import _ from 'lodash'
const empty = (chatId) => {
return {
idIndex: {},
+ idempotencyKeyIndex: {},
messages: [],
newMessageCount: 0,
- lastSeenTimestamp: 0,
+ lastSeenMessageId: '0',
chatId: chatId,
minId: undefined,
maxId: undefined
@@ -13,10 +14,20 @@ const empty = (chatId) => {
}
const clear = (storage) => {
- storage.idIndex = {}
- storage.messages.splice(0, storage.messages.length)
+ const failedMessageIds = []
+
+ for (const message of storage.messages) {
+ if (message.error) {
+ failedMessageIds.push(message.id)
+ } else {
+ delete storage.idIndex[message.id]
+ delete storage.idempotencyKeyIndex[message.idempotency_key]
+ }
+ }
+
+ storage.messages = storage.messages.filter(m => failedMessageIds.includes(m.id))
storage.newMessageCount = 0
- storage.lastSeenTimestamp = 0
+ storage.lastSeenMessageId = '0'
storage.minId = undefined
storage.maxId = undefined
}
@@ -37,6 +48,25 @@ const deleteMessage = (storage, messageId) => {
}
}
+const handleMessageError = (storage, fakeId, isRetry) => {
+ if (!storage) { return }
+ const fakeMessage = storage.idIndex[fakeId]
+ if (fakeMessage) {
+ fakeMessage.error = true
+ fakeMessage.pending = false
+ if (!isRetry) {
+ // Ensure the failed message doesn't stay at the bottom of the list.
+ const lastPersistedMessage = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'desc'])[0]
+ if (lastPersistedMessage) {
+ const oldId = fakeMessage.id
+ fakeMessage.id = `${lastPersistedMessage.id}-${new Date().getTime()}`
+ storage.idIndex[fakeMessage.id] = fakeMessage
+ delete storage.idIndex[oldId]
+ }
+ }
+ }
+}
+
const add = (storage, { messages: newMessages, updateMaxId = true }) => {
if (!storage) { return }
for (let i = 0; i < newMessages.length; i++) {
@@ -45,7 +75,25 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
// sanity check
if (message.chat_id !== storage.chatId) { return }
- if (!storage.minId || message.id < storage.minId) {
+ if (message.fakeId) {
+ const fakeMessage = storage.idIndex[message.fakeId]
+ if (fakeMessage) {
+ // In case the same id exists (chat update before POST response)
+ // make sure to remove the older duplicate message.
+ if (storage.idIndex[message.id]) {
+ delete storage.idIndex[message.id]
+ storage.messages = storage.messages.filter(msg => msg.id !== message.id)
+ }
+ Object.assign(fakeMessage, message, { error: false })
+ delete fakeMessage['fakeId']
+ storage.idIndex[fakeMessage.id] = fakeMessage
+ delete storage.idIndex[message.fakeId]
+
+ return
+ }
+ }
+
+ if (!storage.minId || (!message.pending && message.id < storage.minId)) {
storage.minId = message.id
}
@@ -55,20 +103,26 @@ const add = (storage, { messages: newMessages, updateMaxId = true }) => {
}
}
- if (!storage.idIndex[message.id]) {
- if (storage.lastSeenTimestamp < message.created_at) {
+ if (!storage.idIndex[message.id] && !isConfirmation(storage, message)) {
+ if (storage.lastSeenMessageId < message.id) {
storage.newMessageCount++
}
- storage.messages.push(message)
storage.idIndex[message.id] = message
+ storage.messages.push(storage.idIndex[message.id])
+ storage.idempotencyKeyIndex[message.idempotency_key] = true
}
}
}
+const isConfirmation = (storage, message) => {
+ if (!message.idempotency_key) return
+ return storage.idempotencyKeyIndex[message.idempotency_key]
+}
+
const resetNewMessageCount = (storage) => {
if (!storage) { return }
storage.newMessageCount = 0
- storage.lastSeenTimestamp = new Date()
+ storage.lastSeenMessageId = storage.maxId
}
// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
@@ -76,7 +130,7 @@ const getView = (storage) => {
if (!storage) { return [] }
const result = []
- const messages = _.sortBy(storage.messages, ['id', 'desc'])
+ const messages = _.orderBy(storage.messages, ['pending', 'id'], ['asc', 'asc'])
const firstMessage = messages[0]
let previousMessage = messages[messages.length - 1]
let currentMessageChainId
@@ -148,7 +202,8 @@ const ChatService = {
getView,
deleteMessage,
resetNewMessageCount,
- clear
+ clear,
+ handleMessageError
}
export default ChatService
diff --git a/src/services/chat_utils/chat_utils.js b/src/services/chat_utils/chat_utils.js
index 583438f7..de6e0625 100644
--- a/src/services/chat_utils/chat_utils.js
+++ b/src/services/chat_utils/chat_utils.js
@@ -3,7 +3,7 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_n
export const maybeShowChatNotification = (store, chat) => {
if (!chat.lastMessage) return
if (store.rootState.chats.currentChatId === chat.id && !document.hidden) return
- if (store.rootState.users.currentUser.id === chat.lastMessage.account.id) return
+ if (store.rootState.users.currentUser.id === chat.lastMessage.account_id) return
const opts = {
tag: chat.lastMessage.id,
@@ -18,3 +18,24 @@ export const maybeShowChatNotification = (store, chat) => {
showDesktopNotification(store.rootState, opts)
}
+
+export const buildFakeMessage = ({ content, chatId, attachments, userId, idempotencyKey }) => {
+ const fakeMessage = {
+ content,
+ chat_id: chatId,
+ created_at: new Date(),
+ id: `${new Date().getTime()}`,
+ attachments: attachments,
+ account_id: userId,
+ idempotency_key: idempotencyKey,
+ emojis: [],
+ pending: true,
+ isNormalized: true
+ }
+
+ if (attachments[0]) {
+ fakeMessage.attachment = attachments[0]
+ }
+
+ return fakeMessage
+}
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 1884478a..6ed663e1 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,6 +1,16 @@
import escape from 'escape-html'
import parseLinkHeader from 'parse-link-header'
import { isStatusNotification } from '../notification_utils/notification_utils.js'
+import punycode from 'punycode.js'
+
+/** NOTICE! **
+ * Do not initialize UI-generated data here.
+ * It will override existing data.
+ *
+ * i.e. user.pinnedStatusIds was set to [] here
+ * UI code would update it with data but upon next user fetch
+ * it would be reverted back to []
+ */
const qvitterStatusType = (status) => {
if (status.is_post_verb) {
@@ -53,7 +63,7 @@ export const parseUser = (data) => {
output.fields = data.fields
output.fields_html = data.fields.map(field => {
return {
- name: addEmojis(field.name, data.emojis),
+ name: addEmojis(escape(field.name), data.emojis),
value: addEmojis(field.value, data.emojis)
}
})
@@ -173,15 +183,17 @@ export const parseUser = (data) => {
output.locked = data.locked
output.followers_count = data.followers_count
output.statuses_count = data.statuses_count
- output.friendIds = []
- output.followerIds = []
- output.pinnedStatusIds = []
if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count
output.tags = data.pleroma.tags
- output.deactivated = data.pleroma.deactivated
+
+ // deactivated was changed to is_active in Pleroma 2.3.0
+ // so check if is_active is present
+ output.deactivated = typeof data.pleroma.is_active !== 'undefined'
+ ? !data.pleroma.is_active // new backend
+ : data.pleroma.deactivated // old backend
output.notification_settings = data.pleroma.notification_settings
output.unread_chat_count = data.pleroma.unread_chat_count
@@ -191,6 +203,18 @@ export const parseUser = (data) => {
output.rights = output.rights || {}
output.notification_settings = output.notification_settings || {}
+ // Convert punycode to unicode
+ if (output.screen_name.includes('@')) {
+ const parts = output.screen_name.split('@')
+ let unicodeDomain = punycode.toUnicode(parts[1])
+ if (unicodeDomain !== parts[1]) {
+ // Add some identifier so users can potentially spot spoofing attempts:
+ // lain.com and xn--lin-6cd.com would appear identical otherwise.
+ unicodeDomain = '🌏' + unicodeDomain
+ output.screen_name = [parts[0], unicodeDomain].join('@')
+ }
+ }
+
return output
}
@@ -274,7 +298,7 @@ export const parseStatus = (data) => {
if (output.poll) {
output.poll.options = (output.poll.options || []).map(field => ({
...field,
- title_html: addEmojis(field.title, data.emojis)
+ title_html: addEmojis(escape(field.title), data.emojis)
}))
}
output.pinned = data.pinned
@@ -429,6 +453,9 @@ export const parseChatMessage = (message) => {
} else {
output.attachments = []
}
+ output.pending = !!message.pending
+ output.error = false
+ output.idempotency_key = message.idempotency_key
output.isNormalized = true
return output
}
diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js
new file mode 100644
index 00000000..d1ddee41
--- /dev/null
+++ b/src/services/favicon_service/favicon_service.js
@@ -0,0 +1,61 @@
+import { find } from 'lodash'
+
+const createFaviconService = () => {
+ let favimg, favcanvas, favcontext, favicon
+ const faviconWidth = 128
+ const faviconHeight = 128
+ const badgeRadius = 32
+
+ const initFaviconService = () => {
+ const nodes = document.getElementsByTagName('link')
+ favicon = find(nodes, node => node.rel === 'icon')
+ if (favicon) {
+ favcanvas = document.createElement('canvas')
+ favcanvas.width = faviconWidth
+ favcanvas.height = faviconHeight
+ favimg = new Image()
+ favimg.src = favicon.href
+ favcontext = favcanvas.getContext('2d')
+ }
+ }
+
+ const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0
+
+ const clearFaviconBadge = () => {
+ if (!favimg || !favcontext || !favicon) return
+
+ favcontext.clearRect(0, 0, faviconWidth, faviconHeight)
+ if (isImageLoaded(favimg)) {
+ favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
+ }
+ favicon.href = favcanvas.toDataURL('image/png')
+ }
+
+ const drawFaviconBadge = () => {
+ if (!favimg || !favcontext || !favcontext) return
+
+ clearFaviconBadge()
+
+ const style = getComputedStyle(document.body)
+ const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`
+
+ if (isImageLoaded(favimg)) {
+ favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
+ }
+ favcontext.fillStyle = badgeColor
+ favcontext.beginPath()
+ favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false)
+ favcontext.fill()
+ favicon.href = favcanvas.toDataURL('image/png')
+ }
+
+ return {
+ initFaviconService,
+ clearFaviconBadge,
+ drawFaviconBadge
+ }
+}
+
+const FaviconService = createFaviconService()
+
+export default FaviconService
diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js
new file mode 100644
index 00000000..5be99d81
--- /dev/null
+++ b/src/services/locale/locale.service.js
@@ -0,0 +1,12 @@
+const specialLanguageCodes = {
+ 'ja_easy': 'ja',
+ 'zh_Hant': 'zh-HANT'
+}
+
+const internalToBrowserLocale = code => specialLanguageCodes[code] || code
+
+const localeService = {
+ internalToBrowserLocale
+}
+
+export default localeService
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index c908b644..beeb167c 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -2,7 +2,6 @@ import apiService from '../api/api.service.js'
import { promiseInterval } from '../promise_interval/promise_interval.js'
const update = ({ store, notifications, older }) => {
- store.dispatch('setNotificationsError', { value: false })
store.dispatch('addNewNotifications', { notifications, older })
}
@@ -47,11 +46,22 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
const fetchNotifications = ({ store, args, older }) => {
return apiService.fetchTimeline(args)
- .then(({ data: notifications }) => {
+ .then((response) => {
+ if (response.errors) {
+ throw new Error(`${response.status} ${response.statusText}`)
+ }
+ const notifications = response.data
update({ store, notifications, older })
return notifications
- }, () => store.dispatch('setNotificationsError', { value: true }))
- .catch(() => store.dispatch('setNotificationsError', { value: true }))
+ })
+ .catch((error) => {
+ store.dispatch('pushGlobalNotice', {
+ level: 'error',
+ messageKey: 'notifications.error',
+ messageArgs: [error.message],
+ timeout: 5000
+ })
+ })
}
const startFetching = ({ credentials, store }) => {
diff --git a/src/services/promise_interval/promise_interval.js b/src/services/promise_interval/promise_interval.js
index cf17970d..0c0a66a0 100644
--- a/src/services/promise_interval/promise_interval.js
+++ b/src/services/promise_interval/promise_interval.js
@@ -10,7 +10,14 @@ export const promiseInterval = (promiseCall, interval) => {
let timeout = null
const func = () => {
- promiseCall().finally(() => {
+ const promise = promiseCall()
+ // something unexpected happened and promiseCall did not
+ // return a promise, abort the loop.
+ if (!(promise && promise.finally)) {
+ console.warn('promiseInterval: promise call did not return a promise, stopping interval.')
+ return
+ }
+ promise.finally(() => {
if (stopped) return
timeout = window.setTimeout(func, interval)
})
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 07425abd..a2bba67b 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -242,9 +242,18 @@ export const generateShadows = (input, colors) => {
panelHeader: 'panel',
input: 'input'
}
- const inputShadows = input.shadows && !input.themeEngineVersion
- ? shadows2to3(input.shadows, input.opacity)
- : input.shadows || {}
+
+ const cleanInputShadows = Object.fromEntries(
+ Object.entries(input.shadows || {})
+ .map(([name, shadowSlot]) => [
+ name,
+ // defaulting color to black to avoid potential problems
+ shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef }))
+ ])
+ )
+ const inputShadows = cleanInputShadows && !input.themeEngineVersion
+ ? shadows2to3(cleanInputShadows, input.opacity)
+ : cleanInputShadows || {}
const shadows = Object.entries({
...DEFAULT_SHADOWS,
...inputShadows
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 7ed85797..bec1eebd 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -84,6 +84,10 @@ export const SLOT_INHERITANCE = {
opacity: 'bg',
priority: 1
},
+ wallpaper: {
+ depends: ['bg'],
+ color: (mod, bg) => brightness(-2 * mod, bg).rgb
+ },
fg: {
depends: [],
priority: 1
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index 72ea4890..921df3ed 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -6,9 +6,6 @@ import { promiseInterval } from '../promise_interval/promise_interval.js'
const update = ({ store, statuses, timeline, showImmediately, userId, pagination }) => {
const ccTimeline = camelCase(timeline)
- store.dispatch('setError', { value: false })
- store.dispatch('setErrorData', { value: null })
-
store.dispatch('addNewStatuses', {
timeline: ccTimeline,
userId,
@@ -52,9 +49,8 @@ const fetchAndUpdate = ({
return apiService.fetchTimeline(args)
.then(response => {
- if (response.error) {
- store.dispatch('setErrorData', { value: response })
- return
+ if (response.errors) {
+ throw new Error(`${response.status} ${response.statusText}`)
}
const { data: statuses, pagination } = response
@@ -63,7 +59,15 @@ const fetchAndUpdate = ({
}
update({ store, statuses, timeline, showImmediately, userId, pagination })
return { statuses, pagination }
- }, () => store.dispatch('setError', { value: true }))
+ })
+ .catch((error) => {
+ store.dispatch('pushGlobalNotice', {
+ level: 'error',
+ messageKey: 'timeline.error',
+ messageArgs: [error.message],
+ timeout: 5000
+ })
+ })
}
const startFetching = ({ timeline = 'friends', credentials, store, userId = false, tag = false }) => {