mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
store staged Attachments in redux
still an issue with the File in redux
This commit is contained in:
parent
8a19b50c0f
commit
6a11a4c879
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
103
ts/state/ducks/stagedAttachments.ts
Normal file
103
ts/state/ducks/stagedAttachments.ts
Normal 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;
|
|
@ -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
|
||||
|
|
28
ts/state/selectors/stagedAttachments.ts
Normal file
28
ts/state/selectors/stagedAttachments.ts
Normal 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);
|
||||
}
|
||||
);
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue