29de50c12a
Co-authored-by: scott@signal.org Co-authored-by: ken@signal.org
354 lines
7.6 KiB
TypeScript
354 lines
7.6 KiB
TypeScript
import { AnyAction } from 'redux';
|
|
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 =
|
|
| AnyAction
|
|
| 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 = getEmptyState(),
|
|
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;
|
|
|
|
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;
|
|
}
|