signal-desktop/ts/state/ducks/conversations.ts
2019-05-16 15:23:21 -07:00

356 lines
7.6 KiB
TypeScript

import { omit } from 'lodash';
import { trigger } from '../../shims/events';
import { NoopActionType } from './noop';
// State
export type MessageSearchResultType = {
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;
};
export type ConversationType = {
id: string;
name?: string;
isArchived: boolean;
activeAt?: number;
timestamp: number;
lastMessage?: {
status: 'error' | 'sending' | 'sent' | 'delivered' | 'read';
text: string;
};
phoneNumber: string;
type: 'direct' | 'group';
isMe: boolean;
lastUpdated: number;
unreadCount: number;
isSelected: boolean;
isTyping: boolean;
};
export type ConversationLookupType = {
[key: string]: ConversationType;
};
export type MessageType = {
id: string;
};
export type MessageLookupType = {
[key: string]: MessageType;
};
export type ConversationMessageType = {
// And perhaps this could be part of our ConversationType? What if we moved all the selectors as part of this set of changes?
// We have the infrastructure for it now...
messages: Array<string>;
};
export type MessagesByConversationType = {
[key: string]: ConversationMessageType;
};
export type ConversationsStateType = {
conversationLookup: ConversationLookupType;
selectedConversation?: string;
showArchived: boolean;
// Note: it's very important that both of these locations are always kept up to date
messagesLookup: MessageLookupType;
messagesByConversation: MessagesByConversationType;
};
// 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;
};
};
export type SelectedConversationChangedActionType = {
type: 'SELECTED_CONVERSATION_CHANGED';
payload: {
id: string;
messageId?: string;
};
};
type ShowInboxActionType = {
type: 'SHOW_INBOX';
payload: null;
};
type ShowArchivedConversationsActionType = {
type: 'SHOW_ARCHIVED_CONVERSATIONS';
payload: null;
};
export type ConversationActionType =
| ConversationAddedActionType
| ConversationChangedActionType
| ConversationRemovedActionType
| RemoveAllConversationsActionType
| MessageExpiredActionType
| SelectedConversationChangedActionType
| MessageExpiredActionType
| SelectedConversationChangedActionType
| ShowInboxActionType
| ShowArchivedConversationsActionType;
// Action Creators
export const actions = {
conversationAdded,
conversationChanged,
conversationRemoved,
removeAllConversations,
messageExpired,
openConversationInternal,
openConversationExternal,
showInbox,
showArchivedConversations,
};
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,
};
}
function messageExpired(
id: string,
conversationId: string
): MessageExpiredActionType {
return {
type: 'MESSAGE_EXPIRED',
payload: {
id,
conversationId,
},
};
}
// Note: we need two actions here to simplify. Operations outside of the left pane can
// trigger an 'openConversation' so we go through Whisper.events for all conversation
// selection.
function openConversationInternal(
id: string,
messageId?: string
): NoopActionType {
trigger('showConversation', id, messageId);
return {
type: 'NOOP',
payload: null,
};
}
function openConversationExternal(
id: string,
messageId?: string
): SelectedConversationChangedActionType {
return {
type: 'SELECTED_CONVERSATION_CHANGED',
payload: {
id,
messageId,
},
};
}
function showInbox() {
return {
type: 'SHOW_INBOX',
payload: null,
};
}
function showArchivedConversations() {
return {
type: 'SHOW_ARCHIVED_CONVERSATIONS',
payload: null,
};
}
// Reducer
function getEmptyState(): ConversationsStateType {
return {
conversationLookup: {},
showArchived: false,
messagesLookup: {},
messagesByConversation: {},
};
}
export function reducer(
state: ConversationsStateType,
action: ConversationActionType
): ConversationsStateType {
if (!state) {
return getEmptyState();
}
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;
let showArchived = state.showArchived;
let selectedConversation = state.selectedConversation;
const existing = conversationLookup[id];
// In the change case we only modify the lookup if we already had that conversation
if (!existing) {
return state;
}
if (selectedConversation === id) {
// Archived -> Inbox: we go back to the normal inbox view
if (existing.isArchived && !data.isArchived) {
showArchived = false;
}
// 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;
}
}
return {
...state,
selectedConversation,
showArchived,
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') {
// noop - for now this is only important for search
}
if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
const { payload } = action;
const { id } = payload;
return {
...state,
selectedConversation: id,
};
}
if (action.type === 'SHOW_INBOX') {
return {
...state,
showArchived: false,
};
}
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
return {
...state,
showArchived: true,
};
}
return state;
}