Send story images/video
This commit is contained in:
parent
fcf7406dd4
commit
7bc6bbc668
16 changed files with 807 additions and 567 deletions
|
@ -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": "Can’t download story. You will need to share it again.",
|
"message": "Can’t 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": {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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),
|
||||||
])}
|
])}
|
||||||
|
|
|
@ -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),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
514
ts/components/TextStoryCreator.tsx
Normal file
514
ts/components/TextStoryCreator.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
Loading…
Reference in a new issue