Add Chats
This commit is contained in:
parent
a0ddcbdf5b
commit
aa2cf51c05
69 changed files with 2794 additions and 161 deletions
|
@ -1,5 +1,5 @@
|
|||
import { each, map, concat, last, get } from 'lodash'
|
||||
import { parseStatus, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
||||
|
||||
/* eslint-env browser */
|
||||
|
@ -81,6 +81,11 @@ const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
|
|||
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
|
||||
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||
const PLEROMA_CHATS_URL = `/api/v1/pleroma/chats`
|
||||
const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
|
||||
const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
|
||||
const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
|
||||
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
|
||||
|
||||
const oldfetch = window.fetch
|
||||
|
||||
|
@ -117,13 +122,18 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
|
|||
}
|
||||
return fetch(url, options)
|
||||
.then((response) => {
|
||||
return new Promise((resolve, reject) => response.json()
|
||||
.then((json) => {
|
||||
if (!response.ok) {
|
||||
return reject(new StatusCodeError(response.status, json, { url, options }, response))
|
||||
}
|
||||
return resolve(json)
|
||||
}))
|
||||
return new Promise((resolve, reject) => {
|
||||
response.json()
|
||||
.then((json) => {
|
||||
if (!response.ok) {
|
||||
return reject(new StatusCodeError(response.status, json, { url, options }, response))
|
||||
}
|
||||
return resolve(json)
|
||||
})
|
||||
.catch((error) => {
|
||||
return reject(new StatusCodeError(response.status, error.message, { url, options }, response))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1067,6 +1077,10 @@ const MASTODON_STREAMING_EVENTS = new Set([
|
|||
'filters_changed'
|
||||
])
|
||||
|
||||
const PLEROMA_STREAMING_EVENTS = new Set([
|
||||
'pleroma:chat_update'
|
||||
])
|
||||
|
||||
// A thin wrapper around WebSocket API that allows adding a pre-processor to it
|
||||
// Uses EventTarget and a CustomEvent to proxy events
|
||||
export const ProcessedWS = ({
|
||||
|
@ -1123,7 +1137,7 @@ export const handleMastoWS = (wsEvent) => {
|
|||
if (!data) return
|
||||
const parsedEvent = JSON.parse(data)
|
||||
const { event, payload } = parsedEvent
|
||||
if (MASTODON_STREAMING_EVENTS.has(event)) {
|
||||
if (MASTODON_STREAMING_EVENTS.has(event) || PLEROMA_STREAMING_EVENTS.has(event)) {
|
||||
// MastoBE and PleromaBE both send payload for delete as a PLAIN string
|
||||
if (event === 'delete') {
|
||||
return { event, id: payload }
|
||||
|
@ -1133,6 +1147,8 @@ export const handleMastoWS = (wsEvent) => {
|
|||
return { event, status: parseStatus(data) }
|
||||
} else if (event === 'notification') {
|
||||
return { event, notification: parseNotification(data) }
|
||||
} else if (event === 'pleroma:chat_update') {
|
||||
return { event, chatUpdate: parseChat(data) }
|
||||
}
|
||||
} else {
|
||||
console.warn('Unknown event', wsEvent)
|
||||
|
@ -1140,6 +1156,81 @@ export const handleMastoWS = (wsEvent) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const WSConnectionStatus = Object.freeze({
|
||||
'JOINED': 1,
|
||||
'CLOSED': 2,
|
||||
'ERROR': 3
|
||||
})
|
||||
|
||||
const chats = ({ credentials }) => {
|
||||
return fetch(PLEROMA_CHATS_URL, { headers: authHeaders(credentials) })
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
return { chats: data.map(parseChat).filter(c => c) }
|
||||
})
|
||||
}
|
||||
|
||||
const getOrCreateChat = ({ accountId, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: PLEROMA_CHAT_URL(accountId),
|
||||
method: 'POST',
|
||||
credentials
|
||||
})
|
||||
}
|
||||
|
||||
const chatMessages = ({ id, credentials, maxId, sinceId, limit = 20 }) => {
|
||||
let url = PLEROMA_CHAT_MESSAGES_URL(id)
|
||||
const args = [
|
||||
maxId && `max_id=${maxId}`,
|
||||
sinceId && `since_id=${sinceId}`,
|
||||
limit && `limit=${limit}`
|
||||
].filter(_ => _).join('&')
|
||||
|
||||
url = url + (args ? '?' + args : '')
|
||||
|
||||
return promisedRequest({
|
||||
url,
|
||||
method: 'GET',
|
||||
credentials
|
||||
})
|
||||
}
|
||||
|
||||
const sendChatMessage = ({ id, content, mediaId = null, credentials }) => {
|
||||
const payload = {
|
||||
'content': content
|
||||
}
|
||||
|
||||
if (mediaId) {
|
||||
payload['media_id'] = mediaId
|
||||
}
|
||||
|
||||
return promisedRequest({
|
||||
url: PLEROMA_CHAT_MESSAGES_URL(id),
|
||||
method: 'POST',
|
||||
payload: payload,
|
||||
credentials
|
||||
})
|
||||
}
|
||||
|
||||
const readChat = ({ id, lastReadId, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: PLEROMA_CHAT_READ_URL(id),
|
||||
method: 'POST',
|
||||
payload: {
|
||||
'last_read_id': lastReadId
|
||||
},
|
||||
credentials
|
||||
})
|
||||
}
|
||||
|
||||
const deleteChatMessage = ({ chatId, messageId, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: PLEROMA_DELETE_CHAT_MESSAGE_URL(chatId, messageId),
|
||||
method: 'DELETE',
|
||||
credentials
|
||||
})
|
||||
}
|
||||
|
||||
const apiService = {
|
||||
verifyCredentials,
|
||||
fetchTimeline,
|
||||
|
@ -1218,7 +1309,13 @@ const apiService = {
|
|||
fetchKnownDomains,
|
||||
fetchDomainMutes,
|
||||
muteDomain,
|
||||
unmuteDomain
|
||||
unmuteDomain,
|
||||
chats,
|
||||
getOrCreateChat,
|
||||
chatMessages,
|
||||
sendChatMessage,
|
||||
readChat,
|
||||
deleteChatMessage
|
||||
}
|
||||
|
||||
export default apiService
|
||||
|
|
150
src/services/chat_service/chat_service.js
Normal file
150
src/services/chat_service/chat_service.js
Normal file
|
@ -0,0 +1,150 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
const empty = (chatId) => {
|
||||
return {
|
||||
idIndex: {},
|
||||
messages: [],
|
||||
newMessageCount: 0,
|
||||
lastSeenTimestamp: 0,
|
||||
chatId: chatId,
|
||||
minId: undefined,
|
||||
lastMessage: undefined
|
||||
}
|
||||
}
|
||||
|
||||
const clear = (storage) => {
|
||||
storage.idIndex = {}
|
||||
storage.messages.splice(0, storage.messages.length)
|
||||
storage.newMessageCount = 0
|
||||
storage.lastSeenTimestamp = 0
|
||||
storage.minId = undefined
|
||||
storage.lastMessage = undefined
|
||||
}
|
||||
|
||||
const deleteMessage = (storage, messageId) => {
|
||||
if (!storage) { return }
|
||||
storage.messages = storage.messages.filter(m => m.id !== messageId)
|
||||
delete storage.idIndex[messageId]
|
||||
|
||||
if (storage.lastMessage && (storage.lastMessage.id === messageId)) {
|
||||
storage.lastMessage = _.maxBy(storage.messages, 'id')
|
||||
}
|
||||
|
||||
if (storage.minId === messageId) {
|
||||
storage.minId = _.minBy(storage.messages, 'id')
|
||||
}
|
||||
}
|
||||
|
||||
const add = (storage, { messages: newMessages }) => {
|
||||
if (!storage) { return }
|
||||
for (let i = 0; i < newMessages.length; i++) {
|
||||
const message = newMessages[i]
|
||||
|
||||
// sanity check
|
||||
if (message.chat_id !== storage.chatId) { return }
|
||||
|
||||
if (!storage.minId || message.id < storage.minId) {
|
||||
storage.minId = message.id
|
||||
}
|
||||
|
||||
if (!storage.lastMessage || message.id > storage.lastMessage.id) {
|
||||
storage.lastMessage = message
|
||||
}
|
||||
|
||||
if (!storage.idIndex[message.id]) {
|
||||
if (storage.lastSeenTimestamp < message.created_at) {
|
||||
storage.newMessageCount++
|
||||
}
|
||||
storage.messages.push(message)
|
||||
storage.idIndex[message.id] = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resetNewMessageCount = (storage) => {
|
||||
if (!storage) { return }
|
||||
storage.newMessageCount = 0
|
||||
storage.lastSeenTimestamp = new Date()
|
||||
}
|
||||
|
||||
// Inserts date separators and marks the head and tail if it's the chain of messages made by the same user
|
||||
const getView = (storage) => {
|
||||
if (!storage) { return [] }
|
||||
|
||||
const result = []
|
||||
const messages = _.sortBy(storage.messages, ['id', 'desc'])
|
||||
const firstMessages = messages[0]
|
||||
let prev = messages[messages.length - 1]
|
||||
let currentMessageChainId
|
||||
|
||||
if (firstMessages) {
|
||||
const date = new Date(firstMessages.created_at)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
result.push({
|
||||
type: 'date',
|
||||
date,
|
||||
id: date.getTime().toString()
|
||||
})
|
||||
}
|
||||
|
||||
let afterDate = false
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i]
|
||||
const nextMessage = messages[i + 1]
|
||||
|
||||
const date = new Date(message.created_at)
|
||||
date.setHours(0, 0, 0, 0)
|
||||
|
||||
// insert date separator and start a new message chain
|
||||
if (prev && prev.date < date) {
|
||||
result.push({
|
||||
type: 'date',
|
||||
date,
|
||||
id: date.getTime().toString()
|
||||
})
|
||||
|
||||
prev['isTail'] = true
|
||||
currentMessageChainId = undefined
|
||||
afterDate = true
|
||||
}
|
||||
|
||||
const object = {
|
||||
type: 'message',
|
||||
data: message,
|
||||
date,
|
||||
id: message.id,
|
||||
messageChainId: currentMessageChainId
|
||||
}
|
||||
|
||||
// end a message chian
|
||||
if ((nextMessage && nextMessage.account_id) !== message.account_id) {
|
||||
object['isTail'] = true
|
||||
currentMessageChainId = undefined
|
||||
}
|
||||
|
||||
// start a new message chain
|
||||
if ((prev && prev.data && prev.data.account_id) !== message.account_id || afterDate) {
|
||||
currentMessageChainId = _.uniqueId()
|
||||
object['isHead'] = true
|
||||
object['messageChainId'] = currentMessageChainId
|
||||
}
|
||||
|
||||
result.push(object)
|
||||
prev = object
|
||||
afterDate = false
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const ChatService = {
|
||||
add,
|
||||
empty,
|
||||
getView,
|
||||
deleteMessage,
|
||||
resetNewMessageCount,
|
||||
clear
|
||||
}
|
||||
|
||||
export default ChatService
|
|
@ -183,6 +183,7 @@ export const parseUser = (data) => {
|
|||
output.deactivated = data.pleroma.deactivated
|
||||
|
||||
output.notification_settings = data.pleroma.notification_settings
|
||||
output.unread_chat_count = data.pleroma.unread_chat_count
|
||||
}
|
||||
|
||||
output.tags = output.tags || []
|
||||
|
@ -372,7 +373,7 @@ export const parseNotification = (data) => {
|
|||
? parseStatus(data.notice.favorited_status)
|
||||
: parsedNotice
|
||||
output.action = parsedNotice
|
||||
output.from_profile = parseUser(data.from_profile)
|
||||
output.from_profile = output.type === 'pleroma:chat_mention' ? parseUser(data.account) : parseUser(data.from_profile)
|
||||
}
|
||||
|
||||
output.created_at = new Date(data.created_at)
|
||||
|
@ -398,3 +399,34 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
|
|||
minId: flakeId ? minId : parseInt(minId, 10)
|
||||
}
|
||||
}
|
||||
|
||||
export const parseChat = (chat) => {
|
||||
const output = {}
|
||||
output.id = chat.id
|
||||
output.account = parseUser(chat.account)
|
||||
output.unread = chat.unread
|
||||
output.lastMessage = parseChatMessage(chat.last_message)
|
||||
output.updated_at = new Date(chat.updated_at)
|
||||
return output
|
||||
}
|
||||
|
||||
export const parseChatMessage = (message) => {
|
||||
if (!message) { return }
|
||||
if (message.isNormalized) { return message }
|
||||
const output = message
|
||||
output.id = message.id
|
||||
output.created_at = new Date(message.created_at)
|
||||
output.chat_id = message.chat_id
|
||||
if (message.content) {
|
||||
output.content = addEmojis(message.content, message.emojis)
|
||||
} else {
|
||||
output.content = ''
|
||||
}
|
||||
if (message.attachment) {
|
||||
output.attachments = [parseAttachment(message.attachment)]
|
||||
} else {
|
||||
output.attachments = []
|
||||
}
|
||||
output.isNormalized = true
|
||||
return output
|
||||
}
|
||||
|
|
|
@ -106,7 +106,8 @@ export const generateRadii = (input) => {
|
|||
avatar: 5,
|
||||
avatarAlt: 50,
|
||||
tooltip: 2,
|
||||
attachment: 5
|
||||
attachment: 5,
|
||||
chatMessage: inputRadii.panel
|
||||
})
|
||||
|
||||
return {
|
||||
|
|
|
@ -23,7 +23,9 @@ export const LAYERS = {
|
|||
inputTopBar: 'topBar',
|
||||
alert: 'bg',
|
||||
alertPanel: 'panel',
|
||||
poll: 'bg'
|
||||
poll: 'bg',
|
||||
chatBg: 'underlay',
|
||||
chatMessage: 'chatBg'
|
||||
}
|
||||
|
||||
/* By default opacity slots have 1 as default opacity
|
||||
|
@ -667,5 +669,54 @@ export const SLOT_INHERITANCE = {
|
|||
layer: 'badge',
|
||||
variant: 'badgeNotification',
|
||||
textColor: 'bw'
|
||||
},
|
||||
|
||||
chatBg: {
|
||||
depends: ['bg']
|
||||
},
|
||||
|
||||
chatMessage: {
|
||||
depends: ['chatBg']
|
||||
},
|
||||
|
||||
chatMessageIncomingBg: {
|
||||
depends: ['chatMessage'],
|
||||
layer: 'chatMessage'
|
||||
},
|
||||
|
||||
chatMessageIncomingText: {
|
||||
depends: ['text'],
|
||||
layer: 'text'
|
||||
},
|
||||
|
||||
chatMessageIncomingLink: {
|
||||
depends: ['link'],
|
||||
layer: 'link'
|
||||
},
|
||||
|
||||
chatMessageIncomingBorder: {
|
||||
depends: ['border'],
|
||||
opacity: 'border',
|
||||
color: (mod, border) => brightness(2 * mod, border).rgb
|
||||
},
|
||||
|
||||
chatMessageOutgoingBg: {
|
||||
depends: ['chatMessage'],
|
||||
color: (mod, chatMessage) => brightness(5 * mod, chatMessage).rgb
|
||||
},
|
||||
|
||||
chatMessageOutgoingText: {
|
||||
depends: ['text'],
|
||||
layer: 'text'
|
||||
},
|
||||
|
||||
chatMessageOutgoingLink: {
|
||||
depends: ['link'],
|
||||
layer: 'link'
|
||||
},
|
||||
|
||||
chatMessageOutgoingBorder: {
|
||||
depends: ['chatMessage'],
|
||||
opacity: 'chatMessage'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,3 +3,8 @@ export const windowWidth = () =>
|
|||
window.innerWidth ||
|
||||
document.documentElement.clientWidth ||
|
||||
document.body.clientWidth
|
||||
|
||||
export const windowHeight = () =>
|
||||
window.innerHeight ||
|
||||
document.documentElement.clientHeight ||
|
||||
document.body.clientHeight
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue