link backbone message added to redux

pull/1381/head
Audric Ackermann 5 years ago
parent 2f2eb2ad53
commit f9ab90fb71
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -468,7 +468,12 @@
const model = this.addSingleMessage(message); const model = this.addSingleMessage(message);
MessageController.register(model.id, model); MessageController.register(model.id, model);
this.trigger('change'); window.Whisper.events.trigger('messageAdded', {
conversationKey: this.id,
messageModel: model,
});
this.trigger('change', this);
}, },
addSingleMessage(message, setToExpire = true) { addSingleMessage(message, setToExpire = true) {
const model = this.messageCollection.add(message, { merge: true }); const model = this.messageCollection.add(message, { merge: true });
@ -1201,6 +1206,11 @@
const id = await message.commit(); const id = await message.commit();
message.set({ id }); message.set({ id });
window.Whisper.events.trigger('messageAdded', {
conversationKey: this.id,
messageModel: message,
});
this.set({ this.set({
lastMessage: model.getNotificationText(), lastMessage: model.getNotificationText(),
lastMessageStatus: 'sending', lastMessageStatus: 'sending',

@ -107,7 +107,6 @@ const {
// State // State
const conversationsDuck = require('../../ts/state/ducks/conversations'); const conversationsDuck = require('../../ts/state/ducks/conversations');
const userDuck = require('../../ts/state/ducks/user'); const userDuck = require('../../ts/state/ducks/user');
const messagesDuck = require('../../ts/state/ducks/messages');
// Migrations // Migrations
const { const {
@ -283,7 +282,6 @@ exports.setup = (options = {}) => {
const Ducks = { const Ducks = {
conversations: conversationsDuck, conversations: conversationsDuck,
user: userDuck, user: userDuck,
messages: messagesDuck,
}; };
const State = { const State = {
Ducks, Ducks,

@ -124,13 +124,12 @@
}, },
onEmpty() { onEmpty() {
// const view = this.inboxView; // const view = this.inboxView;
// this.initialLoadComplete = true; // this.initialLoadComplete = true;
// if (view) { // if (view) {
// view.onEmpty(); // view.onEmpty();
// } // }
}, },
onProgress(count) { onProgress() {
// const view = this.inboxView; // const view = this.inboxView;
// if (view) { // if (view) {
// view.onProgress(count); // view.onProgress(count);

@ -56,7 +56,6 @@ export class SessionInboxView extends React.Component<Props, State> {
void this.setupLeftPane(); void this.setupLeftPane();
// ConversationCollection // ConversationCollection
conversationModels;
// this.listenTo(inboxCollection, 'messageError', () => { // this.listenTo(inboxCollection, 'messageError', () => {
// if (this.networkStatusView) { // if (this.networkStatusView) {
// this.networkStatusView.render(); // this.networkStatusView.render();
@ -219,7 +218,7 @@ export class SessionInboxView extends React.Component<Props, State> {
window.inboxStore = this.store; window.inboxStore = this.store;
// Enables our redux store to be updated by backbone events in the outside world // Enables our redux store to be updated by backbone events in the outside world
const { messageExpired } = bindActionCreators( const { messageExpired, messageAdded, messageChanged } = bindActionCreators(
window.Signal.State.Ducks.conversations.actions, window.Signal.State.Ducks.conversations.actions,
this.store.dispatch this.store.dispatch
); );
@ -228,10 +227,6 @@ export class SessionInboxView extends React.Component<Props, State> {
window.Signal.State.Ducks.user.actions, window.Signal.State.Ducks.user.actions,
this.store.dispatch this.store.dispatch
); );
const { messageChanged } = bindActionCreators(
window.Signal.State.Ducks.messages.actions,
this.store.dispatch
);
this.fetchHandleMessageSentData = this.fetchHandleMessageSentData.bind( this.fetchHandleMessageSentData = this.fetchHandleMessageSentData.bind(
this this
@ -248,6 +243,7 @@ export class SessionInboxView extends React.Component<Props, State> {
window.Whisper.events.on('messageExpired', messageExpired); window.Whisper.events.on('messageExpired', messageExpired);
window.Whisper.events.on('messageChanged', messageChanged); window.Whisper.events.on('messageChanged', messageChanged);
window.Whisper.events.on('messageAdded', messageAdded);
window.Whisper.events.on('userChanged', userChanged); window.Whisper.events.on('userChanged', userChanged);
this.setState({ isInitialLoadComplete: true }); this.setState({ isInitialLoadComplete: true });

@ -191,12 +191,12 @@ export class SessionConversation extends React.Component<Props, State> {
} = this.state; } = this.state;
const selectionMode = !!selectedMessages.length; const selectionMode = !!selectedMessages.length;
const { conversation, conversationKey } = this.props; const { conversation, conversationKey, messages } = this.props;
const conversationModel = window.ConversationController.get( const conversationModel = window.ConversationController.get(
conversationKey conversationKey
); );
if (!conversationModel) { if (!conversationModel || !messages) {
// return an empty message view // return an empty message view
return <MessageView />; return <MessageView />;
} }

@ -4,13 +4,12 @@ import { actions as search } from './ducks/search';
import { actions as conversations } from './ducks/conversations'; import { actions as conversations } from './ducks/conversations';
import { actions as user } from './ducks/user'; import { actions as user } from './ducks/user';
import { actions as sections } from './ducks/section'; import { actions as sections } from './ducks/section';
import { actions as messages } from './ducks/messages';
const actions = { const actions = {
...search, ...search,
...conversations, ...conversations,
...user, ...user,
...messages, // ...messages,
...sections, ...sections,
}; };

@ -1,6 +1,8 @@
import { omit } from 'lodash'; import _, { omit } from 'lodash';
import { trigger } from '../../shims/events'; import { Constants } from '../../session';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { MessageModel } from '../../../js/models/messages';
// State // State
@ -30,6 +32,22 @@ export type MessageType = {
isSelected?: boolean; isSelected?: boolean;
}; };
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;
};
export type ConversationType = { export type ConversationType = {
id: string; id: string;
name?: string; name?: string;
@ -65,7 +83,81 @@ export type ConversationLookupType = {
export type ConversationsStateType = { export type ConversationsStateType = {
conversationLookup: ConversationLookupType; conversationLookup: ConversationLookupType;
selectedConversation?: string; selectedConversation?: string;
messages: Array<MessageTypeInConvo>;
};
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,
}; };
}
);
// Actions // Actions
@ -100,6 +192,17 @@ export type MessageExpiredActionType = {
conversationId: string; conversationId: string;
}; };
}; };
export type MessageChangedActionType = {
type: 'MESSAGE_CHANGED';
payload: MessageModel;
};
export type MessageAddedActionType = {
type: 'MESSAGE_ADDED';
payload: {
conversationKey: string;
messageModel: MessageModel;
};
};
export type SelectedConversationChangedActionType = { export type SelectedConversationChangedActionType = {
type: 'SELECTED_CONVERSATION_CHANGED'; type: 'SELECTED_CONVERSATION_CHANGED';
payload: { payload: {
@ -108,15 +211,25 @@ export type SelectedConversationChangedActionType = {
}; };
}; };
export type FetchMessagesForConversationType = {
type: 'messages/fetchByConversationKey/fulfilled';
payload: {
conversationKey: string;
messages: Array<MessageModel>;
};
};
export type ConversationActionType = export type ConversationActionType =
| ConversationAddedActionType | ConversationAddedActionType
| ConversationChangedActionType | ConversationChangedActionType
| ConversationRemovedActionType | ConversationRemovedActionType
| RemoveAllConversationsActionType | RemoveAllConversationsActionType
| MessageExpiredActionType | MessageExpiredActionType
| MessageAddedActionType
| MessageChangedActionType
| SelectedConversationChangedActionType | SelectedConversationChangedActionType
| MessageExpiredActionType | SelectedConversationChangedActionType
| SelectedConversationChangedActionType; | FetchMessagesForConversationType;
// Action Creators // Action Creators
@ -126,6 +239,9 @@ export const actions = {
conversationRemoved, conversationRemoved,
removeAllConversations, removeAllConversations,
messageExpired, messageExpired,
messageAdded,
messageChanged,
fetchMessagesForConversation,
openConversationExternal, openConversationExternal,
}; };
@ -181,6 +297,29 @@ function messageExpired(
}; };
} }
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,
},
};
}
function openConversationExternal( function openConversationExternal(
id: string, id: string,
messageId?: string messageId?: string
@ -196,12 +335,29 @@ function openConversationExternal(
// Reducer // Reducer
const toPickFromMessageModel = [
'attributes',
'id',
'propsForSearchResult',
'propsForMessage',
'receivedAt',
'conversationId',
'firstMessageOfSeries',
'propsForGroupInvitation',
'propsForTimerNotification',
'propsForVerificationNotification',
'propsForResetSessionNotification',
'propsForGroupNotification',
];
function getEmptyState(): ConversationsStateType { function getEmptyState(): ConversationsStateType {
return { return {
conversationLookup: {}, conversationLookup: {},
messages: [],
}; };
} }
// tslint:disable-next-line: cyclomatic-complexity
export function reducer( export function reducer(
state: ConversationsStateType = getEmptyState(), state: ConversationsStateType = getEmptyState(),
action: ConversationActionType action: ConversationActionType
@ -263,7 +419,8 @@ export function reducer(
return getEmptyState(); return getEmptyState();
} }
if (action.type === 'MESSAGE_EXPIRED') { if (action.type === 'MESSAGE_EXPIRED') {
// noop - for now this is only important for search // FIXME
console.warn('EXPIRED');
} }
if (action.type === 'SELECTED_CONVERSATION_CHANGED') { if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
const { payload } = action; const { payload } = action;
@ -273,5 +430,77 @@ export function reducer(
selectedConversation: id, selectedConversation: id,
}; };
} }
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;
}
return state; return state;
} }

@ -1,129 +0,0 @@
import { Constants } from '../../session';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import _ from 'lodash';
import { MessageType } from './conversations';
export type MessagesStateType = Array<MessageType>;
export async function getMessages(
conversationKey: string,
numMessages: number
): Promise<MessagesStateType> {
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 authori
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;
}
// ACTIONS
const fetchMessagesForConversation = createAsyncThunk(
'messages/fetchByConversationKey',
async ({
conversationKey,
count,
}: {
conversationKey: string;
count: number;
}) => {
return getMessages(conversationKey, count);
}
);
const toPickFromMessageModel = [
'attributes',
'id',
'propsForSearchResult',
'propsForMessage',
'receivedAt',
'conversationId',
'firstMessageOfSeries',
'propsForGroupInvitation',
'propsForTimerNotification',
'propsForVerificationNotification',
'propsForResetSessionNotification',
'propsForGroupNotification',
];
const messageSlice = createSlice({
name: 'messages',
initialState: [] as MessagesStateType,
reducers: {
messageChanged(state: MessagesStateType, action) {
console.log('message changed ', action);
const messageInStoreIndex = state.findIndex(
m => m.id === action.payload.id
);
if (messageInStoreIndex >= 0) {
state[messageInStoreIndex] = _.pick(
action.payload,
toPickFromMessageModel
) as MessageType;
}
return state;
},
},
extraReducers: {
// Add reducers for additional action types here, and handle loading state as needed
[fetchMessagesForConversation.fulfilled.type]: (state, action) => {
// console.log('fetchMessagesForConversatio0 NON LIGHT', action.payload);
const lightMessages = action.payload.map((m: any) =>
_.pick(m, toPickFromMessageModel)
) as MessagesStateType;
// console.log('fetchMessagesForConversation', lightMessages);
return lightMessages;
},
},
});
export const actions = {
...messageSlice.actions,
fetchMessagesForConversation,
};
export const reducer = messageSlice.reducer;

@ -8,11 +8,10 @@ import {
import { reducer as user, UserStateType } from './ducks/user'; import { reducer as user, UserStateType } from './ducks/user';
import { reducer as theme, ThemeStateType } from './ducks/theme'; import { reducer as theme, ThemeStateType } from './ducks/theme';
import { reducer as section, SectionStateType } from './ducks/section'; import { reducer as section, SectionStateType } from './ducks/section';
import { MessagesStateType, reducer as messages } from './ducks/messages';
export type StateType = { export type StateType = {
search: SearchStateType; search: SearchStateType;
messages: MessagesStateType; // messages: MessagesStateType;
user: UserStateType; user: UserStateType;
conversations: ConversationsStateType; conversations: ConversationsStateType;
theme: ThemeStateType; theme: ThemeStateType;
@ -22,7 +21,7 @@ export type StateType = {
export const reducers = { export const reducers = {
search, search,
// Temporary until ./ducks/messages is working // Temporary until ./ducks/messages is working
messages, // messages,
// messages: search, // messages: search,
conversations, conversations,
user, user,

@ -14,7 +14,7 @@ const mapStateToProps = (state: StateType) => {
conversation, conversation,
conversationKey, conversationKey,
theme: state.theme, theme: state.theme,
messages: state.messages, messages: state.conversations.messages,
}; };
}; };

Loading…
Cancel
Save