store staged Attachments in redux

still an issue with the File in redux
This commit is contained in:
audric 2021-08-17 16:57:02 +10:00
parent 8a19b50c0f
commit 6a11a4c879
10 changed files with 218 additions and 67 deletions

View file

@ -10,21 +10,41 @@ import {
getUrl,
isVideoAttachment,
} from '../../types/Attachment';
import { useDispatch, useSelector } from 'react-redux';
import {
removeAllStagedAttachmentsInConversation,
removeStagedAttachmentInConversation,
} from '../../state/ducks/stagedAttachments';
import { getSelectedConversationKey } from '../../state/selectors/conversations';
type Props = {
attachments: Array<AttachmentType>;
// onError: () => void;
onClickAttachment: (attachment: AttachmentType) => void;
onCloseAttachment: (attachment: AttachmentType) => void;
onAddAttachment: () => void;
onClose: () => void;
};
const IMAGE_WIDTH = 120;
const IMAGE_HEIGHT = 120;
export const StagedAttachmentList = (props: Props) => {
const { attachments, onAddAttachment, onClickAttachment, onCloseAttachment, onClose } = props;
const { attachments, onAddAttachment, onClickAttachment } = props;
const dispatch = useDispatch();
const conversationKey = useSelector(getSelectedConversationKey);
const onRemoveAllStaged = () => {
if (!conversationKey) {
return;
}
dispatch(removeAllStagedAttachmentsInConversation({ conversationKey }));
};
const onRemoveByFilename = (filename: string) => {
if (!conversationKey) {
return;
}
dispatch(removeStagedAttachmentInConversation({ conversationKey, filename }));
};
if (!attachments.length) {
return null;
@ -36,7 +56,11 @@ export const StagedAttachmentList = (props: Props) => {
<div className="module-attachments">
{attachments.length > 1 ? (
<div className="module-attachments__header">
<div role="button" onClick={onClose} className="module-attachments__close-button" />
<div
role="button"
onClick={onRemoveAllStaged}
className="module-attachments__close-button"
/>
</div>
) : null}
<div className="module-attachments__rail">
@ -58,7 +82,9 @@ export const StagedAttachmentList = (props: Props) => {
url={getUrl(attachment)}
closeButton={true}
onClick={clickCallback}
onClickClose={onCloseAttachment}
onClickClose={() => {
onRemoveByFilename(attachment.fileName);
}}
/>
);
}
@ -69,7 +95,9 @@ export const StagedAttachmentList = (props: Props) => {
<StagedGenericAttachment
key={genericKey}
attachment={attachment}
onClose={onCloseAttachment}
onClose={() => {
onRemoveByFilename(attachment.fileName);
}}
/>
);
})}

View file

@ -26,6 +26,7 @@ import { SessionMainPanel } from '../SessionMainPanel';
import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist';
import { TimerOptionsArray } from '../../state/ducks/timerOptions';
import { getEmptyStagedAttachmentsState } from '../../state/ducks/stagedAttachments';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
@ -108,6 +109,7 @@ export class SessionInboxView extends React.Component<any, State> {
timerOptions: {
timerOptions,
},
stagedAttachments: getEmptyStagedAttachmentsState(),
};
this.store = createStore(initialState);

View file

