+
({
+ data: [],
+ pagination: { maxId: undefined, minId: undefined },
+ idStore: {}
+})
+
+const defaultState = {
+ chatList: emptyChatList(),
+ openedChats: {},
+ openedChatMessageServices: {},
+ fetcher: undefined,
+ chatFocused: false,
+ currentChatId: null
+}
+
+const chats = {
+ state: { ...defaultState },
+ getters: {
+ currentChat: state => state.openedChats[state.currentChatId],
+ currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId]
+ },
+ actions: {
+ // Chat list
+ startFetchingChats ({ dispatch }) {
+ setInterval(() => {
+ dispatch('fetchChats', { reset: true })
+ // dispatch('refreshCurrentUser')
+ }, 5000)
+ },
+ fetchChats ({ dispatch, rootState, commit }, params = {}) {
+ const pagination = rootState.chats.chatList.pagination
+ const opts = { maxId: params.reset ? undefined : pagination.maxId }
+
+ return rootState.api.backendInteractor.chats(opts)
+ .then(({ chats, pagination }) => {
+ dispatch('addNewChats', { chats, pagination })
+ return chats
+ })
+ },
+ addNewChats ({ rootState, commit, dispatch, rootGetters }, { chats, pagination }) {
+ commit('addNewChats', { dispatch, chats, pagination, rootGetters })
+ },
+ updateChatByAccountId: debounce(({ rootState, commit, dispatch, rootGetters }, { accountId }) => {
+ rootState.api.backendInteractor.getOrCreateChat({ accountId }).then(chat => {
+ commit('updateChat', { dispatch, rootGetters, chat: parseChat(chat) })
+ })
+ }, 100),
+
+ // Opened Chats
+ startFetchingCurrentChat ({ commit, dispatch }, { fetcher }) {
+ dispatch('setCurrentChatFetcher', { fetcher })
+ },
+ setCurrentChatFetcher ({ rootState, commit }, { fetcher }) {
+ commit('setCurrentChatFetcher', { fetcher })
+ },
+ addOpenedChat ({ commit, dispatch }, { chat }) {
+ commit('addOpenedChat', { dispatch, chat: parseChat(chat) })
+ dispatch('addNewUsers', [chat.account])
+ },
+ addChatMessages ({ commit }, value) {
+ commit('addChatMessages', value)
+ },
+ setChatFocused ({ commit }, value) {
+ commit('setChatFocused', value)
+ },
+ resetChatNewMessageCount ({ commit }, value) {
+ commit('resetChatNewMessageCount', value)
+ },
+ removeFromCurrentChatStatuses ({ commit }, { id }) {
+ commit('removeFromCurrentChatStatuses', id)
+ },
+ clearCurrentChat ({ rootState, commit, dispatch }, value) {
+ commit('setCurrentChatId', { chatId: undefined })
+ commit('setCurrentChatFetcher', { fetcher: undefined })
+ },
+ readChat ({ rootState, dispatch }, { id }) {
+ dispatch('resetChatNewMessageCount')
+ rootState.api.backendInteractor.readChat({ id }).then(() => {
+ // dispatch('refreshCurrentUser')
+ })
+ }
+ },
+ mutations: {
+ setCurrentChatFetcher (state, { fetcher }) {
+ let prevFetcher = state.fetcher
+ if (prevFetcher) {
+ clearInterval(prevFetcher)
+ }
+ state.fetcher = fetcher && fetcher()
+ },
+ addOpenedChat (state, { _dispatch, chat }) {
+ state.currentChatId = chat.id
+ state.openedChats[chat.id] = chat
+
+ if (!state.openedChatMessageServices[chat.id]) {
+ state.openedChatMessageServices[chat.id] = chatService.empty(chat.id)
+ }
+ },
+ setCurrentChatId (state, { chatId }) {
+ state.currentChatId = chatId
+ },
+ addNewChats (state, { _dispatch, chats, pagination, _rootGetters }) {
+ if (chats.length > 0) {
+ state.chatList.pagination = { maxId: last(chats).id }
+ }
+ chats.forEach((conversation) => {
+ // This is to prevent duplicate conversations being added
+ // (right now, backend can return the same conversation on different pages)
+ if (!state.chatList.idStore[conversation.id]) {
+ state.chatList.data.push(conversation)
+ state.chatList.idStore[conversation.id] = conversation
+ } else {
+ const chat = find(state.chatList.data, { id: conversation.id })
+ chat.last_status = conversation.last_status
+ chat.unread = conversation.unread
+ state.chatList.idStore[conversation.id] = conversation
+ }
+ })
+ },
+ updateChat (state, { _dispatch, chat: updatedChat, _rootGetters }) {
+ let chat = find(state.chatList.data, { id: updatedChat.id })
+ if (chat) {
+ state.chatList.data = state.chatList.data.filter(d => {
+ return d.id !== updatedChat.id
+ })
+ }
+ state.chatList.data.unshift(updatedChat)
+ state.chatList.idStore[updatedChat.id] = updatedChat
+ },
+ deleteChat (state, { _dispatch, id, _rootGetters }) {
+ state.chats.data = state.chats.data.filter(conversation =>
+ conversation.last_status.id !== id
+ )
+ state.chats.idStore = omitBy(state.chats.idStore, conversation => conversation.last_status.id === id)
+ },
+ resetChats (state, { _dispatch }) {
+ state.chats.data = []
+ state.chats.idStore = {}
+ },
+ setChatsLoading (state, { value }) {
+ state.chats.loading = value
+ },
+ addChatMessages (state, { chatId, messages }) {
+ const chatMessageService = state.openedChatMessageServices[chatId]
+ if (chatMessageService) {
+ chatService.add(chatMessageService, { messages: messages.map(parseChatMessage) })
+ }
+ },
+ resetChatNewMessageCount (state, _value) {
+ const chatMessageService = state.openedChatMessageServices[state.currentChatId]
+ chatService.resetNewMessageCount(chatMessageService)
+ },
+ setChatFocused (state, value) {
+ state.chatFocused = value
+ }
+ }
+}
+
+export default chats
diff --git a/src/modules/config.js b/src/modules/config.js
index 8f4638f5..024f257b 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -35,7 +35,8 @@ export const defaultState = {
repeats: true,
moves: true,
emojiReactions: false,
- followRequest: true
+ followRequest: true,
+ chatMention: true
},
webPushNotifications: false,
muteWords: [],
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index cd8c1dba..ed78cffd 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -83,7 +83,8 @@ const visibleNotificationTypes = (rootState) => {
rootState.config.notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.moves && 'move',
- rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions'
+ rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions',
+ rootState.config.notificationVisibility.chatMention && 'pleroma:chat_mention'
].filter(_ => _)
}
@@ -331,6 +332,11 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
dispatch('fetchEmojiReactionsBy', notification.status.id)
}
+ if (notification.type === 'pleroma:chat_mention') {
+ dispatch('addChatMessages', { chatId: notification.chatMessage.chat_id, messages: [notification.chatMessage] })
+ dispatch('updateChatByAccountId', { accountId: notification.from_profile.id })
+ }
+
// Only add a new notification if we don't have one for the same action
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
state.notifications.maxId = notification.id > state.notifications.maxId
@@ -373,6 +379,8 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
} else if (isStatusNotification(notification.type)) {
notifObj.body = notification.status.text
+ } else if (notification.type === 'pleroma:chat_mention') {
+ notifObj.body = notification.chatMessage.content
}
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
@@ -489,7 +497,7 @@ export const mutations = {
},
setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
- newStatus.deleted = true
+ if (newStatus) newStatus.deleted = true
},
setManyDeleted (state, condition) {
Object.values(state.allStatusesObject).forEach(status => {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 9c7530a2..04c8f214 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,5 +1,5 @@
import { each, map, concat, last, get } from 'lodash'
-import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
+import { parseStatus, parseUser, parseNotification, parseAttachment, parseChat } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch'
import { RegistrationError, StatusCodeError } from '../errors/errors'
@@ -78,6 +78,10 @@ const MASTODON_STREAMING = '/api/v1/streaming'
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 oldfetch = window.fetch
@@ -1119,6 +1123,60 @@ export const handleMastoWS = (wsEvent) => {
}
}
+const chats = ({ maxId, sinceId, limit = 20, recipients = [], credentials }) => {
+ let url = PLEROMA_CHATS_URL
+ const recipientIds = recipients.map(r => `recipients[]=${r}`).join('&')
+ const args = [
+ maxId && `max_id=${maxId}`,
+ sinceId && `since_id=${sinceId}`,
+ limit && `limit=${limit}`,
+ recipientIds && `${recipientIds}`
+ ].filter(_ => _).join('&')
+
+ let pagination = {}
+ url = url + (args ? '?' + args : '')
+ return fetch(url, { headers: authHeaders(credentials) })
+ .then((data) => data.json())
+ .then((data) => {
+ return { chats: data.map(parseChat), pagination }
+ })
+}
+
+const getOrCreateChat = ({ accountId, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_URL(accountId),
+ method: 'POST',
+ credentials
+ })
+}
+
+const chatMessages = ({ id, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_MESSAGES_URL(id),
+ method: 'GET',
+ credentials
+ })
+}
+
+const postChatMessage = ({ id, content, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_MESSAGES_URL(id),
+ method: 'POST',
+ payload: {
+ 'content': content
+ },
+ credentials
+ })
+}
+
+const readChat = ({ id, credentials }) => {
+ return promisedRequest({
+ url: PLEROMA_CHAT_READ_URL(id),
+ method: 'POST',
+ credentials
+ })
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
@@ -1195,7 +1253,12 @@ const apiService = {
searchUsers,
fetchDomainMutes,
muteDomain,
- unmuteDomain
+ unmuteDomain,
+ chats,
+ getOrCreateChat,
+ chatMessages,
+ postChatMessage,
+ readChat
}
export default apiService
diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js
new file mode 100644
index 00000000..b570a065
--- /dev/null
+++ b/src/services/chat_service/chat_service.js
@@ -0,0 +1,100 @@
+import _ from 'lodash'
+
+const empty = (chatId) => {
+ return {
+ idIndex: {},
+ messages: [],
+ newMessageCount: 0,
+ lastSeenTimestamp: 0,
+ chatId: chatId
+ }
+}
+
+const add = (storage, { messages: newMessages }) => {
+ if (!storage) { return }
+ for (let i = 0; i < newMessages.length; i++) {
+ let message = newMessages[i]
+
+ // sanity check
+ if (message.chat_id !== storage.chatId) { return }
+
+ 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 sequence of messages made by the same user
+const getView = (storage) => {
+ if (!storage) { return [] }
+ let messages = _.sortBy(storage.messages, 'id')
+
+ let res = []
+
+ let prev = messages[messages.length - 1]
+ let currentSequenceId
+
+ let firstMessages = messages[0]
+
+ if (firstMessages) {
+ let date = new Date(firstMessages.created_at)
+ date.setHours(0, 0, 0, 0)
+ res.push({ type: 'date', date: date, id: date.getTime().toString() })
+ }
+
+ let afterDate = false
+
+ for (let i = 0; i < messages.length; i++) {
+ let message = messages[i]
+ let nextMessage = messages[i + 1]
+
+ let date = new Date(message.created_at)
+ date.setHours(0, 0, 0, 0)
+
+ // insert date separator and start a new sequence
+ if (prev && prev.date < date) {
+ res.push({ type: 'date', date: date, id: date.getTime().toString() })
+ prev['isTail'] = true
+ currentSequenceId = undefined
+ afterDate = true
+ }
+
+ let object = { type: 'message', data: message, date: date, id: message.id, sequenceId: currentSequenceId }
+
+ // end a message sequence
+ if ((nextMessage && nextMessage.account_id) !== message.account_id) {
+ object['isTail'] = true
+ currentSequenceId = undefined
+ }
+ // start a new message sequence
+ if ((prev && prev.data && prev.data.account_id) !== message.account_id || afterDate) {
+ currentSequenceId = _.uniqueId()
+ object['isHead'] = true
+ object['sequenceId'] = currentSequenceId
+ }
+ res.push(object)
+ prev = object
+ afterDate = false
+ }
+
+ return res
+}
+
+const ChatService = {
+ add,
+ empty,
+ getView,
+ resetNewMessageCount
+}
+
+export default ChatService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 6dac7c15..20e7a616 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -342,6 +342,7 @@ 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.chatMessage = output.type === 'pleroma:chat_mention' ? parseChatMessage(data.chat_message) : null
output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move'
? null
@@ -356,7 +357,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)
@@ -369,3 +370,19 @@ const isNsfw = (status) => {
const nsfwRegex = /#nsfw/i
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
}
+
+export const parseChat = (chat) => {
+ let output = chat
+ output.id = parseInt(chat.id, 10)
+ output.account = parseUser(chat.account)
+ output.unread = chat.unread
+ output.lastMessage = undefined
+ return output
+}
+
+export const parseChatMessage = (message) => {
+ let output = message
+ output.created_at = new Date(message.created_at)
+ output.chat_id = parseInt(message.chat_id, 10)
+ return output
+}
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index eb479227..d73f747c 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -9,7 +9,8 @@ export const visibleTypes = store => ([
store.state.config.notificationVisibility.follows && 'follow',
store.state.config.notificationVisibility.followRequest && 'follow_request',
store.state.config.notificationVisibility.moves && 'move',
- store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
+ store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
+ store.state.config.notificationVisibility.chatMention && 'pleroma:chat_mention'
].filter(_ => _))
const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index fbdcf562..07425abd 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -106,7 +106,8 @@ export const generateRadii = (input) => {
avatar: 5,
avatarAlt: 50,
tooltip: 2,
- attachment: 5
+ attachment: 5,
+ chatMessage: inputRadii.panel
})
return {
diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js
index 0c1fe543..7f0f6078 100644
--- a/src/services/theme_data/pleromafe.js
+++ b/src/services/theme_data/pleromafe.js
@@ -627,5 +627,51 @@ export const SLOT_INHERITANCE = {
layer: 'badge',
variant: 'badgeNotification',
textColor: 'bw'
+ },
+
+ chatBg: {
+ depends: ['bg'],
+ layer: 'bg'
+ },
+
+ chatMessageIncomingBg: {
+ depends: ['bg'],
+ layer: 'bg'
+ },
+
+ chatMessageIncomingText: {
+ depends: ['text'],
+ layer: 'text'
+ },
+
+ chatMessageIncomingLink: {
+ depends: ['link'],
+ layer: 'link'
+ },
+
+ chatMessageIncomingBorder: {
+ depends: ['fg'],
+ opacity: 'border',
+ color: (mod, fg) => brightness(2 * mod, fg).rgb
+ },
+
+ chatMessageOutgoingBg: {
+ depends: ['bg'],
+ color: (mod, fg) => brightness(5 * mod, fg).rgb
+ },
+
+ chatMessageOutgoingText: {
+ depends: ['text'],
+ layer: 'text'
+ },
+
+ chatMessageOutgoingLink: {
+ depends: ['link'],
+ layer: 'link'
+ },
+
+ chatMessageOutgoingBorder: {
+ depends: ['bg'],
+ opacity: 'bg'
}
}
diff --git a/static/fontello.json b/static/fontello.json
old mode 100755
new mode 100644
diff --git a/static/themes/breezy-dark.json b/static/themes/breezy-dark.json
index 76b962c5..6fb9d09c 100644
--- a/static/themes/breezy-dark.json
+++ b/static/themes/breezy-dark.json
@@ -115,7 +115,8 @@
"cOrange": "#f67400",
"btnPressed": "--accent",
"selectedMenu": "--accent",
- "selectedMenuPopover": "--accent"
+ "selectedMenuPopover": "--accent",
+ "chatMessageIncomingBorder": "#3d4349"
},
"radii": {
"btn": "2",
diff --git a/static/themes/redmond-xx-se.json b/static/themes/redmond-xx-se.json
index 7a4a29da..22d0d184 100644
--- a/static/themes/redmond-xx-se.json
+++ b/static/themes/redmond-xx-se.json
@@ -286,7 +286,8 @@
"cGreen": "#008000",
"cOrange": "#808000",
"highlight": "--accent",
- "selectedPost": "--bg,-10"
+ "selectedPost": "--bg,-10",
+ "chatFg": "#808080"
},
"radii": {
"btn": "0",
diff --git a/static/themes/redmond-xx.json b/static/themes/redmond-xx.json
index ff95b1e0..33544cbe 100644
--- a/static/themes/redmond-xx.json
+++ b/static/themes/redmond-xx.json
@@ -277,7 +277,8 @@
"cGreen": "#008000",
"cOrange": "#808000",
"highlight": "--accent",
- "selectedPost": "--bg,-10"
+ "selectedPost": "--bg,-10",
+ "chatMessageIncomingBorder": "#808080"
},
"radii": {
"btn": "0",
diff --git a/static/themes/redmond-xxi.json b/static/themes/redmond-xxi.json
index f788bdb8..0bff9fe1 100644
--- a/static/themes/redmond-xxi.json
+++ b/static/themes/redmond-xxi.json
@@ -259,7 +259,8 @@
"cGreen": "#669966",
"cOrange": "#cc6633",
"highlight": "--accent",
- "selectedPost": "--bg,-10"
+ "selectedPost": "--bg,-10",
+ "chatFg": "#808080"
},
"radii": {
"btn": "0",