feat(conversations): add message creation limit lock
This commit is contained in:
@@ -289,12 +289,14 @@ const actions = {
|
||||
|
||||
sendMessageWithData: async ({ commit }, pendingMessage) => {
|
||||
const { conversation_id: conversationId, id } = pendingMessage;
|
||||
const shouldRetryMessage =
|
||||
hasMessageFailedWithExternalError(pendingMessage);
|
||||
try {
|
||||
commit(types.ADD_MESSAGE, {
|
||||
...pendingMessage,
|
||||
status: MESSAGE_STATUS.PROGRESS,
|
||||
});
|
||||
const response = hasMessageFailedWithExternalError(pendingMessage)
|
||||
const response = shouldRetryMessage
|
||||
? await MessageApi.retry(conversationId, id)
|
||||
: await MessageApi.create(pendingMessage);
|
||||
commit(types.ADD_MESSAGE, {
|
||||
@@ -309,6 +311,31 @@ const actions = {
|
||||
const errorMessage = error.response
|
||||
? error.response.data.error
|
||||
: undefined;
|
||||
const errorResponse = error.response?.data || {};
|
||||
if (errorResponse.message_creation_locked) {
|
||||
if (shouldRetryMessage) {
|
||||
commit(types.ADD_MESSAGE, {
|
||||
...pendingMessage,
|
||||
meta: {
|
||||
...(pendingMessage.meta || {}),
|
||||
error: errorResponse.error,
|
||||
},
|
||||
status: MESSAGE_STATUS.FAILED,
|
||||
});
|
||||
} else {
|
||||
commit(types.DELETE_MESSAGE, pendingMessage);
|
||||
}
|
||||
commit(types.SET_CONVERSATION_MESSAGE_CREATION_LOCK, {
|
||||
conversationId,
|
||||
message_limit: errorResponse.message_limit,
|
||||
message_limit_reached: errorResponse.message_limit_reached,
|
||||
message_creation_locked: errorResponse.message_creation_locked,
|
||||
message_creation_lock_reason:
|
||||
errorResponse.message_creation_lock_reason,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
commit(types.ADD_MESSAGE, {
|
||||
...pendingMessage,
|
||||
meta: {
|
||||
@@ -322,6 +349,12 @@ const actions = {
|
||||
|
||||
addMessage({ commit, rootGetters }, message) {
|
||||
commit(types.ADD_MESSAGE, message);
|
||||
if (message.conversation) {
|
||||
commit(types.SET_CONVERSATION_MESSAGE_CREATION_LOCK, {
|
||||
conversationId: message.conversation_id,
|
||||
...message.conversation,
|
||||
});
|
||||
}
|
||||
if (message.message_type === MESSAGE_TYPE.INCOMING) {
|
||||
commit(types.SET_CONVERSATION_CAN_REPLY, {
|
||||
conversationId: message.conversation_id,
|
||||
|
||||
@@ -29,6 +29,24 @@ const getConversationById = _state => conversationId => {
|
||||
return _state.allConversations.find(c => c.id === conversationId);
|
||||
};
|
||||
|
||||
const MESSAGE_CREATION_LOCK_FIELDS = [
|
||||
'message_limit',
|
||||
'message_limit_reached',
|
||||
'message_creation_locked',
|
||||
'message_creation_lock_reason',
|
||||
];
|
||||
|
||||
const updateConversationMessageCreationLock = (
|
||||
conversation,
|
||||
lockState = {}
|
||||
) => {
|
||||
MESSAGE_CREATION_LOCK_FIELDS.forEach(field => {
|
||||
if (Object.prototype.hasOwnProperty.call(lockState, field)) {
|
||||
conversation[field] = lockState[field];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// mutations
|
||||
export const mutations = {
|
||||
[types.SET_ALL_CONVERSATION](_state, conversationList) {
|
||||
@@ -205,6 +223,20 @@ export const mutations = {
|
||||
});
|
||||
},
|
||||
|
||||
[types.DELETE_MESSAGE]({ allConversations }, message) {
|
||||
const { conversation_id: conversationId } = message;
|
||||
const [chat] = getSelectedChatConversation({
|
||||
allConversations,
|
||||
selectedChatId: conversationId,
|
||||
});
|
||||
if (!chat) return;
|
||||
|
||||
const pendingMessageIndex = findPendingMessageIndex(chat, message);
|
||||
if (pendingMessageIndex !== -1) {
|
||||
chat.messages.splice(pendingMessageIndex, 1);
|
||||
}
|
||||
},
|
||||
|
||||
[types.ADD_MESSAGE]({ allConversations, selectedChatId }, message) {
|
||||
const { conversation_id: conversationId } = message;
|
||||
const [chat] = getSelectedChatConversation({
|
||||
@@ -216,17 +248,29 @@ export const mutations = {
|
||||
const pendingMessageIndex = findPendingMessageIndex(chat, message);
|
||||
if (pendingMessageIndex !== -1) {
|
||||
chat.messages[pendingMessageIndex] = message;
|
||||
updateConversationMessageCreationLock(chat, message.conversation);
|
||||
} else {
|
||||
chat.messages.push(message);
|
||||
chat.timestamp = message.created_at;
|
||||
const { conversation: { unread_count: unreadCount = 0 } = {} } = message;
|
||||
chat.unread_count = unreadCount;
|
||||
updateConversationMessageCreationLock(chat, message.conversation);
|
||||
if (selectedChatId === conversationId) {
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
[types.SET_CONVERSATION_MESSAGE_CREATION_LOCK](
|
||||
_state,
|
||||
{ conversationId, ...lockState }
|
||||
) {
|
||||
const chat = getConversationById(_state)(conversationId);
|
||||
if (chat) {
|
||||
updateConversationMessageCreationLock(chat, lockState);
|
||||
}
|
||||
},
|
||||
|
||||
[types.ADD_CONVERSATION](_state, conversation) {
|
||||
const exists = _state.allConversations.some(c => c.id === conversation.id);
|
||||
if (!exists) {
|
||||
|
||||
@@ -288,6 +288,138 @@ describe('#actions', () => {
|
||||
actions.addMessage({ commit }, message);
|
||||
expect(commit.mock.calls).toEqual([[types.ADD_MESSAGE, message]]);
|
||||
});
|
||||
|
||||
it('syncs message creation lock metadata from message payload', () => {
|
||||
const message = {
|
||||
id: 1,
|
||||
message_type: 1,
|
||||
conversation_id: 1,
|
||||
conversation: {
|
||||
message_limit: 10000,
|
||||
message_limit_reached: true,
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'message_limit',
|
||||
},
|
||||
};
|
||||
actions.addMessage({ commit }, message);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.ADD_MESSAGE, message],
|
||||
[
|
||||
types.SET_CONVERSATION_MESSAGE_CREATION_LOCK,
|
||||
{
|
||||
conversationId: 1,
|
||||
...message.conversation,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sendMessageWithData', () => {
|
||||
it('removes optimistic message and updates lock metadata when creation is locked', async () => {
|
||||
const localCommit = vi.fn();
|
||||
const pendingMessage = {
|
||||
id: 'temp-1',
|
||||
conversation_id: 1,
|
||||
message: 'Locked message',
|
||||
};
|
||||
const error = {
|
||||
response: {
|
||||
data: {
|
||||
error: 'This conversation has reached the 10000 message limit.',
|
||||
message_limit: 10000,
|
||||
message_limit_reached: true,
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'message_limit',
|
||||
},
|
||||
},
|
||||
};
|
||||
axios.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
actions.sendMessageWithData({ commit: localCommit }, pendingMessage)
|
||||
).rejects.toBe(error);
|
||||
|
||||
expect(localCommit.mock.calls).toEqual([
|
||||
[
|
||||
types.ADD_MESSAGE,
|
||||
{
|
||||
...pendingMessage,
|
||||
status: 'progress',
|
||||
},
|
||||
],
|
||||
[types.DELETE_MESSAGE, pendingMessage],
|
||||
[
|
||||
types.SET_CONVERSATION_MESSAGE_CREATION_LOCK,
|
||||
{
|
||||
conversationId: 1,
|
||||
message_limit: 10000,
|
||||
message_limit_reached: true,
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'message_limit',
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps an existing failed message when retry is locked', async () => {
|
||||
const localCommit = vi.fn();
|
||||
const pendingMessage = {
|
||||
id: 42,
|
||||
conversation_id: 1,
|
||||
message: 'Retry message',
|
||||
status: 'failed',
|
||||
content_attributes: {
|
||||
external_error: 'Provider rejected the message',
|
||||
},
|
||||
};
|
||||
const error = {
|
||||
response: {
|
||||
data: {
|
||||
error: 'This conversation has reached the 10000 message limit.',
|
||||
message_limit: 10000,
|
||||
message_limit_reached: true,
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'message_limit',
|
||||
},
|
||||
},
|
||||
};
|
||||
axios.post.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
actions.sendMessageWithData({ commit: localCommit }, pendingMessage)
|
||||
).rejects.toBe(error);
|
||||
|
||||
expect(localCommit.mock.calls).toEqual([
|
||||
[
|
||||
types.ADD_MESSAGE,
|
||||
{
|
||||
...pendingMessage,
|
||||
status: 'progress',
|
||||
},
|
||||
],
|
||||
[
|
||||
types.ADD_MESSAGE,
|
||||
{
|
||||
...pendingMessage,
|
||||
meta: {
|
||||
error: error.response.data.error,
|
||||
},
|
||||
status: 'failed',
|
||||
},
|
||||
],
|
||||
[
|
||||
types.SET_CONVERSATION_MESSAGE_CREATION_LOCK,
|
||||
{
|
||||
conversationId: 1,
|
||||
message_limit: 10000,
|
||||
message_limit_reached: true,
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'message_limit',
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#markMessagesRead', () => {
|
||||
|
||||
@@ -109,6 +109,46 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_CONVERSATION_MESSAGE_CREATION_LOCK', () => {
|
||||
it('sets message creation lock metadata', () => {
|
||||
const state = { allConversations: [{ id: 1, messages: [] }] };
|
||||
mutations[types.SET_CONVERSATION_MESSAGE_CREATION_LOCK](state, {
|
||||
conversationId: 1,
|
||||
message_limit: 10000,
|
||||
message_limit_reached: true,
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'message_limit',
|
||||
});
|
||||
|
||||
expect(state.allConversations[0]).toMatchObject({
|
||||
message_limit: 10000,
|
||||
message_limit_reached: true,
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'message_limit',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_MESSAGE', () => {
|
||||
it('removes a pending message from the conversation', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
messages: [{ id: 'temp-1', echo_id: 'temp-1' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_MESSAGE](state, {
|
||||
conversation_id: 1,
|
||||
id: 'temp-1',
|
||||
});
|
||||
|
||||
expect(state.allConversations[0].messages).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ADD_MESSAGE', () => {
|
||||
it('does not add message to the store if conversation does not exist', () => {
|
||||
const state = { allConversations: [] };
|
||||
@@ -208,6 +248,32 @@ describe('#mutations', () => {
|
||||
]);
|
||||
expect(emitter.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates conversation lock metadata from message payload', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [] }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
|
||||
mutations[types.ADD_MESSAGE](state, {
|
||||
conversation_id: 1,
|
||||
content: 'Test message',
|
||||
created_at: 1602256198,
|
||||
conversation: {
|
||||
message_limit: 10000,
|
||||
message_limit_reached: true,
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'message_limit',
|
||||
},
|
||||
});
|
||||
|
||||
expect(state.allConversations[0]).toMatchObject({
|
||||
message_limit: 10000,
|
||||
message_limit_reached: true,
|
||||
message_creation_locked: true,
|
||||
message_creation_lock_reason: 'message_limit',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CHANGE_CONVERSATION_STATUS', () => {
|
||||
|
||||
Reference in New Issue
Block a user