diff options
Diffstat (limited to 'src/services')
| -rw-r--r-- | src/services/api/api.service.js | 26 | ||||
| -rw-r--r-- | src/services/chat_service/chat_service.js | 77 | ||||
| -rw-r--r-- | src/services/chat_utils/chat_utils.js | 23 | ||||
| -rw-r--r-- | src/services/entity_normalizer/entity_normalizer.service.js | 39 | ||||
| -rw-r--r-- | src/services/favicon_service/favicon_service.js | 61 | ||||
| -rw-r--r-- | src/services/locale/locale.service.js | 12 | ||||
| -rw-r--r-- | src/services/notifications_fetcher/notifications_fetcher.service.js | 18 | ||||
| -rw-r--r-- | src/services/promise_interval/promise_interval.js | 9 | ||||
| -rw-r--r-- | src/services/style_setter/style_setter.js | 15 | ||||
| -rw-r--r-- | src/services/theme_data/pleromafe.js | 4 | ||||
| -rw-r--r-- | src/services/timeline_fetcher/timeline_fetcher.service.js | 18 |
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 }) => { |
