Keep story creator around until we've verified contacts and queued job

This commit is contained in:
Scott Nonnenberg 2022-11-01 17:36:16 -07:00 committed by GitHub
parent 4fc1b6388c
commit 9fba33943a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 178 additions and 41 deletions

View file

@ -587,6 +587,7 @@ export const CompositionArea = ({
<MediaEditor
i18n={i18n}
imageSrc={attachmentToEdit.url}
isSending={false}
onClose={() => setAttachmentToEdit(undefined)}
onDone={data => {
const newAttachment = {

View file

@ -27,6 +27,7 @@ const getDefaultProps = (): PropsType => ({
imageSrc: IMAGE_2,
onClose: action('onClose'),
onDone: action('onDone'),
isSending: false,
// StickerButtonProps
installedPacks,
@ -49,6 +50,10 @@ export const Portrait = (): JSX.Element => (
<MediaEditor {...getDefaultProps()} imageSrc={IMAGE_4} />
);
export const Sending = (): JSX.Element => (
<MediaEditor {...getDefaultProps()} isSending />
);
export const WithCaption = (): JSX.Element => (
<MediaEditor
{...getDefaultProps()}

View file

@ -39,11 +39,13 @@ import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTe
import { Emojify } from './conversation/Emojify';
import { AddNewLines } from './conversation/AddNewLines';
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
import { Spinner } from './Spinner';
export type PropsType = {
doneButtonLabel?: string;
i18n: LocalizerType;
imageSrc: string;
isSending: boolean;
onClose: () => unknown;
onDone: (data: Uint8Array, caption?: string | undefined) => unknown;
} & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'> &
@ -106,6 +108,7 @@ export const MediaEditor = ({
doneButtonLabel,
i18n,
imageSrc,
isSending,
onClose,
onDone,
@ -1102,7 +1105,7 @@ export const MediaEditor = ({
/>
</div>
<Button
disabled={!image || isSaving}
disabled={!image || isSaving || isSending}
onClick={async () => {
if (!fabricCanvas) {
return;
@ -1160,7 +1163,11 @@ export const MediaEditor = ({
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{doneButtonLabel || i18n('save')}
{isSending ? (
<Spinner svgSize="small" />
) : (
doneButtonLabel || i18n('save')
)}
</Button>
</div>
</div>

View file

@ -14,9 +14,9 @@ import type {
} from '../types/Stories';
import type { LocalizerType } from '../types/Util';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { PropsType as SmartStoryCreatorPropsType } from '../state/smart/StoryCreator';
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
import type {
AddStoryData,
ViewUserStoriesActionCreatorType,
ViewStoryActionCreatorType,
} from '../state/ducks/stories';
@ -27,6 +27,7 @@ import { getWidthFromPreferredWidth } from '../util/leftPaneWidth';
import { useEscapeHandling } from '../hooks/useEscapeHandling';
export type PropsType = {
addStoryData: AddStoryData;
deleteStoryForEveryone: (story: StoryViewType) => unknown;
getPreferredBadge: PreferredBadgeSelectorType;
hiddenStories: Array<ConversationStoryType>;
@ -37,7 +38,8 @@ export type PropsType = {
onSaveStory: (story: StoryViewType) => unknown;
preferredWidthFromStorage: number;
queueStoryDownload: (storyId: string) => unknown;
renderStoryCreator: (props: SmartStoryCreatorPropsType) => JSX.Element;
renderStoryCreator: () => JSX.Element;
setAddStoryData: (data: AddStoryData) => unknown;
showConversation: ShowConversationType;
showStoriesSettings: () => unknown;
showToast: ShowToastActionCreatorType;
@ -51,15 +53,8 @@ export type PropsType = {
hasViewReceiptSetting: boolean;
};
type AddStoryType =
| {
type: 'Media';
file: File;
}
| { type: 'Text' }
| undefined;
export const Stories = ({
addStoryData,
deleteStoryForEveryone,
getPreferredBadge,
hiddenStories,
@ -71,6 +66,7 @@ export const Stories = ({
preferredWidthFromStorage,
queueStoryDownload,
renderStoryCreator,
setAddStoryData,
showConversation,
showStoriesSettings,
showToast,
@ -87,7 +83,6 @@ export const Stories = ({
requiresFullWidth: true,
});
const [addStoryData, setAddStoryData] = useState<AddStoryType>();
const [isMyStories, setIsMyStories] = useState(false);
// only handle ESC if not showing a child that handles their own ESC
@ -102,11 +97,7 @@ export const Stories = ({
return (
<div className={classNames('Stories', themeClassName(Theme.Dark))}>
{addStoryData &&
renderStoryCreator({
file: addStoryData.type === 'Media' ? addStoryData.file : undefined,
onClose: () => setAddStoryData(undefined),
})}
{addStoryData && renderStoryCreator()}
<div className="Stories__pane" style={{ width }}>
{isMyStories && myStories.length ? (
<MyStories

View file

@ -40,6 +40,9 @@ export default {
installedPacks: {
defaultValue: [],
},
isSending: {
defaultValue: false,
},
linkPreview: {
defaultValue: undefined,
},
@ -95,3 +98,8 @@ FirstTime.args = {
FirstTime.story = {
name: 'First time posting a story',
};
export const Sending = Template.bind({});
Sending.args = {
isSending: true,
};

View file

@ -30,6 +30,7 @@ export type PropsType = {
) => unknown;
file?: File;
i18n: LocalizerType;
isSending: boolean;
linkPreview?: LinkPreviewType;
onClose: () => unknown;
onSend: (
@ -78,6 +79,7 @@ export const StoryCreator = ({
hasFirstStoryPostExperience,
i18n,
installedPacks,
isSending,
linkPreview,
me,
onClose,
@ -162,7 +164,6 @@ export const StoryCreator = ({
onSend={(listIds, groupIds) => {
onSend(listIds, groupIds, draftAttachment);
setDraftAttachment(undefined);
onClose();
}}
onViewersUpdated={onViewersUpdated}
setMyStoriesToAllSignalConnections={
@ -179,6 +180,7 @@ export const StoryCreator = ({
i18n={i18n}
imageSrc={attachmentUrl}
installedPacks={installedPacks}
isSending={isSending}
onClose={onClose}
supportsCaption
renderCompositionTextArea={renderCompositionTextArea}
@ -197,6 +199,7 @@ export const StoryCreator = ({
<TextStoryCreator
debouncedMaybeGrabLinkPreview={debouncedMaybeGrabLinkPreview}
i18n={i18n}
isSending={isSending}
linkPreview={linkPreview}
onClose={onClose}
onDone={textAttachment => {

View file

@ -29,6 +29,7 @@ import {
import { objectMap } from '../util/objectMap';
import { handleOutsideClick } from '../util/handleOutsideClick';
import { ConfirmDiscardDialog } from './ConfirmDiscardDialog';
import { Spinner } from './Spinner';
export type PropsType = {
debouncedMaybeGrabLinkPreview: (
@ -37,6 +38,7 @@ export type PropsType = {
options?: MaybeGrabLinkPreviewOptionsType
) => unknown;
i18n: LocalizerType;
isSending: boolean;
linkPreview?: LinkPreviewType;
onClose: () => unknown;
onDone: (textAttachment: TextAttachmentType) => unknown;
@ -122,6 +124,7 @@ function getBgButtonAriaLabel(
export const TextStoryCreator = ({
debouncedMaybeGrabLinkPreview,
i18n,
isSending,
linkPreview,
onClose,
onDone,
@ -566,12 +569,16 @@ export const TextStoryCreator = ({
)}
</div>
<Button
disabled={!hasChanges}
disabled={!hasChanges || isSending}
onClick={() => onDone(textAttachment)}
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{i18n('StoryCreator__next')}
{isSending ? (
<Spinner svgSize="small" />
) : (
i18n('StoryCreator__next')
)}
</Button>
</div>
</div>

View file

@ -3,6 +3,8 @@
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
import { isEqual, pick } from 'lodash';
import * as Errors from '../../types/errors';
import type { AttachmentType } from '../../types/Attachment';
import type { BodyRangeType } from '../../types/Util';
import type { ConversationModel } from '../../models/conversations';
@ -87,6 +89,18 @@ export type SelectedStoryDataType = {
viewTarget?: StoryViewTargetType;
};
export type AddStoryData =
| {
type: 'Media';
file: File;
sending?: boolean;
}
| {
type: 'Text';
sending?: boolean;
}
| undefined;
// State
export type StoriesStateType = {
@ -97,6 +111,7 @@ export type StoriesStateType = {
replies: Array<MessageAttributesType>;
};
readonly selectedStoryData?: SelectedStoryDataType;
readonly addStoryData: AddStoryData;
readonly sendStoryModalData?: {
untrustedUuids: Array<string>;
verifiedUuids: Array<string>;
@ -117,6 +132,8 @@ const STORY_CHANGED = 'stories/STORY_CHANGED';
const TOGGLE_VIEW = 'stories/TOGGLE_VIEW';
const VIEW_STORY = 'stories/VIEW_STORY';
const REMOVE_ALL_STORIES = 'stories/REMOVE_ALL_STORIES';
const SET_ADD_STORY_DATA = 'stories/SET_ADD_STORY_DATA';
const SET_STORY_SENDING = 'stories/SET_STORY_SENDING';
type DOEStoryActionType = {
type: typeof DOE_STORY;
@ -175,6 +192,16 @@ type RemoveAllStoriesActionType = {
type: typeof REMOVE_ALL_STORIES;
};
type SetAddStoryDataType = {
type: typeof SET_ADD_STORY_DATA;
payload: AddStoryData;
};
type SetStorySendingType = {
type: typeof SET_STORY_SENDING;
payload: boolean;
};
export type StoriesActionType =
| DOEStoryActionType
| ListMembersVerified
@ -188,7 +215,9 @@ export type StoriesActionType =
| StoryChangedActionType
| ToggleViewActionType
| ViewStoryActionType
| RemoveAllStoriesActionType;
| RemoveAllStoriesActionType
| SetAddStoryDataType
| SetStorySendingType;
// Action Creators
@ -451,10 +480,22 @@ function sendStoryMessage(
listIds: Array<UUIDStringType>,
conversationIds: Array<string>,
attachment: AttachmentType
): ThunkAction<void, RootStateType, unknown, SendStoryModalOpenStateChanged> {
): ThunkAction<
void,
RootStateType,
unknown,
SendStoryModalOpenStateChanged | SetStorySendingType | SetAddStoryDataType
> {
return async (dispatch, getState) => {
const { stories } = getState();
const { openedAtTimestamp, sendStoryModalData } = stories;
// Add spinners in the story creator
dispatch({
type: SET_STORY_SENDING,
payload: true,
});
assertDev(
openedAtTimestamp,
'sendStoryMessage: openedAtTimestamp is undefined, cannot send'
@ -464,11 +505,6 @@ function sendStoryMessage(
'sendStoryMessage: sendStoryModalData is not defined, cannot send'
);
dispatch({
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
payload: undefined,
});
if (sendStoryModalData.untrustedUuids.length) {
log.info('sendStoryMessage: SN changed for some conversations');
@ -491,12 +527,39 @@ function sendStoryMessage(
);
if (!result) {
log.info('sendStoryMessage: did not send');
log.info('sendStoryMessage: failed to verify untrusted; stopping send');
dispatch({
type: SET_STORY_SENDING,
payload: false,
});
return;
}
// Clear all untrusted and verified uuids; we're clear to send!
dispatch({
type: SEND_STORY_MODAL_OPEN_STATE_CHANGED,
payload: undefined,
});
}
await doSendStoryMessage(listIds, conversationIds, attachment);
try {
await doSendStoryMessage(listIds, conversationIds, attachment);
// Note: Only when we've successfully queued the message do we dismiss the story
// composer view.
dispatch({
type: SET_ADD_STORY_DATA,
payload: undefined,
});
} catch (error) {
log.error('sendStoryMessage:', Errors.toLogFormat(error));
// Get rid of spinners in the story creator
dispatch({
type: SET_STORY_SENDING,
payload: false,
});
}
};
}
@ -1111,6 +1174,20 @@ const viewStory: ViewStoryActionCreatorType = (
};
};
function setAddStoryData(addStoryData: AddStoryData): SetAddStoryDataType {
return {
type: SET_ADD_STORY_DATA,
payload: addStoryData,
};
}
function setStorySending(sending: boolean): SetStorySendingType {
return {
type: SET_STORY_SENDING,
payload: sending,
};
}
function setStoriesDisabled(
value: boolean
): ThunkAction<void, RootStateType, unknown, never> {
@ -1134,7 +1211,9 @@ export const actions = {
verifyStoryListMembers,
viewUserStories,
viewStory,
setAddStoryData,
setStoriesDisabled,
setStorySending,
};
export const useStoriesActions = (): typeof actions => useBoundActions(actions);
@ -1147,6 +1226,7 @@ export function getEmptyState(
return {
lastOpenedAtTimestamp: undefined,
openedAtTimestamp: undefined,
addStoryData: undefined,
stories: [],
...overrideState,
};
@ -1475,5 +1555,31 @@ export function reducer(
};
}
if (action.type === SET_ADD_STORY_DATA) {
return {
...state,
addStoryData: action.payload,
};
}
if (action.type === SET_STORY_SENDING) {
const existing = state.addStoryData;
if (!existing) {
log.warn(
'stories/reducer: Set story sending, but no existing addStoryData'
);
return state;
}
return {
...state,
addStoryData: {
...existing,
sending: action.payload,
},
};
}
return state;
}

View file

@ -21,6 +21,7 @@ import type {
SelectedStoryDataType,
StoryDataType,
StoriesStateType,
AddStoryData,
} from '../ducks/stories';
import { HasStories, MY_STORIES_ID } from '../../types/Stories';
import { ReadStatus } from '../../messages/MessageReadStatus';
@ -58,6 +59,11 @@ export const getSelectedStoryData = createSelector(
selectedStoryData
);
export const getAddStoryData = createSelector(
getStoriesState,
({ addStoryData }): AddStoryData => addStoryData
);
function getReactionUniqueId(reaction: MessageReactionType): string {
return `${reaction.fromId}:${reaction.targetAuthorUuid}:${reaction.timestamp}`;
}

View file

@ -6,7 +6,6 @@ import { useSelector } from 'react-redux';
import type { LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer';
import type { PropsType as SmartStoryCreatorPropsType } from './StoryCreator';
import { SmartStoryCreator } from './StoryCreator';
import { Stories } from '../../components/Stories';
import { getMe } from '../selectors/conversations';
@ -17,6 +16,7 @@ import {
getPreferredLeftPaneWidth,
} from '../selectors/items';
import {
getAddStoryData,
getSelectedStoryData,
getStories,
shouldShowStoriesView,
@ -27,11 +27,8 @@ import { useGlobalModalActions } from '../ducks/globalModals';
import { useStoriesActions } from '../ducks/stories';
import { useToastActions } from '../ducks/toast';
function renderStoryCreator({
file,
onClose,
}: SmartStoryCreatorPropsType): JSX.Element {
return <SmartStoryCreator file={file} onClose={onClose} />;
function renderStoryCreator(): JSX.Element {
return <SmartStoryCreator />;
}
export function SmartStories(): JSX.Element | null {
@ -52,6 +49,7 @@ export function SmartStories(): JSX.Element | null {
);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const addStoryData = useSelector(getAddStoryData);
const { hiddenStories, myStories, stories } = useSelector(getStories);
const me = useSelector(getMe);
@ -70,6 +68,7 @@ export function SmartStories(): JSX.Element | null {
return (
<Stories
addStoryData={addStoryData}
getPreferredBadge={getPreferredBadge}
hiddenStories={hiddenStories}
i18n={i18n}

View file

@ -31,21 +31,20 @@ import { useLinkPreviewActions } from '../ducks/linkPreviews';
import { useStoriesActions } from '../ducks/stories';
import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists';
import { SmartCompositionTextArea } from './CompositionTextArea';
import { getAddStoryData } from '../selectors/stories';
export type PropsType = {
file?: File;
onClose: () => unknown;
};
export function SmartStoryCreator({
file,
onClose,
}: PropsType): JSX.Element | null {
export function SmartStoryCreator(): JSX.Element | null {
const { debouncedMaybeGrabLinkPreview } = useLinkPreviewActions();
const {
sendStoryModalOpenStateChanged,
sendStoryMessage,
verifyStoryListMembers,
setAddStoryData,
} = useStoriesActions();
const { toggleGroupsForStorySend } = useConversationsActions();
const {
@ -72,6 +71,10 @@ export function SmartStoryCreator({
const recentStickers = useSelector(getRecentStickers);
const signalConnections = useSelector(getAllSignalConnections);
const addStoryData = useSelector(getAddStoryData);
const file = addStoryData?.type === 'Media' ? addStoryData.file : undefined;
const isSending = addStoryData?.sending || false;
return (
<StoryCreator
candidateConversations={candidateConversations}
@ -84,9 +87,10 @@ export function SmartStoryCreator({
hasFirstStoryPostExperience={!hasSetMyStoriesPrivacy}
i18n={i18n}
installedPacks={installedPacks}
isSending={isSending}
linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)}
me={me}
onClose={onClose}
onClose={() => setAddStoryData(undefined)}
onDeleteList={deleteDistributionList}
onDistributionListCreated={createDistributionList}
onHideMyStoriesFrom={hideMyStoriesFrom}