Send story images/video

This commit is contained in:
Josh Perez 2022-08-04 15:23:24 -04:00 committed by GitHub
parent fcf7406dd4
commit 7bc6bbc668
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 807 additions and 567 deletions

View file

@ -7179,6 +7179,14 @@
"message": "Add a story", "message": "Add a story",
"description": "Description hint to add a story" "description": "Description hint to add a story"
}, },
"Stories__add-story--text": {
"message": "Text story",
"description": "Label to create a new text story"
},
"Stories__add-story--media": {
"message": "Photo or video",
"description": "Label to create a new multimedia story"
},
"Stories__hidden-stories": { "Stories__hidden-stories": {
"message": "Hidden stories", "message": "Hidden stories",
"description": "Button label to go to hidden stories pane" "description": "Button label to go to hidden stories pane"
@ -7495,8 +7503,16 @@
"message": "Cant download story. You will need to share it again.", "message": "Cant download story. You will need to share it again.",
"description": "Description for image errors but when it is your own image" "description": "Description for image errors but when it is your own image"
}, },
"StoryCreator__text-bg": { "StoryCreator__text-bg--background": {
"message": "Toggle text background color", "message": "Text has a white background color",
"description": "Button label"
},
"StoryCreator__text-bg--inverse": {
"message": "Text has selected color as the background color",
"description": "Button label"
},
"StoryCreator__text-bg--none": {
"message": "Text has no background color",
"description": "Button label" "description": "Button label"
}, },
"StoryCreator__story-bg": { "StoryCreator__story-bg": {

View file

@ -21,6 +21,15 @@
width: 380px; width: 380px;
padding-top: calc(14px + var(--title-bar-drag-area-height)); padding-top: calc(14px + var(--title-bar-drag-area-height));
&__add-story__button {
@include color-svg('../images/icons/v2/plus-24.svg', $color-white);
height: 22px;
position: absolute;
right: 63px;
top: 0px;
width: 22px;
}
&__settings__button { &__settings__button {
@include dark-theme { @include dark-theme {
@include color-svg( @include color-svg(
@ -61,18 +70,6 @@
width: 100%; width: 100%;
} }
&--camera {
@include button-reset;
@include color-svg(
'../images/icons/v2/camera-outline-24.svg',
$color-white
);
height: 22px;
position: absolute;
right: 63px;
width: 22px;
}
&--back { &--back {
@include button-reset; @include button-reset;

View file

@ -36,7 +36,7 @@ function getListViewers(
} }
return memberCount === 1 return memberCount === 1
? i18n('StoriesSettingsModal__list__viewers--singular', ['1']) ? i18n('StoriesSettingsModal__viewers--singular', ['1'])
: i18n('StoriesSettings__viewers--plural', [String(memberCount)]); : i18n('StoriesSettings__viewers--plural', [String(memberCount)]);
} }

View file

@ -1,7 +1,6 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import FocusTrap from 'focus-trap-react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import type { import type {
@ -43,6 +42,14 @@ export type PropsType = {
viewStory: ViewStoryActionCreatorType; viewStory: ViewStoryActionCreatorType;
}; };
type AddStoryType =
| {
type: 'Media';
file: File;
}
| { type: 'Text' }
| undefined;
export const Stories = ({ export const Stories = ({
deleteStoryForEveryone, deleteStoryForEveryone,
getPreferredBadge, getPreferredBadge,
@ -67,16 +74,16 @@ export const Stories = ({
requiresFullWidth: true, requiresFullWidth: true,
}); });
const [isShowingStoryCreator, setIsShowingStoryCreator] = useState(false); const [addStoryData, setAddStoryData] = useState<AddStoryType>();
const [isMyStories, setIsMyStories] = useState(false); const [isMyStories, setIsMyStories] = useState(false);
return ( return (
<div className={classNames('Stories', themeClassName(Theme.Dark))}> <div className={classNames('Stories', themeClassName(Theme.Dark))}>
{isShowingStoryCreator && {addStoryData &&
renderStoryCreator({ renderStoryCreator({
onClose: () => setIsShowingStoryCreator(false), file: addStoryData.type === 'Media' ? addStoryData.file : undefined,
onClose: () => setAddStoryData(undefined),
})} })}
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
<div className="Stories__pane" style={{ width }}> <div className="Stories__pane" style={{ width }}>
{isMyStories && myStories.length ? ( {isMyStories && myStories.length ? (
<MyStories <MyStories
@ -96,12 +103,16 @@ export const Stories = ({
i18n={i18n} i18n={i18n}
me={me} me={me}
myStories={myStories} myStories={myStories}
onAddStory={() => setIsShowingStoryCreator(true)} onAddStory={file =>
file
? setAddStoryData({ type: 'Media', file })
: setAddStoryData({ type: 'Text' })
}
onMyStoriesClicked={() => { onMyStoriesClicked={() => {
if (myStories.length) { if (myStories.length) {
setIsMyStories(true); setIsMyStories(true);
} else { } else {
setIsShowingStoryCreator(true); setAddStoryData({ type: 'Text' });
} }
}} }}
onStoriesSettings={showStoriesSettings} onStoriesSettings={showStoriesSettings}
@ -114,7 +125,6 @@ export const Stories = ({
/> />
)} )}
</div> </div>
</FocusTrap>
<div className="Stories__placeholder"> <div className="Stories__placeholder">
<div className="Stories__placeholder__stories" /> <div className="Stories__placeholder__stories" />
{i18n('Stories__placeholder--text')} {i18n('Stories__placeholder--text')}

View file

@ -65,7 +65,7 @@ export type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
me: ConversationType; me: ConversationType;
myStories: Array<MyStoryType>; myStories: Array<MyStoryType>;
onAddStory: () => unknown; onAddStory: (file?: File) => unknown;
onMyStoriesClicked: () => unknown; onMyStoriesClicked: () => unknown;
onStoriesSettings: () => unknown; onStoriesSettings: () => unknown;
queueStoryDownload: (storyId: string) => unknown; queueStoryDownload: (storyId: string) => unknown;
@ -118,18 +118,45 @@ export const StoriesPane = ({
<div className="Stories__pane__header--title"> <div className="Stories__pane__header--title">
{i18n('Stories__title')} {i18n('Stories__title')}
</div> </div>
<button <ContextMenu
aria-label={i18n('Stories__add')} i18n={i18n}
className="Stories__pane__header--camera" menuOptions={[
onClick={onAddStory} {
type="button" label: i18n('Stories__add-story--media'),
onClick: () => {
const input = document.createElement('input');
input.accept = 'image/*,video/*';
input.type = 'file';
input.onchange = () => {
const file = input.files ? input.files[0] : undefined;
if (!file) {
return;
}
onAddStory(file);
};
input.click();
},
},
{
label: i18n('Stories__add-story--text'),
onClick: () => onAddStory(),
},
]}
moduleClassName="Stories__pane__add-story"
popperOptions={{
placement: 'bottom',
strategy: 'absolute',
}}
theme={Theme.Dark}
/> />
<ContextMenu <ContextMenu
i18n={i18n} i18n={i18n}
menuOptions={[ menuOptions={[
{ {
onClick: () => onStoriesSettings(),
label: i18n('StoriesSettings__context-menu'), label: i18n('StoriesSettings__context-menu'),
onClick: () => onStoriesSettings(),
}, },
]} ]}
moduleClassName="Stories__pane__settings" moduleClassName="Stories__pane__settings"

View file

@ -599,7 +599,7 @@ export const StoriesSettingsModal = ({
<span className="StoriesSettingsModal__list__viewers"> <span className="StoriesSettingsModal__list__viewers">
{list.members.length === 1 {list.members.length === 1
? i18n('StoriesSettingsModal__list__viewers--singular', ['1']) ? i18n('StoriesSettings__viewers--singular', ['1'])
: i18n('StoriesSettings__viewers--plural', [ : i18n('StoriesSettings__viewers--plural', [
String(list.members.length), String(list.members.length),
])} ])}

View file

@ -24,11 +24,18 @@ export default {
defaultValue: undefined, defaultValue: undefined,
}, },
i18n: { defaultValue: i18n }, i18n: { defaultValue: i18n },
installedPacks: {
defaultValue: [],
},
me: { me: {
defaultValue: getDefaultConversation(), defaultValue: getDefaultConversation(),
}, },
onClose: { action: true }, onClose: { action: true },
onSend: { action: true }, onSend: { action: true },
processAttachment: { action: true },
recentStickers: {
defaultValue: [],
},
signalConnections: { signalConnections: {
defaultValue: Array.from(Array(42), getDefaultConversation), defaultValue: Array.from(Array(42), getDefaultConversation),
}, },

View file

@ -1,35 +1,27 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import FocusTrap from 'focus-trap-react'; import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { get, has } from 'lodash'; import { get, has } from 'lodash';
import { usePopper } from 'react-popper';
import type {
AttachmentType,
InMemoryAttachmentDraftType,
} from '../types/Attachment';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { LinkPreviewSourceType } from '../types/LinkPreview';
import type { LinkPreviewType } from '../types/message/LinkPreviews'; import type { LinkPreviewType } from '../types/message/LinkPreviews';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import type { Props as StickerButtonProps } from './stickers/StickerButton';
import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists'; import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists';
import type { TextAttachmentType } from '../types/Attachment';
import type { UUIDStringType } from '../types/UUID'; import type { UUIDStringType } from '../types/UUID';
import { Button, ButtonVariant } from './Button'; import { IMAGE_JPEG, TEXT_ATTACHMENT } from '../types/MIME';
import { ContextMenu } from './ContextMenu'; import { isVideoAttachment } from '../types/Attachment';
import { LinkPreviewSourceType, findLinks } from '../types/LinkPreview';
import { Input } from './Input';
import { Slider } from './Slider';
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
import { SendStoryModal } from './SendStoryModal'; import { SendStoryModal } from './SendStoryModal';
import { TextAttachment } from './TextAttachment';
import { Theme, themeClassName } from '../util/theme'; import { MediaEditor } from './MediaEditor';
import { getRGBA, getRGBANumber } from '../mediaEditor/util/color'; import { TextStoryCreator } from './TextStoryCreator';
import {
COLOR_BLACK_INT,
COLOR_WHITE_INT,
getBackgroundColor,
} from '../util/getStoryBackground';
import { objectMap } from '../util/objectMap';
export type PropsType = { export type PropsType = {
debouncedMaybeGrabLinkPreview: ( debouncedMaybeGrabLinkPreview: (
@ -37,495 +29,119 @@ export type PropsType = {
source: LinkPreviewSourceType source: LinkPreviewSourceType
) => unknown; ) => unknown;
distributionLists: Array<StoryDistributionListDataType>; distributionLists: Array<StoryDistributionListDataType>;
file?: File;
i18n: LocalizerType; i18n: LocalizerType;
linkPreview?: LinkPreviewType; linkPreview?: LinkPreviewType;
me: ConversationType; me: ConversationType;
onClose: () => unknown; onClose: () => unknown;
onSend: ( onSend: (
listIds: Array<UUIDStringType>, listIds: Array<UUIDStringType>,
textAttachment: TextAttachmentType attachment: AttachmentType
) => unknown; ) => unknown;
processAttachment: (
file: File
) => Promise<void | InMemoryAttachmentDraftType>;
signalConnections: Array<ConversationType>; signalConnections: Array<ConversationType>;
}; } & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'>;
enum TextStyle {
Default,
Regular,
Bold,
Serif,
Script,
Condensed,
}
enum TextBackground {
None,
Background,
Inverse,
}
const BackgroundStyle = {
BG1099: { angle: 191, endColor: 4282529679, startColor: 4294260804 },
BG1098: { startColor: 4293938406, endColor: 4279119837, angle: 192 },
BG1031: { startColor: 4294950980, endColor: 4294859832, angle: 175 },
BG1101: { startColor: 4278227945, endColor: 4286632135, angle: 180 },
BG1100: { startColor: 4284861868, endColor: 4278884698, angle: 180 },
BG1070: { color: 4294951251 },
BG1080: { color: 4291607859 },
BG1079: { color: 4286869806 },
BG1083: { color: 4278825851 },
BG1095: { color: 4287335417 },
BG1088: { color: 4283519478 },
BG1077: { color: 4294405742 },
BG1094: { color: 4291315265 },
BG1097: { color: 4291216549 },
BG1074: { color: 4288976277 },
BG1092: { color: 4280887593 },
};
type BackgroundStyleType = typeof BackgroundStyle[keyof typeof BackgroundStyle];
function getBackground(
bgStyle: BackgroundStyleType
): Pick<TextAttachmentType, 'color' | 'gradient'> {
if (has(bgStyle, 'color')) {
return { color: get(bgStyle, 'color') };
}
const angle = get(bgStyle, 'angle');
const startColor = get(bgStyle, 'startColor');
const endColor = get(bgStyle, 'endColor');
return {
gradient: { angle, startColor, endColor },
};
}
export const StoryCreator = ({ export const StoryCreator = ({
debouncedMaybeGrabLinkPreview, debouncedMaybeGrabLinkPreview,
distributionLists, distributionLists,
file,
i18n, i18n,
installedPacks,
linkPreview, linkPreview,
me, me,
onClose, onClose,
onSend, onSend,
processAttachment,
recentStickers,
signalConnections, signalConnections,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [isEditingText, setIsEditingText] = useState(false); const [draftAttachment, setDraftAttachment] = useState<
const [selectedBackground, setSelectedBackground] = AttachmentType | undefined
useState<BackgroundStyleType>(BackgroundStyle.BG1099); >();
const [textStyle, setTextStyle] = useState<TextStyle>(TextStyle.Regular); const [attachmentUrl, setAttachmentUrl] = useState<string | undefined>();
const [textBackground, setTextBackground] = useState<TextBackground>(
TextBackground.None
);
const [sliderValue, setSliderValue] = useState<number>(100);
const [text, setText] = useState<string>('');
const [hasSendToModal, setHasSendToModal] = useState(false);
const textEditorRef = useRef<HTMLInputElement | null>(null);
useEffect(() => { useEffect(() => {
if (isEditingText) { let url: string | undefined;
textEditorRef.current?.focus(); let unmounted = false;
} else {
textEditorRef.current?.blur();
}
}, [isEditingText]);
const [isColorPickerShowing, setIsColorPickerShowing] = useState(false); async function loadAttachment(): Promise<void> {
const [colorPickerPopperButtonRef, setColorPickerPopperButtonRef] = if (!file || unmounted) {
useState<HTMLButtonElement | null>(null);
const [colorPickerPopperRef, setColorPickerPopperRef] =
useState<HTMLDivElement | null>(null);
const colorPickerPopper = usePopper(
colorPickerPopperButtonRef,
colorPickerPopperRef,
{
modifiers: [
{
name: 'arrow',
},
],
placement: 'top',
strategy: 'fixed',
}
);
const [hasLinkPreviewApplied, setHasLinkPreviewApplied] = useState(false);
const [linkPreviewInputValue, setLinkPreviewInputValue] = useState('');
useEffect(() => {
if (!linkPreviewInputValue) {
return;
}
debouncedMaybeGrabLinkPreview(
linkPreviewInputValue,
LinkPreviewSourceType.StoryCreator
);
}, [debouncedMaybeGrabLinkPreview, linkPreviewInputValue]);
useEffect(() => {
if (!text) {
return;
}
debouncedMaybeGrabLinkPreview(text, LinkPreviewSourceType.StoryCreator);
}, [debouncedMaybeGrabLinkPreview, text]);
useEffect(() => {
if (!linkPreview || !text) {
return; return;
} }
const links = findLinks(text); const attachment = await processAttachment(file);
if (!attachment || unmounted) {
const shouldApplyLinkPreview = links.includes(linkPreview.url); return;
setHasLinkPreviewApplied(shouldApplyLinkPreview);
}, [linkPreview, text]);
const [isLinkPreviewInputShowing, setIsLinkPreviewInputShowing] =
useState(false);
const [linkPreviewInputPopperButtonRef, setLinkPreviewInputPopperButtonRef] =
useState<HTMLButtonElement | null>(null);
const [linkPreviewInputPopperRef, setLinkPreviewInputPopperRef] =
useState<HTMLDivElement | null>(null);
const linkPreviewInputPopper = usePopper(
linkPreviewInputPopperButtonRef,
linkPreviewInputPopperRef,
{
modifiers: [
{
name: 'arrow',
},
],
placement: 'top',
strategy: 'fixed',
} }
);
useEffect(() => { if (isVideoAttachment(attachment)) {
const handleOutsideClick = (event: MouseEvent) => { setDraftAttachment(attachment);
if (!colorPickerPopperButtonRef?.contains(event.target as Node)) { } else if (attachment && has(attachment, 'data')) {
setIsColorPickerShowing(false); url = URL.createObjectURL(new Blob([get(attachment, 'data')]));
event.stopPropagation(); setAttachmentUrl(url);
event.preventDefault();
} }
};
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsColorPickerShowing(false);
event.preventDefault();
event.stopPropagation();
} }
};
document.addEventListener('click', handleOutsideClick); loadAttachment();
document.addEventListener('keydown', handleEscape);
return () => { return () => {
document.removeEventListener('click', handleOutsideClick); unmounted = true;
document.removeEventListener('keydown', handleEscape); if (url) {
}; URL.revokeObjectURL(url);
}, [isColorPickerShowing, colorPickerPopperButtonRef]);
const sliderColorNumber = getRGBANumber(sliderValue);
let textForegroundColor = sliderColorNumber;
let textBackgroundColor: number | undefined;
if (textBackground === TextBackground.Background) {
textBackgroundColor = COLOR_WHITE_INT;
textForegroundColor =
sliderValue >= 95 ? COLOR_BLACK_INT : sliderColorNumber;
} else if (textBackground === TextBackground.Inverse) {
textBackgroundColor =
sliderValue >= 95 ? COLOR_BLACK_INT : sliderColorNumber;
textForegroundColor = COLOR_WHITE_INT;
} }
const textAttachment: TextAttachmentType = {
...getBackground(selectedBackground),
text,
textStyle,
textForegroundColor,
textBackgroundColor,
preview: hasLinkPreviewApplied ? linkPreview : undefined,
}; };
}, [file, processAttachment]);
const hasChanges = Boolean(text || hasLinkPreviewApplied);
return ( return (
<> <>
{hasSendToModal && ( {draftAttachment && (
<SendStoryModal <SendStoryModal
distributionLists={distributionLists} distributionLists={distributionLists}
i18n={i18n} i18n={i18n}
me={me} me={me}
onClose={() => setHasSendToModal(false)} onClose={() => setDraftAttachment(undefined)}
onSend={listIds => { onSend={listIds => {
onSend(listIds, textAttachment); onSend(listIds, draftAttachment);
setHasSendToModal(false); setDraftAttachment(undefined);
onClose(); onClose();
}} }}
signalConnections={signalConnections} signalConnections={signalConnections}
/> />
)} )}
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}> {attachmentUrl && (
<div className="StoryCreator"> <MediaEditor
<div className="StoryCreator__container">
<TextAttachment
disableLinkPreviewPopup
i18n={i18n} i18n={i18n}
isEditingText={isEditingText} imageSrc={attachmentUrl}
onChange={setText} installedPacks={installedPacks}
onClick={() => { onClose={onClose}
if (!isEditingText) { onDone={data => {
setIsEditingText(true); setDraftAttachment({
} contentType: IMAGE_JPEG,
data,
size: data.byteLength,
});
}} }}
onRemoveLinkPreview={() => { recentStickers={recentStickers}
setHasLinkPreviewApplied(false);
}}
textAttachment={textAttachment}
/> />
</div> )}
<div className="StoryCreator__toolbar"> {!file && (
{isEditingText ? ( <TextStoryCreator
<div className="StoryCreator__tools"> debouncedMaybeGrabLinkPreview={debouncedMaybeGrabLinkPreview}
<Slider
handleStyle={{ backgroundColor: getRGBA(sliderValue) }}
label={i18n('CustomColorEditor__hue')}
moduleClassName="HueSlider StoryCreator__tools__tool"
onChange={setSliderValue}
value={sliderValue}
/>
<ContextMenu
i18n={i18n} i18n={i18n}
menuOptions={[ linkPreview={linkPreview}
{ onClose={onClose}
icon: 'StoryCreator__icon--font-regular', onDone={textAttachment => {
label: i18n('StoryCreator__text--regular'), setDraftAttachment({
onClick: () => setTextStyle(TextStyle.Regular), contentType: TEXT_ATTACHMENT,
value: TextStyle.Regular, textAttachment,
}, size: textAttachment.text?.length || 0,
{ });
icon: 'StoryCreator__icon--font-bold',
label: i18n('StoryCreator__text--bold'),
onClick: () => setTextStyle(TextStyle.Bold),
value: TextStyle.Bold,
},
{
icon: 'StoryCreator__icon--font-serif',
label: i18n('StoryCreator__text--serif'),
onClick: () => setTextStyle(TextStyle.Serif),
value: TextStyle.Serif,
},
{
icon: 'StoryCreator__icon--font-script',
label: i18n('StoryCreator__text--script'),
onClick: () => setTextStyle(TextStyle.Script),
value: TextStyle.Script,
},
{
icon: 'StoryCreator__icon--font-condensed',
label: i18n('StoryCreator__text--condensed'),
onClick: () => setTextStyle(TextStyle.Condensed),
value: TextStyle.Condensed,
},
]}
moduleClassName={classNames('StoryCreator__tools__tool', {
'StoryCreator__tools__button--font-regular':
textStyle === TextStyle.Regular,
'StoryCreator__tools__button--font-bold':
textStyle === TextStyle.Bold,
'StoryCreator__tools__button--font-serif':
textStyle === TextStyle.Serif,
'StoryCreator__tools__button--font-script':
textStyle === TextStyle.Script,
'StoryCreator__tools__button--font-condensed':
textStyle === TextStyle.Condensed,
})}
theme={Theme.Dark}
value={textStyle}
/>
<button
aria-label={i18n('StoryCreator__text-bg')}
className={classNames('StoryCreator__tools__tool', {
'StoryCreator__tools__button--bg-none':
textBackground === TextBackground.None,
'StoryCreator__tools__button--bg':
textBackground === TextBackground.Background,
'StoryCreator__tools__button--bg-inverse':
textBackground === TextBackground.Inverse,
})}
onClick={() => {
if (textBackground === TextBackground.None) {
setTextBackground(TextBackground.Background);
} else if (textBackground === TextBackground.Background) {
setTextBackground(TextBackground.Inverse);
} else {
setTextBackground(TextBackground.None);
}
}}
type="button"
/>
</div>
) : (
<div className="StoryCreator__toolbar--space" />
)}
<div className="StoryCreator__toolbar--buttons">
<Button
onClick={onClose}
theme={Theme.Dark}
variant={ButtonVariant.Secondary}
>
{i18n('discard')}
</Button>
<div className="StoryCreator__controls">
<button
aria-label={i18n('StoryCreator__story-bg')}
className={classNames({
StoryCreator__control: true,
'StoryCreator__control--bg': true,
'StoryCreator__control--bg--selected': isColorPickerShowing,
})}
onClick={() => setIsColorPickerShowing(!isColorPickerShowing)}
ref={setColorPickerPopperButtonRef}
style={{
background: getBackgroundColor(
getBackground(selectedBackground)
),
}}
type="button"
/>
{isColorPickerShowing && (
<div
className="StoryCreator__popper"
ref={setColorPickerPopperRef}
style={colorPickerPopper.styles.popper}
{...colorPickerPopper.attributes.popper}
>
<div
data-popper-arrow
className="StoryCreator__popper__arrow"
/>
{objectMap<BackgroundStyleType>(
BackgroundStyle,
(bg, backgroundValue) => (
<button
aria-label={i18n('StoryCreator__story-bg')}
className={classNames({
StoryCreator__bg: true,
'StoryCreator__bg--selected':
selectedBackground === backgroundValue,
})}
key={String(bg)}
onClick={() => {
setSelectedBackground(backgroundValue);
setIsColorPickerShowing(false);
}}
type="button"
style={{
background: getBackgroundColor(
getBackground(backgroundValue)
),
}} }}
/> />
)
)} )}
</div>
)}
<button
aria-label={i18n('StoryCreator__control--text')}
className={classNames({
StoryCreator__control: true,
'StoryCreator__control--text': true,
'StoryCreator__control--selected': isEditingText,
})}
onClick={() => {
setIsEditingText(!isEditingText);
}}
type="button"
/>
<button
aria-label={i18n('StoryCreator__control--link')}
className="StoryCreator__control StoryCreator__control--link"
onClick={() =>
setIsLinkPreviewInputShowing(!isLinkPreviewInputShowing)
}
ref={setLinkPreviewInputPopperButtonRef}
type="button"
/>
{isLinkPreviewInputShowing && (
<div
className={classNames(
'StoryCreator__popper StoryCreator__link-preview-input-popper',
themeClassName(Theme.Dark)
)}
ref={setLinkPreviewInputPopperRef}
style={linkPreviewInputPopper.styles.popper}
{...linkPreviewInputPopper.attributes.popper}
>
<div
data-popper-arrow
className="StoryCreator__popper__arrow"
/>
<Input
disableSpellcheck
i18n={i18n}
moduleClassName="StoryCreator__link-preview-input"
onChange={setLinkPreviewInputValue}
placeholder={i18n(
'StoryCreator__link-preview-placeholder'
)}
ref={el => el?.focus()}
value={linkPreviewInputValue}
/>
<div className="StoryCreator__link-preview-container">
{linkPreview ? (
<>
<StagedLinkPreview
domain={linkPreview.domain}
i18n={i18n}
image={linkPreview.image}
moduleClassName="StoryCreator__link-preview"
title={linkPreview.title}
url={linkPreview.url}
/>
<Button
className="StoryCreator__link-preview-button"
onClick={() => {
setHasLinkPreviewApplied(true);
setIsLinkPreviewInputShowing(false);
}}
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{i18n('StoryCreator__add-link')}
</Button>
</>
) : (
<div className="StoryCreator__link-preview-empty">
<div className="StoryCreator__link-preview-empty__icon" />
{i18n('StoryCreator__link-preview-empty')}
</div>
)}
</div>
</div>
)}
</div>
<Button
disabled={!hasChanges}
onClick={() => setHasSendToModal(true)}
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{i18n('StoryCreator__next')}
</Button>
</div>
</div>
</div>
</FocusTrap>
</> </>
); );
}; };

View file

@ -0,0 +1,514 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import FocusTrap from 'focus-trap-react';
import React, { useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import { get, has } from 'lodash';
import { usePopper } from 'react-popper';
import type { LinkPreviewType } from '../types/message/LinkPreviews';
import type { LocalizerType } from '../types/Util';
import type { TextAttachmentType } from '../types/Attachment';
import { Button, ButtonVariant } from './Button';
import { ContextMenu } from './ContextMenu';
import { LinkPreviewSourceType, findLinks } from '../types/LinkPreview';
import { Input } from './Input';
import { Slider } from './Slider';
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
import { TextAttachment } from './TextAttachment';
import { Theme, themeClassName } from '../util/theme';
import { getRGBA, getRGBANumber } from '../mediaEditor/util/color';
import {
COLOR_BLACK_INT,
COLOR_WHITE_INT,
getBackgroundColor,
} from '../util/getStoryBackground';
import { objectMap } from '../util/objectMap';
export type PropsType = {
debouncedMaybeGrabLinkPreview: (
message: string,
source: LinkPreviewSourceType
) => unknown;
i18n: LocalizerType;
linkPreview?: LinkPreviewType;
onClose: () => unknown;
onDone: (textAttachment: TextAttachmentType) => unknown;
};
enum TextStyle {
Default,
Regular,
Bold,
Serif,
Script,
Condensed,
}
enum TextBackground {
None,
Background,
Inverse,
}
const BackgroundStyle = {
BG1099: { angle: 191, endColor: 4282529679, startColor: 4294260804 },
BG1098: { startColor: 4293938406, endColor: 4279119837, angle: 192 },
BG1031: { startColor: 4294950980, endColor: 4294859832, angle: 175 },
BG1101: { startColor: 4278227945, endColor: 4286632135, angle: 180 },
BG1100: { startColor: 4284861868, endColor: 4278884698, angle: 180 },
BG1070: { color: 4294951251 },
BG1080: { color: 4291607859 },
BG1079: { color: 4286869806 },
BG1083: { color: 4278825851 },
BG1095: { color: 4287335417 },
BG1088: { color: 4283519478 },
BG1077: { color: 4294405742 },
BG1094: { color: 4291315265 },
BG1097: { color: 4291216549 },
BG1074: { color: 4288976277 },
BG1092: { color: 4280887593 },
};
type BackgroundStyleType = typeof BackgroundStyle[keyof typeof BackgroundStyle];
function getBackground(
bgStyle: BackgroundStyleType
): Pick<TextAttachmentType, 'color' | 'gradient'> {
if (has(bgStyle, 'color')) {
return { color: get(bgStyle, 'color') };
}
const angle = get(bgStyle, 'angle');
const startColor = get(bgStyle, 'startColor');
const endColor = get(bgStyle, 'endColor');
return {
gradient: { angle, startColor, endColor },
};
}
function getBgButtonAriaLabel(
i18n: LocalizerType,
textBackground: TextBackground
): string {
if (textBackground === TextBackground.Background) {
return i18n('StoryCreator__text-bg--background');
}
if (textBackground === TextBackground.Inverse) {
return i18n('StoryCreator__text-bg--inverse');
}
return i18n('StoryCreator__text-bg--none');
}
export const TextStoryCreator = ({
debouncedMaybeGrabLinkPreview,
i18n,
linkPreview,
onClose,
onDone,
}: PropsType): JSX.Element => {
const [isEditingText, setIsEditingText] = useState(false);
const [selectedBackground, setSelectedBackground] =
useState<BackgroundStyleType>(BackgroundStyle.BG1099);
const [textStyle, setTextStyle] = useState<TextStyle>(TextStyle.Regular);
const [textBackground, setTextBackground] = useState<TextBackground>(
TextBackground.None
);
const [sliderValue, setSliderValue] = useState<number>(100);
const [text, setText] = useState<string>('');
const textEditorRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (isEditingText) {
textEditorRef.current?.focus();
} else {
textEditorRef.current?.blur();
}
}, [isEditingText]);
const [isColorPickerShowing, setIsColorPickerShowing] = useState(false);
const [colorPickerPopperButtonRef, setColorPickerPopperButtonRef] =
useState<HTMLButtonElement | null>(null);
const [colorPickerPopperRef, setColorPickerPopperRef] =
useState<HTMLDivElement | null>(null);
const colorPickerPopper = usePopper(
colorPickerPopperButtonRef,
colorPickerPopperRef,
{
modifiers: [
{
name: 'arrow',
},
],
placement: 'top',
strategy: 'fixed',
}
);
const [hasLinkPreviewApplied, setHasLinkPreviewApplied] = useState(false);
const [linkPreviewInputValue, setLinkPreviewInputValue] = useState('');
useEffect(() => {
if (!linkPreviewInputValue) {
return;
}
debouncedMaybeGrabLinkPreview(
linkPreviewInputValue,
LinkPreviewSourceType.StoryCreator
);
}, [debouncedMaybeGrabLinkPreview, linkPreviewInputValue]);
useEffect(() => {
if (!text) {
return;
}
debouncedMaybeGrabLinkPreview(text, LinkPreviewSourceType.StoryCreator);
}, [debouncedMaybeGrabLinkPreview, text]);
useEffect(() => {
if (!linkPreview || !text) {
return;
}
const links = findLinks(text);
const shouldApplyLinkPreview = links.includes(linkPreview.url);
setHasLinkPreviewApplied(shouldApplyLinkPreview);
}, [linkPreview, text]);
const [isLinkPreviewInputShowing, setIsLinkPreviewInputShowing] =
useState(false);
const [linkPreviewInputPopperButtonRef, setLinkPreviewInputPopperButtonRef] =
useState<HTMLButtonElement | null>(null);
const [linkPreviewInputPopperRef, setLinkPreviewInputPopperRef] =
useState<HTMLDivElement | null>(null);
const linkPreviewInputPopper = usePopper(
linkPreviewInputPopperButtonRef,
linkPreviewInputPopperRef,
{
modifiers: [
{
name: 'arrow',
},
],
placement: 'top',
strategy: 'fixed',
}
);
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (!colorPickerPopperButtonRef?.contains(event.target as Node)) {
setIsColorPickerShowing(false);
event.stopPropagation();
event.preventDefault();
}
};
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsColorPickerShowing(false);
event.preventDefault();
event.stopPropagation();
}
};
document.addEventListener('click', handleOutsideClick);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('click', handleOutsideClick);
document.removeEventListener('keydown', handleEscape);
};
}, [isColorPickerShowing, colorPickerPopperButtonRef]);
const sliderColorNumber = getRGBANumber(sliderValue);
let textForegroundColor = sliderColorNumber;
let textBackgroundColor: number | undefined;
if (textBackground === TextBackground.Background) {
textBackgroundColor = COLOR_WHITE_INT;
textForegroundColor =
sliderValue >= 95 ? COLOR_BLACK_INT : sliderColorNumber;
} else if (textBackground === TextBackground.Inverse) {
textBackgroundColor =
sliderValue >= 95 ? COLOR_BLACK_INT : sliderColorNumber;
textForegroundColor = COLOR_WHITE_INT;
}
const textAttachment: TextAttachmentType = {
...getBackground(selectedBackground),
text,
textStyle,
textForegroundColor,
textBackgroundColor,
preview: hasLinkPreviewApplied ? linkPreview : undefined,
};
const hasChanges = Boolean(text || hasLinkPreviewApplied);
return (
<FocusTrap focusTrapOptions={{ allowOutsideClick: true }}>
<div className="StoryCreator">
<div className="StoryCreator__container">
<TextAttachment
disableLinkPreviewPopup
i18n={i18n}
isEditingText={isEditingText}
onChange={setText}
onClick={() => {
if (!isEditingText) {
setIsEditingText(true);
}
}}
onRemoveLinkPreview={() => {
setHasLinkPreviewApplied(false);
}}
textAttachment={textAttachment}
/>
</div>
<div className="StoryCreator__toolbar">
{isEditingText ? (
<div className="StoryCreator__tools">
<Slider
handleStyle={{ backgroundColor: getRGBA(sliderValue) }}
label={getRGBA(sliderValue)}
moduleClassName="HueSlider StoryCreator__tools__tool"
onChange={setSliderValue}
value={sliderValue}
/>
<ContextMenu
i18n={i18n}
menuOptions={[
{
icon: 'StoryCreator__icon--font-regular',
label: i18n('StoryCreator__text--regular'),
onClick: () => setTextStyle(TextStyle.Regular),
value: TextStyle.Regular,
},
{
icon: 'StoryCreator__icon--font-bold',
label: i18n('StoryCreator__text--bold'),
onClick: () => setTextStyle(TextStyle.Bold),
value: TextStyle.Bold,
},
{
icon: 'StoryCreator__icon--font-serif',
label: i18n('StoryCreator__text--serif'),
onClick: () => setTextStyle(TextStyle.Serif),
value: TextStyle.Serif,
},
{
icon: 'StoryCreator__icon--font-script',
label: i18n('StoryCreator__text--script'),
onClick: () => setTextStyle(TextStyle.Script),
value: TextStyle.Script,
},
{
icon: 'StoryCreator__icon--font-condensed',
label: i18n('StoryCreator__text--condensed'),
onClick: () => setTextStyle(TextStyle.Condensed),
value: TextStyle.Condensed,
},
]}
moduleClassName={classNames('StoryCreator__tools__tool', {
'StoryCreator__tools__button--font-regular':
textStyle === TextStyle.Regular,
'StoryCreator__tools__button--font-bold':
textStyle === TextStyle.Bold,
'StoryCreator__tools__button--font-serif':
textStyle === TextStyle.Serif,
'StoryCreator__tools__button--font-script':
textStyle === TextStyle.Script,
'StoryCreator__tools__button--font-condensed':
textStyle === TextStyle.Condensed,
})}
theme={Theme.Dark}
value={textStyle}
/>
<button
aria-label={getBgButtonAriaLabel(i18n, textBackground)}
className={classNames('StoryCreator__tools__tool', {
'StoryCreator__tools__button--bg-none':
textBackground === TextBackground.None,
'StoryCreator__tools__button--bg':
textBackground === TextBackground.Background,
'StoryCreator__tools__button--bg-inverse':
textBackground === TextBackground.Inverse,
})}
onClick={() => {
if (textBackground === TextBackground.None) {
setTextBackground(TextBackground.Background);
} else if (textBackground === TextBackground.Background) {
setTextBackground(TextBackground.Inverse);
} else {
setTextBackground(TextBackground.None);
}
}}
type="button"
/>
</div>
) : (
<div className="StoryCreator__toolbar--space" />
)}
<div className="StoryCreator__toolbar--buttons">
<Button
onClick={onClose}
theme={Theme.Dark}
variant={ButtonVariant.Secondary}
>
{i18n('discard')}
</Button>
<div className="StoryCreator__controls">
<button
aria-label={i18n('StoryCreator__story-bg')}
className={classNames({
StoryCreator__control: true,
'StoryCreator__control--bg': true,
'StoryCreator__control--bg--selected': isColorPickerShowing,
})}
onClick={() => setIsColorPickerShowing(!isColorPickerShowing)}
ref={setColorPickerPopperButtonRef}
style={{
background: getBackgroundColor(
getBackground(selectedBackground)
),
}}
type="button"
/>
{isColorPickerShowing && (
<div
className="StoryCreator__popper"
ref={setColorPickerPopperRef}
style={colorPickerPopper.styles.popper}
{...colorPickerPopper.attributes.popper}
>
<div
data-popper-arrow
className="StoryCreator__popper__arrow"
/>
{objectMap<BackgroundStyleType>(
BackgroundStyle,
(bg, backgroundValue) => (
<button
aria-label={i18n('StoryCreator__story-bg')}
className={classNames({
StoryCreator__bg: true,
'StoryCreator__bg--selected':
selectedBackground === backgroundValue,
})}
key={String(bg)}
onClick={() => {
setSelectedBackground(backgroundValue);
setIsColorPickerShowing(false);
}}
type="button"
style={{
background: getBackgroundColor(
getBackground(backgroundValue)
),
}}
/>
)
)}
</div>
)}
<button
aria-label={i18n('StoryCreator__control--text')}
className={classNames({
StoryCreator__control: true,
'StoryCreator__control--text': true,
'StoryCreator__control--selected': isEditingText,
})}
onClick={() => {
setIsEditingText(!isEditingText);
}}
type="button"
/>
<button
aria-label={i18n('StoryCreator__control--link')}
className="StoryCreator__control StoryCreator__control--link"
onClick={() =>
setIsLinkPreviewInputShowing(!isLinkPreviewInputShowing)
}
ref={setLinkPreviewInputPopperButtonRef}
type="button"
/>
{isLinkPreviewInputShowing && (
<div
className={classNames(
'StoryCreator__popper StoryCreator__link-preview-input-popper',
themeClassName(Theme.Dark)
)}
ref={setLinkPreviewInputPopperRef}
style={linkPreviewInputPopper.styles.popper}
{...linkPreviewInputPopper.attributes.popper}
>
<div
data-popper-arrow
className="StoryCreator__popper__arrow"
/>
<Input
disableSpellcheck
i18n={i18n}
moduleClassName="StoryCreator__link-preview-input"
onChange={setLinkPreviewInputValue}
placeholder={i18n('StoryCreator__link-preview-placeholder')}
ref={el => el?.focus()}
value={linkPreviewInputValue}
/>
<div className="StoryCreator__link-preview-container">
{linkPreview ? (
<>
<StagedLinkPreview
domain={linkPreview.domain}
i18n={i18n}
image={linkPreview.image}
moduleClassName="StoryCreator__link-preview"
title={linkPreview.title}
url={linkPreview.url}
/>
<Button
className="StoryCreator__link-preview-button"
onClick={() => {
setHasLinkPreviewApplied(true);
setIsLinkPreviewInputShowing(false);
}}
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{i18n('StoryCreator__add-link')}
</Button>
</>
) : (
<div className="StoryCreator__link-preview-empty">
<div className="StoryCreator__link-preview-empty__icon" />
{i18n('StoryCreator__link-preview-empty')}
</div>
)}
</div>
</div>
)}
</div>
<Button
disabled={!hasChanges}
onClick={() => onDone(textAttachment)}
theme={Theme.Dark}
variant={ButtonVariant.Primary}
>
{i18n('StoryCreator__next')}
</Button>
</div>
</div>
</div>
</FocusTrap>
);
};

View file

@ -112,7 +112,6 @@ const storyJobDataSchema = z.object({
conversationId: z.string(), conversationId: z.string(),
// Note: recipients are baked into the message itself // Note: recipients are baked into the message itself
messageIds: z.string().array(), messageIds: z.string().array(),
textAttachment: z.any(), // TODO TextAttachmentType
timestamp: z.number(), timestamp: z.number(),
revision: z.number().optional(), revision: z.number().optional(),
}); });

View file

@ -2,6 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import type {
AttachmentWithHydratedData,
TextAttachmentType,
} from '../../types/Attachment';
import type { ConversationModel } from '../../models/conversations'; import type { ConversationModel } from '../../models/conversations';
import type { import type {
ConversationQueueJobBundle, ConversationQueueJobBundle,
@ -42,7 +46,7 @@ export async function sendStory(
}: ConversationQueueJobBundle, }: ConversationQueueJobBundle,
data: StoryJobData data: StoryJobData
): Promise<void> { ): Promise<void> {
const { messageIds, textAttachment, timestamp } = data; const { messageIds, timestamp } = data;
const profileKey = await ourProfileKeyService.get(); const profileKey = await ourProfileKeyService.get();
@ -51,14 +55,57 @@ export async function sendStory(
return; return;
} }
// We want to generate the StoryMessage proto once at the top level so we
// can reuse it but first we'll need textAttachment | fileAttachment.
// This function pulls off the attachment and generates the proto from the
// first message on the list prior to continuing.
const originalStoryMessage = await (async (): Promise<
Proto.StoryMessage | undefined
> => {
const [messageId] = messageIds;
const message = await getMessageById(messageId);
if (!message) {
log.info(
`stories.sendStory: message ${messageId} was not found, maybe because it was deleted. Giving up on sending it`
);
return;
}
const attachments = message.get('attachments') || [];
const [attachment] = attachments;
if (!attachment) {
log.info(
`stories.sendStory: message ${messageId} does not have any attachments to send. Giving up on sending it`
);
return;
}
let textAttachment: TextAttachmentType | undefined;
let fileAttachment: AttachmentWithHydratedData | undefined;
if (attachment.textAttachment) {
textAttachment = attachment.textAttachment;
} else {
fileAttachment = await window.Signal.Migrations.loadAttachmentData(
attachment
);
}
// Some distribution lists need allowsReplies false, some need it set to true // Some distribution lists need allowsReplies false, some need it set to true
// we create this proto (for the sync message) and also to re-use some of the // we create this proto (for the sync message) and also to re-use some of the
// attributes inside it. // attributes inside it.
const originalStoryMessage = await messaging.getStoryMessage({ return messaging.getStoryMessage({
allowsReplies: true, allowsReplies: true,
fileAttachment,
textAttachment, textAttachment,
profileKey, profileKey,
}); });
})();
if (!originalStoryMessage) {
return;
}
const accSendStateByConversationId = new Map<string, SendState>(); const accSendStateByConversationId = new Map<string, SendState>();
const canReplyUuids = new Set<string>(); const canReplyUuids = new Set<string>();

View file

@ -3,10 +3,7 @@
import type { ThunkAction, ThunkDispatch } from 'redux-thunk'; import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
import { isEqual, noop, pick } from 'lodash'; import { isEqual, noop, pick } from 'lodash';
import type { import type { AttachmentType } from '../../types/Attachment';
AttachmentType,
TextAttachmentType,
} from '../../types/Attachment';
import type { BodyRangeType } from '../../types/Util'; import type { BodyRangeType } from '../../types/Util';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import type { import type {
@ -562,10 +559,10 @@ function replyToStory(
function sendStoryMessage( function sendStoryMessage(
listIds: Array<UUIDStringType>, listIds: Array<UUIDStringType>,
textAttachment: TextAttachmentType attachment: AttachmentType
): ThunkAction<void, RootStateType, unknown, NoopActionType> { ): ThunkAction<void, RootStateType, unknown, NoopActionType> {
return async dispatch => { return async dispatch => {
await doSendStoryMessage(listIds, textAttachment); await doSendStoryMessage(listIds, attachment);
dispatch({ dispatch({
type: 'NOOP', type: 'NOOP',

View file

@ -20,9 +20,10 @@ import { useGlobalModalActions } from '../ducks/globalModals';
import { useStoriesActions } from '../ducks/stories'; import { useStoriesActions } from '../ducks/stories';
function renderStoryCreator({ function renderStoryCreator({
file,
onClose, onClose,
}: SmartStoryCreatorPropsType): JSX.Element { }: SmartStoryCreatorPropsType): JSX.Element {
return <SmartStoryCreator onClose={onClose} />; return <SmartStoryCreator file={file} onClose={onClose} />;
} }
export function SmartStories(): JSX.Element | null { export function SmartStories(): JSX.Element | null {

View file

@ -8,25 +8,36 @@ import type { LocalizerType } from '../../types/Util';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import { LinkPreviewSourceType } from '../../types/LinkPreview'; import { LinkPreviewSourceType } from '../../types/LinkPreview';
import { StoryCreator } from '../../components/StoryCreator'; import { StoryCreator } from '../../components/StoryCreator';
import { getAllSignalConnections, getMe } from '../selectors/conversations';
import { getDistributionLists } from '../selectors/storyDistributionLists'; import { getDistributionLists } from '../selectors/storyDistributionLists';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import {
getInstalledStickerPacks,
getRecentStickers,
} from '../selectors/stickers';
import { getLinkPreview } from '../selectors/linkPreviews'; import { getLinkPreview } from '../selectors/linkPreviews';
import { getAllSignalConnections, getMe } from '../selectors/conversations'; import { processAttachment } from '../../util/processAttachment';
import { useLinkPreviewActions } from '../ducks/linkPreviews'; import { useLinkPreviewActions } from '../ducks/linkPreviews';
import { useStoriesActions } from '../ducks/stories'; import { useStoriesActions } from '../ducks/stories';
export type PropsType = { export type PropsType = {
file?: File;
onClose: () => unknown; onClose: () => unknown;
}; };
export function SmartStoryCreator({ onClose }: PropsType): JSX.Element | null { export function SmartStoryCreator({
file,
onClose,
}: PropsType): JSX.Element | null {
const { debouncedMaybeGrabLinkPreview } = useLinkPreviewActions(); const { debouncedMaybeGrabLinkPreview } = useLinkPreviewActions();
const { sendStoryMessage } = useStoriesActions(); const { sendStoryMessage } = useStoriesActions();
const i18n = useSelector<StateType, LocalizerType>(getIntl); const i18n = useSelector<StateType, LocalizerType>(getIntl);
const linkPreviewForSource = useSelector(getLinkPreview);
const distributionLists = useSelector(getDistributionLists); const distributionLists = useSelector(getDistributionLists);
const installedPacks = useSelector(getInstalledStickerPacks);
const linkPreviewForSource = useSelector(getLinkPreview);
const me = useSelector(getMe); const me = useSelector(getMe);
const recentStickers = useSelector(getRecentStickers);
const signalConnections = useSelector(getAllSignalConnections); const signalConnections = useSelector(getAllSignalConnections);
return ( return (
@ -34,10 +45,14 @@ export function SmartStoryCreator({ onClose }: PropsType): JSX.Element | null {
debouncedMaybeGrabLinkPreview={debouncedMaybeGrabLinkPreview} debouncedMaybeGrabLinkPreview={debouncedMaybeGrabLinkPreview}
distributionLists={distributionLists} distributionLists={distributionLists}
i18n={i18n} i18n={i18n}
installedPacks={installedPacks}
file={file}
linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)} linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)}
me={me} me={me}
onClose={onClose} onClose={onClose}
onSend={sendStoryMessage} onSend={sendStoryMessage}
processAttachment={processAttachment}
recentStickers={recentStickers}
signalConnections={signalConnections} signalConnections={signalConnections}
/> />
); );

View file

@ -9205,7 +9205,7 @@
}, },
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/StoryCreator.tsx", "path": "ts/components/TextStoryCreator.tsx",
"line": " const textEditorRef = useRef<HTMLInputElement | null>(null);", "line": " const textEditorRef = useRef<HTMLInputElement | null>(null);",
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2022-06-16T23:23:32.306Z" "updated": "2022-06-16T23:23:32.306Z"

View file

@ -1,9 +1,9 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { AttachmentType } from '../types/Attachment';
import type { MessageAttributesType } from '../model-types.d'; import type { MessageAttributesType } from '../model-types.d';
import type { SendStateByConversationId } from '../messages/MessageSendState'; import type { SendStateByConversationId } from '../messages/MessageSendState';
import type { TextAttachmentType } from '../types/Attachment';
import type { UUIDStringType } from '../types/UUID'; import type { UUIDStringType } from '../types/UUID';
import * as log from '../logging/log'; import * as log from '../logging/log';
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
@ -12,7 +12,6 @@ import { MY_STORIES_ID } from '../types/Stories';
import { ReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus } from '../messages/MessageReadStatus';
import { SeenStatus } from '../MessageSeenStatus'; import { SeenStatus } from '../MessageSeenStatus';
import { SendStatus } from '../messages/MessageSendState'; import { SendStatus } from '../messages/MessageSendState';
import { TEXT_ATTACHMENT } from '../types/MIME';
import { UUID } from '../types/UUID'; import { UUID } from '../types/UUID';
import { import {
conversationJobQueue, conversationJobQueue,
@ -25,7 +24,7 @@ import { isNotNil } from './isNotNil';
export async function sendStoryMessage( export async function sendStoryMessage(
listIds: Array<string>, listIds: Array<string>,
textAttachment: TextAttachmentType attachment: AttachmentType
): Promise<void> { ): Promise<void> {
const { messaging } = window.textsecure; const { messaging } = window.textsecure;
@ -124,6 +123,8 @@ export async function sendStoryMessage(
sendStateByListId.set(distributionList.id, sendStateByConversationId); sendStateByListId.set(distributionList.id, sendStateByConversationId);
}); });
const attachments: Array<AttachmentType> = [attachment];
// * Gather all the job data we'll be sending to the sendStory job // * Gather all the job data we'll be sending to the sendStory job
// * Create the message for each distribution list // * Create the message for each distribution list
const messagesToSave: Array<MessageAttributesType> = await Promise.all( const messagesToSave: Array<MessageAttributesType> = await Promise.all(
@ -140,13 +141,7 @@ export async function sendStoryMessage(
} }
return window.Signal.Migrations.upgradeMessageSchema({ return window.Signal.Migrations.upgradeMessageSchema({
attachments: [ attachments,
{
contentType: TEXT_ATTACHMENT,
textAttachment,
size: textAttachment.text?.length || 0,
},
],
conversationId: ourConversation.id, conversationId: ourConversation.id,
expireTimer: DAY / SECOND, expireTimer: DAY / SECOND,
id: UUID.generate().toString(), id: UUID.generate().toString(),
@ -189,7 +184,6 @@ export async function sendStoryMessage(
type: conversationQueueJobEnum.enum.Story, type: conversationQueueJobEnum.enum.Story,
conversationId: ourConversation.id, conversationId: ourConversation.id,
messageIds: messagesToSave.map(m => m.id), messageIds: messagesToSave.map(m => m.id),
textAttachment,
timestamp, timestamp,
}, },
async jobToInsert => { async jobToInsert => {