Add Chats

This commit is contained in:
eugenijm 2020-05-07 16:10:53 +03:00
parent a0ddcbdf5b
commit aa2cf51c05
69 changed files with 2794 additions and 161 deletions

View file

@ -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

View 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

View file

@ -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
}

View file

@ -106,7 +106,8 @@ export const generateRadii = (input) => {
avatar: 5,
avatarAlt: 50,
tooltip: 2,
attachment: 5
attachment: 5,
chatMessage: inputRadii.panel
})
return {

View file

@ -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'
}
}

View file

@ -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