@ -54,6 +54,7 @@ import {
import { connect } from 'react-redux';
import { StateType } from '../../../state/reducer';
import { getTheme } from '../../../state/selectors/theme';
import { removeAllStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments';
export interface ReplyingToMessageProps {
convoId: string;
@ -95,8 +96,6 @@ interface Props {
selectedConversation: ReduxConversationType | undefined;
quotedMessageProps?: ReplyingToMessageProps;
stagedAttachments: Array<StagedAttachmentType>;
clearAttachments: () => any;
removeAttachment: (toRemove: AttachmentType) => void;
onChoseAttachments: (newAttachments: Array<File>) => void;
}
@ -736,8 +735,6 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
attachments={stagedAttachments}
onClickAttachment={this.onClickAttachment}
onAddAttachment={this.onChooseAttachment}
onCloseAttachment={this.props.removeAttachment}
onClose={this.props.clearAttachments}
/>
{this.renderCaptionEditor(showCaptionEditor)}
</>
@ -886,8 +883,11 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
groupInvitation: undefined,
});
this.props.clearAttachments();
window.inboxStore?.dispatch(
removeAllStagedAttachmentsInConversation({
conversationKey: this.props.selectedConversationKey,
})
);
// Empty composition box and stagedAttachments
this.setState({
showEmojiPanel: false,
@ -907,8 +907,12 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
}
// this function is called right before sending a message, to gather really the files behind attachments.
private async getFiles() {
private async getFiles(): Promise<Array<any>> {
const { stagedAttachments } = this.props;
if (_.isEmpty(stagedAttachments)) {
return [];
}
// scale them down
const files = await Promise.all(
stagedAttachments.map(attachment =>
@ -917,8 +921,12 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
})
)
);
this.props.clearAttachments();
return files;
window.inboxStore?.dispatch(
removeAllStagedAttachmentsInConversation({
conversationKey: this.props.selectedConversationKey,
})
);
return _.compact(files);
}
private async sendVoiceMessage(audioBlob: Blob) {
@ -940,7 +948,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
isVoiceMessage: true,
};
await this.props.sendMessage({
this.props.sendMessage({
body: '',
attachments: [audioAttachment],
preview: undefined,

View file

@ -18,7 +18,7 @@ import styled, { DefaultTheme } from 'styled-components';
import { SessionMessagesListContainer } from './SessionMessagesListContainer';
import { LightboxGallery, MediaItemType } from '../../LightboxGallery';
import { AttachmentType, AttachmentTypeWithPath } from '../../../types/Attachment';
import { AttachmentTypeWithPath } from '../../../types/Attachment';
import { ToastUtils, UserUtils } from '../../../session/utils';
import * as MIME from '../../../types/MIME';
import { SessionFileDropzone } from './SessionFileDropzone';
@ -42,10 +42,10 @@ import {
import { SessionButtonColor } from '../SessionButton';
import { updateConfirmModal } from '../../../state/ducks/modalDialog';
import { addStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments';
interface State {
showRecordingView: boolean;
stagedAttachments: Array<StagedAttachmentType>;
isDraggingFile: boolean;
}
export interface LightBoxOptions {
@ -65,6 +65,8 @@ interface Props {
// lightbox options
lightBoxOptions?: LightBoxOptions;
stagedAttachments: Array<StagedAttachmentType>;
}
const SessionUnreadAboveIndicator = styled.div`
@ -102,7 +104,6 @@ export class SessionConversation extends React.Component<Props, State> {
this.state = {
showRecordingView: false,
stagedAttachments: [],
isDraggingFile: false,
};
this.messageContainerRef = React.createRef();
@ -163,7 +164,6 @@ export class SessionConversation extends React.Component<Props, State> {
if (newConversationKey !== oldConversationKey) {
this.setState({
showRecordingView: false,
stagedAttachments: [],
isDraggingFile: false,
});
}
@ -227,7 +227,7 @@ export class SessionConversation extends React.Component<Props, State> {
// ~~~~~~~~~~~~~~ RENDER METHODS ~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public render() {
const { showRecordingView, isDraggingFile, stagedAttachments } = this.state;
const { showRecordingView, isDraggingFile } = this.state;
const {
selectedConversation,
@ -274,11 +274,9 @@ export class SessionConversation extends React.Component<Props, State> {
<SessionCompositionBox
sendMessage={this.sendMessageFn}
stagedAttachments={stagedAttachments}
stagedAttachments={this.props.stagedAttachments}
onLoadVoiceNoteView={this.onLoadVoiceNoteView}
onExitVoiceNoteView={this.onExitVoiceNoteView}
clearAttachments={this.clearAttachments}
removeAttachment={this.removeAttachment}
onChoseAttachments={this.onChoseAttachments}
/>
</div>
@ -332,45 +330,6 @@ export class SessionConversation extends React.Component<Props, State> {
}
}
private clearAttachments() {
this.state.stagedAttachments.forEach(attachment => {
if (attachment.url) {
URL.revokeObjectURL(attachment.url);
}
if (attachment.videoUrl) {
URL.revokeObjectURL(attachment.videoUrl);
}
});
this.setState({ stagedAttachments: [] });
}
private removeAttachment(attachment: AttachmentType) {
const { stagedAttachments } = this.state;
const updatedStagedAttachments = (stagedAttachments || []).filter(
m => m.fileName !== attachment.fileName
);
this.setState({ stagedAttachments: updatedStagedAttachments });
}
private addAttachments(newAttachments: Array<StagedAttachmentType>) {
const { stagedAttachments } = this.state;
let newAttachmentsFiltered: Array<StagedAttachmentType> = [];
if (newAttachments?.length > 0) {
if (newAttachments.some(a => a.isVoiceMessage) && stagedAttachments.length > 0) {
throw new Error('A voice note cannot be sent with other attachments');
}
// do not add already added attachments
newAttachmentsFiltered = newAttachments.filter(
a => !stagedAttachments.some(b => b.file.path === a.file.path)
);
}
this.setState({
stagedAttachments: [...stagedAttachments, ...newAttachmentsFiltered],
});
}
private renderLightBox({ media, attachment }: LightBoxOptions) {
const selectedIndex =
media.length > 1
@ -399,7 +358,7 @@ export class SessionConversation extends React.Component<Props, State> {
const fileName = file.name;
const contentType = file.type;
const { stagedAttachments } = this.state;
const { stagedAttachments } = this.props;
if (window.Signal.Util.isFileDangerous(fileName)) {
ToastUtils.pushDangerousFileError();
@ -568,6 +527,15 @@ export class SessionConversation extends React.Component<Props, State> {
}
}
private addAttachments(newAttachments: Array<StagedAttachmentType>) {
window.inboxStore?.dispatch(
addStagedAttachmentsInConversation({
conversationKey: this.props.selectedConversationKey,
newAttachments,
})
);
}
private handleDrag(e: any) {
e.preventDefault();
e.stopPropagation();

View file

@ -208,6 +208,11 @@ function _cleanData(data: any): any {
if (_.isFunction(value.toNumber)) {
// eslint-disable-next-line no-param-reassign
data[key] = value.toNumber();
} else if (_.isFunction(value)) {
// just skip a function which has not a toNumber function. We don't want to save a function to the db.
// an attachment comes with a toJson() function
// tslint:disable-next-line: no-dynamic-delete
delete data[key];
} else if (Array.isArray(value)) {
// eslint-disable-next-line no-param-reassign
data[key] = value.map(_cleanData);
@ -619,7 +624,8 @@ export async function updateLastHash(data: {
}
export async function saveMessage(data: MessageAttributes): Promise<string> {
const id = await channels.saveMessage(_cleanData(data));
const cleanedData = _cleanData(data);
const id = await channels.saveMessage(cleanedData);
window.Whisper.ExpiringMessagesListener.update();
return id;
}

View file

@ -0,0 +1,103 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import _ from 'lodash';
import { StagedAttachmentType } from '../../components/session/conversation/SessionCompositionBox';
export type StagedAttachmentsStateType = {
stagedAttachments: { [conversationKey: string]: Array<StagedAttachmentType> };
};
// Reducer
export function getEmptyStagedAttachmentsState(): StagedAttachmentsStateType {
return {
stagedAttachments: {},
};
}
const stagedAttachmentsSlice = createSlice({
name: 'stagedAttachments',
initialState: getEmptyStagedAttachmentsState(),
reducers: {
addStagedAttachmentsInConversation(
state: StagedAttachmentsStateType,
action: PayloadAction<{
conversationKey: string;
newAttachments: Array<StagedAttachmentType>;
}>
) {
const { conversationKey, newAttachments } = action.payload;
if (newAttachments.length === 0) {
return state;
}
const currentStagedAttachments = state.stagedAttachments[conversationKey] || [];
if (newAttachments.some(a => a.isVoiceMessage) && currentStagedAttachments.length > 0) {
window?.log?.warn('A voice note cannot be sent with other attachments');
return state;
}
const allAttachments = _.concat(currentStagedAttachments, newAttachments);
const uniqAttachments = _.uniqBy(allAttachments, m => m.fileName);
state.stagedAttachments[conversationKey] = uniqAttachments;
return state;
},
removeAllStagedAttachmentsInConversation(
state: StagedAttachmentsStateType,
action: PayloadAction<{ conversationKey: string }>
) {
const { conversationKey } = action.payload;
const currentStagedAttachments = state.stagedAttachments[conversationKey];
if (!currentStagedAttachments || _.isEmpty(currentStagedAttachments)) {
return state;
}
currentStagedAttachments.forEach(attachment => {
if (attachment.url) {
URL.revokeObjectURL(attachment.url);
}
if (attachment.videoUrl) {
URL.revokeObjectURL(attachment.videoUrl);
}
});
// tslint:disable-next-line: no-dynamic-delete
delete state.stagedAttachments[conversationKey];
return state;
},
removeStagedAttachmentInConversation(
state: StagedAttachmentsStateType,
action: PayloadAction<{ conversationKey: string; filename: string }>
) {
const { conversationKey, filename } = action.payload;
const currentStagedAttachments = state.stagedAttachments[conversationKey];
if (!currentStagedAttachments || _.isEmpty(currentStagedAttachments)) {
return state;
}
const attachmentToRemove = currentStagedAttachments.find(m => m.fileName === filename);
if (!attachmentToRemove) {
return state;
}
if (attachmentToRemove.url) {
URL.revokeObjectURL(attachmentToRemove.url);
}
if (attachmentToRemove.videoUrl) {
URL.revokeObjectURL(attachmentToRemove.videoUrl);
}
state.stagedAttachments[conversationKey] = state.stagedAttachments[conversationKey].filter(
a => a.fileName !== filename
);
return state;
},
},
});
export const { actions, reducer } = stagedAttachmentsSlice;
export const {
addStagedAttachmentsInConversation,
removeAllStagedAttachmentsInConversation,
removeStagedAttachmentInConversation,
} = actions;

View file

@ -11,6 +11,10 @@ import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion';
import { modalReducer as modals, ModalState } from './ducks/modalDialog';
import { userConfigReducer as userConfig, UserConfigState } from './ducks/userConfig';
import { timerOptionReducer as timerOptions, TimerOptionsState } from './ducks/timerOptions';
import {
reducer as stagedAttachments,
StagedAttachmentsStateType,
} from './ducks/stagedAttachments';
export type StateType = {
search: SearchStateType;
@ -23,6 +27,7 @@ export type StateType = {
modals: ModalState;
userConfig: UserConfigState;
timerOptions: TimerOptionsState;
stagedAttachments: StagedAttachmentsStateType;
};
export const reducers = {
@ -36,6 +41,7 @@ export const reducers = {
modals,
userConfig,
timerOptions,
stagedAttachments,
};
// Making this work would require that our reducer signature supported AnyAction, not

View file

@ -0,0 +1,28 @@
import { createSelector } from 'reselect';
import { StagedAttachmentType } from '../../components/session/conversation/SessionCompositionBox';
import { StagedAttachmentsStateType } from '../ducks/stagedAttachments';
import { StateType } from '../reducer';
import { getSelectedConversationKey } from './conversations';
export const getStagedAttachmentsState = (state: StateType): StagedAttachmentsStateType =>
state.stagedAttachments;
const getStagedAttachmentsForConversation = (
state: StagedAttachmentsStateType,
conversationKey: string | undefined
) => {
if (!conversationKey) {
return undefined;
}
return state.stagedAttachments[conversationKey] || [];
};
export const getStagedAttachmentsForCurrentConversation = createSelector(
[getSelectedConversationKey, getStagedAttachmentsState],
(
selectedConversationKey: string | undefined,
state: StagedAttachmentsStateType
): Array<StagedAttachmentType> | undefined => {
return getStagedAttachmentsForConversation(state, selectedConversationKey);
}
);

View file

@ -13,6 +13,7 @@ import {
isRightPanelShowing,
} from '../selectors/conversations';
import { getOurNumber } from '../selectors/user';
import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments';
const mapStateToProps = (state: StateType) => {
return {
@ -25,6 +26,7 @@ const mapStateToProps = (state: StateType) => {
isRightPanelShowing: isRightPanelShowing(state),
selectedMessages: getSelectedMessageIds(state),
lightBoxOptions: getLightBoxOptions(state),
stagedAttachments: getStagedAttachmentsForCurrentConversation(state),
};
};

View file

@ -99,7 +99,7 @@ export async function autoScale<T extends { contentType: string; file: any }>(
export async function getFile(attachment: StagedAttachmentType, maxMeasurements?: MaxScaleSize) {
if (!attachment) {
return Promise.resolve();
return null;
}
const attachmentFlags = attachment.isVoiceMessage