session-desktop/ts/state/ducks/conversations.ts

506 lines
12 KiB
TypeScript
Raw Normal View History

2020-11-16 04:45:13 +01:00
import _, { omit } from 'lodash';
2019-01-14 22:49:58 +01:00
2020-11-16 04:45:13 +01:00
import { Constants } from '../../session';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { MessageModel } from '../../../js/models/messages';
2019-01-14 22:49:58 +01:00
// State
export type MessageType = {
id: string;
conversationId: string;
receivedAt: number;
snippet: string;
from: {
phoneNumber: string;
isMe?: boolean;
name?: string;
color?: string;
profileName?: string;
avatarPath?: string;
};
to: {
groupName?: string;
phoneNumber: string;
isMe?: boolean;
name?: string;
profileName?: string;
};
isSelected?: boolean;
};
2020-11-16 04:45:13 +01:00
export type MessageTypeInConvo = {
id: string;
conversationId: string;
attributes: any;
propsForMessage: Object;
propsForSearchResult: Object;
propsForGroupInvitation: Object;
propsForTimerNotification: Object;
propsForVerificationNotification: Object;
propsForResetSessionNotification: Object;
propsForGroupNotification: Object;
firstMessageOfSeries: boolean;
receivedAt: number;
};
2019-01-14 22:49:58 +01:00
export type ConversationType = {
id: string;
name?: string;
2019-03-12 01:20:16 +01:00
isArchived: boolean;
2019-01-14 22:49:58 +01:00
activeAt?: number;
timestamp: number;
lastMessage?: {
status: 'error' | 'sending' | 'sent' | 'delivered' | 'read';
text: string;
2019-07-23 08:15:39 +02:00
isRss: boolean;
2019-01-14 22:49:58 +01:00
};
phoneNumber: string;
type: 'direct' | 'group';
isMe: boolean;
isPublic?: boolean;
isRss?: boolean;
isClosable?: boolean;
2019-01-14 22:49:58 +01:00
lastUpdated: number;
unreadCount: number;
mentionedUs: boolean;
2019-01-14 22:49:58 +01:00
isSelected: boolean;
isTyping: boolean;
2020-05-12 01:29:10 +02:00
isSecondary?: boolean;
2020-05-12 05:15:58 +02:00
primaryDevice: string;
isBlocked: boolean;
isKickedFromGroup: boolean;
leftGroup: boolean;
2019-01-14 22:49:58 +01:00
};
export type ConversationLookupType = {
[key: string]: ConversationType;
};
export type ConversationsStateType = {
conversationLookup: ConversationLookupType;
selectedConversation?: string;
2020-11-16 04:45:13 +01:00
messages: Array<MessageTypeInConvo>;
2019-01-14 22:49:58 +01:00
};
2020-11-16 04:45:13 +01:00
async function getMessages(
conversationKey: string,
numMessages: number
): Promise<Array<MessageTypeInConvo>> {
const conversation = window.ConversationController.get(conversationKey);
if (!conversation) {
// no valid conversation, early return
window.log.error('Failed to get convo on reducer.');
return [];
}
const unreadCount = await conversation.getUnreadCount();
let msgCount =
numMessages ||
Number(Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT) + unreadCount;
msgCount =
msgCount > Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT
? Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT
: msgCount;
if (msgCount < Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT) {
msgCount = Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT;
}
const messageSet = await window.Signal.Data.getMessagesByConversation(
conversationKey,
{ limit: msgCount, MessageCollection: window.Whisper.MessageCollection }
);
// Set first member of series here.
const messageModels = messageSet.models;
const messages = [];
// no need to do that `firstMessageOfSeries` on a private chat
if (conversation.isPrivate()) {
return messageModels;
}
// messages are got from the more recent to the oldest, so we need to check if
// the next messages in the list is still the same author.
// The message is the first of the series if the next message is not from the same author
for (let i = 0; i < messageModels.length; i++) {
// Handle firstMessageOfSeries for conditional avatar rendering
let firstMessageOfSeries = true;
const currentSender = messageModels[i].propsForMessage?.authorPhoneNumber;
const nextSender =
i < messageModels.length - 1
? messageModels[i + 1].propsForMessage?.authorPhoneNumber
: undefined;
if (i > 0 && currentSender === nextSender) {
firstMessageOfSeries = false;
}
messages.push({ ...messageModels[i], firstMessageOfSeries });
}
return messages;
}
const fetchMessagesForConversation = createAsyncThunk(
'messages/fetchByConversationKey',
async ({
conversationKey,
count,
}: {
conversationKey: string;
count: number;
}) => {
const messages = await getMessages(conversationKey, count);
return {
conversationKey,
messages,
};
}
);
2019-01-14 22:49:58 +01:00
// Actions
type ConversationAddedActionType = {
type: 'CONVERSATION_ADDED';
payload: {
id: string;
data: ConversationType;
};
};
type ConversationChangedActionType = {
type: 'CONVERSATION_CHANGED';
payload: {
id: string;
data: ConversationType;
};
};
type ConversationRemovedActionType = {
type: 'CONVERSATION_REMOVED';
payload: {
id: string;
};
};
export type RemoveAllConversationsActionType = {
type: 'CONVERSATIONS_REMOVE_ALL';
payload: null;
};
export type MessageExpiredActionType = {
type: 'MESSAGE_EXPIRED';
payload: {
id: string;
conversationId: string;
};
};
2020-11-16 04:45:13 +01:00
export type MessageChangedActionType = {
type: 'MESSAGE_CHANGED';
payload: MessageModel;
};
export type MessageAddedActionType = {
type: 'MESSAGE_ADDED';
payload: {
conversationKey: string;
messageModel: MessageModel;
};
};
2019-01-14 22:49:58 +01:00
export type SelectedConversationChangedActionType = {
type: 'SELECTED_CONVERSATION_CHANGED';
payload: {
id: string;
messageId?: string;
};
};
2020-11-16 04:45:13 +01:00
export type FetchMessagesForConversationType = {
type: 'messages/fetchByConversationKey/fulfilled';
payload: {
conversationKey: string;
messages: Array<MessageModel>;
};
};
2019-01-14 22:49:58 +01:00
export type ConversationActionType =
| ConversationAddedActionType
| ConversationChangedActionType
| ConversationRemovedActionType
| RemoveAllConversationsActionType
| MessageExpiredActionType
2020-11-16 04:45:13 +01:00
| MessageAddedActionType
| MessageChangedActionType
2019-03-12 01:20:16 +01:00
| SelectedConversationChangedActionType
2020-11-16 04:45:13 +01:00
| SelectedConversationChangedActionType
| FetchMessagesForConversationType;
2019-01-14 22:49:58 +01:00
// Action Creators
export const actions = {
conversationAdded,
conversationChanged,
conversationRemoved,
removeAllConversations,
messageExpired,
2020-11-16 04:45:13 +01:00
messageAdded,
messageChanged,
fetchMessagesForConversation,
2019-01-14 22:49:58 +01:00
openConversationExternal,
};
function conversationAdded(
id: string,
data: ConversationType
): ConversationAddedActionType {
return {
type: 'CONVERSATION_ADDED',
payload: {
id,
data,
},
};
}
function conversationChanged(
id: string,
data: ConversationType
): ConversationChangedActionType {
return {
type: 'CONVERSATION_CHANGED',
payload: {
id,
data,
},
};
}
function conversationRemoved(id: string): ConversationRemovedActionType {
return {
type: 'CONVERSATION_REMOVED',
payload: {
id,
},
};
}
function removeAllConversations(): RemoveAllConversationsActionType {
return {
type: 'CONVERSATIONS_REMOVE_ALL',
payload: null,
};
}
2019-03-12 01:20:16 +01:00
2019-01-14 22:49:58 +01:00
function messageExpired(
id: string,
conversationId: string
): MessageExpiredActionType {
return {
type: 'MESSAGE_EXPIRED',
payload: {
id,
conversationId,
},
};
}
2020-11-16 04:45:13 +01:00
function messageChanged(messageModel: MessageModel): MessageChangedActionType {
return {
type: 'MESSAGE_CHANGED',
payload: messageModel,
};
}
function messageAdded({
conversationKey,
messageModel,
}: {
conversationKey: string;
messageModel: MessageModel;
}): MessageAddedActionType {
return {
type: 'MESSAGE_ADDED',
payload: {
conversationKey,
messageModel,
},
};
}
2019-01-14 22:49:58 +01:00
function openConversationExternal(
id: string,
messageId?: string
): SelectedConversationChangedActionType {
return {
type: 'SELECTED_CONVERSATION_CHANGED',
payload: {
id,
messageId,
},
};
}
// Reducer
2020-11-16 04:45:13 +01:00
const toPickFromMessageModel = [
'attributes',
'id',
'propsForSearchResult',
'propsForMessage',
'receivedAt',
'conversationId',
'firstMessageOfSeries',
'propsForGroupInvitation',
'propsForTimerNotification',
'propsForVerificationNotification',
'propsForResetSessionNotification',
'propsForGroupNotification',
];
2019-01-14 22:49:58 +01:00
function getEmptyState(): ConversationsStateType {
return {
conversationLookup: {},
2020-11-16 04:45:13 +01:00
messages: [],
2019-01-14 22:49:58 +01:00
};
}
2020-11-16 04:45:13 +01:00
// tslint:disable-next-line: cyclomatic-complexity
2019-01-14 22:49:58 +01:00
export function reducer(
state: ConversationsStateType = getEmptyState(),
2019-01-14 22:49:58 +01:00
action: ConversationActionType
): ConversationsStateType {
if (action.type === 'CONVERSATION_ADDED') {
const { payload } = action;
const { id, data } = payload;
const { conversationLookup } = state;
return {
...state,
conversationLookup: {
...conversationLookup,
[id]: data,
},
};
}
if (action.type === 'CONVERSATION_CHANGED') {
const { payload } = action;
const { id, data } = payload;
const { conversationLookup } = state;
2019-03-12 01:20:16 +01:00
let selectedConversation = state.selectedConversation;
const existing = conversationLookup[id];
2019-01-14 22:49:58 +01:00
// In the change case we only modify the lookup if we already had that conversation
2019-03-12 01:20:16 +01:00
if (!existing) {
2019-01-14 22:49:58 +01:00
return state;
}
2019-03-12 01:20:16 +01:00
if (selectedConversation === id) {
// Inbox -> Archived: no conversation is selected
// Note: With today's stacked converastions architecture, this can result in weird
// behavior - no selected conversation in the left pane, but a conversation show
// in the right pane.
if (!existing.isArchived && data.isArchived) {
selectedConversation = undefined;
}
}
2019-01-14 22:49:58 +01:00
return {
...state,
2019-03-12 01:20:16 +01:00
selectedConversation,
2019-01-14 22:49:58 +01:00
conversationLookup: {
...conversationLookup,
[id]: data,
},
};
}
if (action.type === 'CONVERSATION_REMOVED') {
const { payload } = action;
const { id } = payload;
const { conversationLookup } = state;
return {
...state,
conversationLookup: omit(conversationLookup, [id]),
};
}
if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
return getEmptyState();
}
if (action.type === 'MESSAGE_EXPIRED') {
// nothing to do here
2019-01-14 22:49:58 +01:00
}
2019-03-12 01:20:16 +01:00
if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
const { payload } = action;
const { id } = payload;
return {
...state,
selectedConversation: id,
};
}
2020-11-16 04:45:13 +01:00
if (action.type === fetchMessagesForConversation.fulfilled.type) {
const { messages, conversationKey } = action.payload as any;
// double check that this update is for the shown convo
if (conversationKey === state.selectedConversation) {
const lightMessages = messages.map((m: any) =>
_.pick(m, toPickFromMessageModel)
) as Array<MessageTypeInConvo>;
return {
...state,
messages: lightMessages,
};
}
return state;
}
if (action.type === 'MESSAGE_CHANGED') {
const messageInStoreIndex = state?.messages.findIndex(
m => m.id === action.payload.id
);
if (messageInStoreIndex >= 0) {
const changedMessage = _.pick(
action.payload as any,
toPickFromMessageModel
) as MessageTypeInConvo;
// we cannot edit the array directly, so slice the first part, insert our edited message, and slice the second part
const editedMessages = [
...state.messages.slice(0, messageInStoreIndex),
changedMessage,
...state.messages.slice(messageInStoreIndex + 1),
];
return {
...state,
messages: editedMessages,
};
}
return state;
}
if (action.type === 'MESSAGE_ADDED') {
const { conversationKey, messageModel } = action.payload;
if (conversationKey === state.selectedConversation) {
const { messages } = state;
const addedMessage = _.pick(
messageModel as any,
toPickFromMessageModel
) as MessageTypeInConvo;
const messagesWithNewMessage = [...messages, addedMessage];
const convo = state.conversationLookup[state.selectedConversation];
const isPublic = convo?.isPublic;
if (convo && isPublic) {
return {
...state,
messages: messagesWithNewMessage.sort(
(a: any, b: any) =>
b.attributes.serverTimestamp - a.attributes.serverTimestamp
),
};
}
if (convo) {
return {
...state,
messages: messagesWithNewMessage.sort(
(a, b) => b.attributes.timestamp - a.attributes.timestamp
),
};
}
}
return state;
}
2019-01-14 22:49:58 +01:00
return state;
}