Notify story creator for replies
This commit is contained in:
parent
512d655d32
commit
25bc16300c
16 changed files with 250 additions and 135 deletions
|
@ -154,6 +154,7 @@ import { SeenStatus } from './MessageSeenStatus';
|
||||||
import MessageSender from './textsecure/SendMessage';
|
import MessageSender from './textsecure/SendMessage';
|
||||||
import type AccountManager from './textsecure/AccountManager';
|
import type AccountManager from './textsecure/AccountManager';
|
||||||
import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate';
|
import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate';
|
||||||
|
import { StoryViewModeType, StoryViewTargetType } from './types/Stories';
|
||||||
|
|
||||||
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
|
||||||
|
|
||||||
|
@ -1879,10 +1880,19 @@ export async function startApp(): Promise<void> {
|
||||||
activeWindowService.registerForActive(() => notificationService.clear());
|
activeWindowService.registerForActive(() => notificationService.clear());
|
||||||
window.addEventListener('unload', () => notificationService.fastClear());
|
window.addEventListener('unload', () => notificationService.fastClear());
|
||||||
|
|
||||||
notificationService.on('click', (id, messageId) => {
|
notificationService.on('click', (id, messageId, storyId) => {
|
||||||
window.showWindow();
|
window.showWindow();
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
window.Whisper.events.trigger('showConversation', id, messageId);
|
if (storyId) {
|
||||||
|
window.reduxActions.stories.viewStory({
|
||||||
|
storyId,
|
||||||
|
storyViewMode: StoryViewModeType.Single,
|
||||||
|
viewTarget: StoryViewTargetType.Replies,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.Whisper.events.trigger('showConversation', id, messageId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
window.reduxActions.app.openInbox();
|
window.reduxActions.app.openInbox();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,12 @@
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import type { MyStoryType, StoryViewType } from '../types/Stories';
|
import type { MyStoryType, StoryViewType } from '../types/Stories';
|
||||||
|
import { StoryViewTargetType, StoryViewModeType } from '../types/Stories';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import { ContextMenu } from './ContextMenu';
|
import { ContextMenu } from './ContextMenu';
|
||||||
import { StoryViewModeType } from '../types/Stories';
|
|
||||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
|
@ -151,7 +152,7 @@ export const MyStories = ({
|
||||||
viewStory({
|
viewStory({
|
||||||
storyId: story.messageId,
|
storyId: story.messageId,
|
||||||
storyViewMode: StoryViewModeType.User,
|
storyViewMode: StoryViewModeType.User,
|
||||||
shouldShowDetailsModal: true,
|
viewTarget: StoryViewTargetType.Details,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,13 +5,14 @@ import React, { useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { ConversationStoryType, StoryViewType } from '../types/Stories';
|
import type { ConversationStoryType, StoryViewType } from '../types/Stories';
|
||||||
|
import { StoryViewTargetType, HasStories } from '../types/Stories';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||||
import type { ViewUserStoriesActionCreatorType } from '../state/ducks/stories';
|
import type { ViewUserStoriesActionCreatorType } from '../state/ducks/stories';
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import { ContextMenu } from './ContextMenu';
|
import { ContextMenu } from './ContextMenu';
|
||||||
import { HasStories } from '../types/Stories';
|
|
||||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
import { ThemeType } from '../types/Util';
|
import { ThemeType } from '../types/Util';
|
||||||
|
@ -134,7 +135,10 @@ export const StoryListItem = ({
|
||||||
icon: 'StoryListItem__icon--info',
|
icon: 'StoryListItem__icon--info',
|
||||||
label: i18n('StoryListItem__info'),
|
label: i18n('StoryListItem__info'),
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
viewUserStories({ conversationId, shouldShowDetailsModal: true }),
|
viewUserStories({
|
||||||
|
conversationId,
|
||||||
|
viewTarget: StoryViewTargetType.Details,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'StoryListItem__icon--chat',
|
icon: 'StoryListItem__icon--chat',
|
||||||
|
|
|
@ -31,7 +31,11 @@ import { SendStatus } from '../messages/MessageSendState';
|
||||||
import { StoryDetailsModal } from './StoryDetailsModal';
|
import { StoryDetailsModal } from './StoryDetailsModal';
|
||||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||||
import { StoryImage } from './StoryImage';
|
import { StoryImage } from './StoryImage';
|
||||||
import { StoryViewDirectionType, StoryViewModeType } from '../types/Stories';
|
import {
|
||||||
|
StoryViewDirectionType,
|
||||||
|
StoryViewModeType,
|
||||||
|
StoryViewTargetType,
|
||||||
|
} from '../types/Stories';
|
||||||
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
import { StoryViewsNRepliesModal } from './StoryViewsNRepliesModal';
|
||||||
import { Theme } from '../util/theme';
|
import { Theme } from '../util/theme';
|
||||||
import { ToastType } from '../state/ducks/toast';
|
import { ToastType } from '../state/ducks/toast';
|
||||||
|
@ -83,7 +87,7 @@ export type PropsType = {
|
||||||
recentEmojis?: Array<string>;
|
recentEmojis?: Array<string>;
|
||||||
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
|
||||||
replyState?: ReplyStateType;
|
replyState?: ReplyStateType;
|
||||||
shouldShowDetailsModal?: boolean;
|
viewTarget?: StoryViewTargetType;
|
||||||
showToast: ShowToastActionCreatorType;
|
showToast: ShowToastActionCreatorType;
|
||||||
skinTone?: number;
|
skinTone?: number;
|
||||||
story: StoryViewType;
|
story: StoryViewType;
|
||||||
|
@ -128,7 +132,7 @@ export const StoryViewer = ({
|
||||||
recentEmojis,
|
recentEmojis,
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
replyState,
|
replyState,
|
||||||
shouldShowDetailsModal,
|
viewTarget,
|
||||||
showToast,
|
showToast,
|
||||||
skinTone,
|
skinTone,
|
||||||
story,
|
story,
|
||||||
|
@ -167,12 +171,14 @@ export const StoryViewer = ({
|
||||||
|
|
||||||
const conversationId = group?.id || story.sender.id;
|
const conversationId = group?.id || story.sender.id;
|
||||||
|
|
||||||
const [hasStoryViewsNRepliesModal, setHasStoryViewsNRepliesModal] =
|
const [currentViewTarget, setCurrentViewTarget] = useState(
|
||||||
useState(false);
|
viewTarget ?? null
|
||||||
const [hasStoryDetailsModal, setHasStoryDetailsModal] = useState(
|
|
||||||
Boolean(shouldShowDetailsModal)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentViewTarget(viewTarget ?? null);
|
||||||
|
}, [viewTarget]);
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
viewStory({
|
viewStory({
|
||||||
closeViewer: true,
|
closeViewer: true,
|
||||||
|
@ -180,12 +186,12 @@ export const StoryViewer = ({
|
||||||
}, [viewStory]);
|
}, [viewStory]);
|
||||||
|
|
||||||
const onEscape = useCallback(() => {
|
const onEscape = useCallback(() => {
|
||||||
if (hasStoryViewsNRepliesModal) {
|
if (currentViewTarget != null) {
|
||||||
setHasStoryViewsNRepliesModal(false);
|
setCurrentViewTarget(null);
|
||||||
} else {
|
} else {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}, [hasStoryViewsNRepliesModal, onClose]);
|
}, [currentViewTarget, onClose]);
|
||||||
|
|
||||||
useEscapeHandling(onEscape);
|
useEscapeHandling(onEscape);
|
||||||
|
|
||||||
|
@ -314,8 +320,7 @@ export const StoryViewer = ({
|
||||||
hasActiveCall ||
|
hasActiveCall ||
|
||||||
hasConfirmHideStory ||
|
hasConfirmHideStory ||
|
||||||
hasExpandedCaption ||
|
hasExpandedCaption ||
|
||||||
hasStoryDetailsModal ||
|
currentViewTarget != null ||
|
||||||
hasStoryViewsNRepliesModal ||
|
|
||||||
isShowingContextMenu ||
|
isShowingContextMenu ||
|
||||||
pauseStory ||
|
pauseStory ||
|
||||||
Boolean(reactionEmoji);
|
Boolean(reactionEmoji);
|
||||||
|
@ -351,7 +356,7 @@ export const StoryViewer = ({
|
||||||
(ev: KeyboardEvent) => {
|
(ev: KeyboardEvent) => {
|
||||||
// the replies modal can consume arrow keys
|
// the replies modal can consume arrow keys
|
||||||
// we don't want to navigate while someone is typing a reply
|
// we don't want to navigate while someone is typing a reply
|
||||||
if (hasStoryViewsNRepliesModal) {
|
if (currentViewTarget != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,7 +379,7 @@ export const StoryViewer = ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
hasStoryViewsNRepliesModal,
|
currentViewTarget,
|
||||||
canNavigateLeft,
|
canNavigateLeft,
|
||||||
canNavigateRight,
|
canNavigateRight,
|
||||||
story.messageId,
|
story.messageId,
|
||||||
|
@ -466,7 +471,7 @@ export const StoryViewer = ({
|
||||||
{
|
{
|
||||||
icon: 'StoryListItem__icon--info',
|
icon: 'StoryListItem__icon--info',
|
||||||
label: i18n('StoryListItem__info'),
|
label: i18n('StoryListItem__info'),
|
||||||
onClick: () => setHasStoryDetailsModal(true),
|
onClick: () => setCurrentViewTarget(StoryViewTargetType.Details),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'StoryListItem__icon--delete',
|
icon: 'StoryListItem__icon--delete',
|
||||||
|
@ -478,7 +483,7 @@ export const StoryViewer = ({
|
||||||
{
|
{
|
||||||
icon: 'StoryListItem__icon--info',
|
icon: 'StoryListItem__icon--info',
|
||||||
label: i18n('StoryListItem__info'),
|
label: i18n('StoryListItem__info'),
|
||||||
onClick: () => setHasStoryDetailsModal(true),
|
onClick: () => setCurrentViewTarget(StoryViewTargetType.Details),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'StoryListItem__icon--hide',
|
icon: 'StoryListItem__icon--hide',
|
||||||
|
@ -726,7 +731,9 @@ export const StoryViewer = ({
|
||||||
{(canReply || isSent) && (
|
{(canReply || isSent) && (
|
||||||
<button
|
<button
|
||||||
className="StoryViewer__reply"
|
className="StoryViewer__reply"
|
||||||
onClick={() => setHasStoryViewsNRepliesModal(true)}
|
onClick={() =>
|
||||||
|
setCurrentViewTarget(StoryViewTargetType.Replies)
|
||||||
|
}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -788,11 +795,11 @@ export const StoryViewer = ({
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{hasStoryDetailsModal && (
|
{currentViewTarget === StoryViewTargetType.Details && (
|
||||||
<StoryDetailsModal
|
<StoryDetailsModal
|
||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClose={() => setHasStoryDetailsModal(false)}
|
onClose={() => setCurrentViewTarget(null)}
|
||||||
sender={story.sender}
|
sender={story.sender}
|
||||||
sendState={sendState}
|
sendState={sendState}
|
||||||
size={attachment?.size}
|
size={attachment?.size}
|
||||||
|
@ -800,7 +807,8 @@ export const StoryViewer = ({
|
||||||
expirationTimestamp={story.expirationTimestamp}
|
expirationTimestamp={story.expirationTimestamp}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasStoryViewsNRepliesModal && (
|
{(currentViewTarget === StoryViewTargetType.Replies ||
|
||||||
|
currentViewTarget === StoryViewTargetType.Views) && (
|
||||||
<StoryViewsNRepliesModal
|
<StoryViewsNRepliesModal
|
||||||
authorTitle={firstName || title}
|
authorTitle={firstName || title}
|
||||||
canReply={Boolean(canReply)}
|
canReply={Boolean(canReply)}
|
||||||
|
@ -809,18 +817,18 @@ export const StoryViewer = ({
|
||||||
hasViewsCapability={isSent}
|
hasViewsCapability={isSent}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isGroupStory={isGroupStory}
|
isGroupStory={isGroupStory}
|
||||||
onClose={() => setHasStoryViewsNRepliesModal(false)}
|
onClose={() => setCurrentViewTarget(null)}
|
||||||
onReact={emoji => {
|
onReact={emoji => {
|
||||||
onReactToStory(emoji, story);
|
onReactToStory(emoji, story);
|
||||||
if (!isGroupStory) {
|
if (!isGroupStory) {
|
||||||
setHasStoryViewsNRepliesModal(false);
|
setCurrentViewTarget(null);
|
||||||
showToast(ToastType.StoryReact);
|
showToast(ToastType.StoryReact);
|
||||||
}
|
}
|
||||||
setReactionEmoji(emoji);
|
setReactionEmoji(emoji);
|
||||||
}}
|
}}
|
||||||
onReply={(message, mentions, replyTimestamp) => {
|
onReply={(message, mentions, replyTimestamp) => {
|
||||||
if (!isGroupStory) {
|
if (!isGroupStory) {
|
||||||
setHasStoryViewsNRepliesModal(false);
|
setCurrentViewTarget(null);
|
||||||
showToast(ToastType.StoryReply);
|
showToast(ToastType.StoryReply);
|
||||||
}
|
}
|
||||||
onReplyToStory(message, mentions, replyTimestamp, story);
|
onReplyToStory(message, mentions, replyTimestamp, story);
|
||||||
|
@ -836,6 +844,8 @@ export const StoryViewer = ({
|
||||||
sortedGroupMembers={group?.sortedGroupMembers}
|
sortedGroupMembers={group?.sortedGroupMembers}
|
||||||
storyPreviewAttachment={attachment}
|
storyPreviewAttachment={attachment}
|
||||||
views={views}
|
views={views}
|
||||||
|
viewTarget={currentViewTarget}
|
||||||
|
onChangeViewTarget={setCurrentViewTarget}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasConfirmHideStory && (
|
{hasConfirmHideStory && (
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import type { Meta, Story } from '@storybook/react';
|
import type { Meta, Story } from '@storybook/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { useArgs } from '@storybook/addons';
|
||||||
import type { PropsType } from './StoryViewsNRepliesModal';
|
import type { PropsType } from './StoryViewsNRepliesModal';
|
||||||
import * as durations from '../util/durations';
|
import * as durations from '../util/durations';
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
@ -14,6 +15,7 @@ import { UUID } from '../types/UUID';
|
||||||
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
|
import { fakeAttachment } from '../test-both/helpers/fakeAttachment';
|
||||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||||
import { setupI18n } from '../util/setupI18n';
|
import { setupI18n } from '../util/setupI18n';
|
||||||
|
import { StoryViewTargetType } from '../types/Stories';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -64,6 +66,12 @@ export default {
|
||||||
views: {
|
views: {
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
},
|
},
|
||||||
|
viewTarget: {
|
||||||
|
defaultValue: StoryViewTargetType.Views,
|
||||||
|
},
|
||||||
|
onChangeViewTarget: {
|
||||||
|
action: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as Meta;
|
} as Meta;
|
||||||
|
|
||||||
|
@ -161,9 +169,21 @@ function getViewsAndReplies() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const Template: Story<PropsType> = args => (
|
const Template: Story<PropsType> = args => {
|
||||||
<StoryViewsNRepliesModal {...args} />
|
const [, updateArgs] = useArgs();
|
||||||
);
|
|
||||||
|
function onChangeViewTarget(viewTarget: StoryViewTargetType) {
|
||||||
|
args.onChangeViewTarget(viewTarget);
|
||||||
|
updateArgs({ viewTarget });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StoryViewsNRepliesModal
|
||||||
|
{...args}
|
||||||
|
onChangeViewTarget={onChangeViewTarget}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const CanReply = Template.bind({});
|
export const CanReply = Template.bind({});
|
||||||
CanReply.args = {};
|
CanReply.args = {};
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
// 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 React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { usePopper } from 'react-popper';
|
import { usePopper } from 'react-popper';
|
||||||
import type { AttachmentType } from '../types/Attachment';
|
import type { AttachmentType } from '../types/Attachment';
|
||||||
|
@ -12,6 +18,7 @@ import type { InputApi } from './CompositionInput';
|
||||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||||
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
|
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
|
||||||
import type { ReplyType, StorySendStateType } from '../types/Stories';
|
import type { ReplyType, StorySendStateType } from '../types/Stories';
|
||||||
|
import { StoryViewTargetType } from '../types/Stories';
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { CompositionInput } from './CompositionInput';
|
import { CompositionInput } from './CompositionInput';
|
||||||
import { ContactName } from './conversation/ContactName';
|
import { ContactName } from './conversation/ContactName';
|
||||||
|
@ -78,7 +85,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
||||||
viewStory: shouldNeverBeCalled,
|
viewStory: shouldNeverBeCalled,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum Tab {
|
export enum StoryViewsNRepliesTab {
|
||||||
Replies = 'Replies',
|
Replies = 'Replies',
|
||||||
Views = 'Views',
|
Views = 'Views',
|
||||||
}
|
}
|
||||||
|
@ -109,6 +116,8 @@ export type PropsType = {
|
||||||
sortedGroupMembers?: Array<ConversationType>;
|
sortedGroupMembers?: Array<ConversationType>;
|
||||||
storyPreviewAttachment?: AttachmentType;
|
storyPreviewAttachment?: AttachmentType;
|
||||||
views: Array<StorySendStateType>;
|
views: Array<StorySendStateType>;
|
||||||
|
viewTarget: StoryViewTargetType;
|
||||||
|
onChangeViewTarget: (target: StoryViewTargetType) => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StoryViewsNRepliesModal = ({
|
export const StoryViewsNRepliesModal = ({
|
||||||
|
@ -133,14 +142,30 @@ export const StoryViewsNRepliesModal = ({
|
||||||
sortedGroupMembers,
|
sortedGroupMembers,
|
||||||
storyPreviewAttachment,
|
storyPreviewAttachment,
|
||||||
views,
|
views,
|
||||||
|
viewTarget,
|
||||||
|
onChangeViewTarget,
|
||||||
}: PropsType): JSX.Element | null => {
|
}: PropsType): JSX.Element | null => {
|
||||||
const containerElementRef = useRef<HTMLDivElement | null>(null);
|
const containerElementRef = useRef<HTMLDivElement | null>(null);
|
||||||
const inputApiRef = useRef<InputApi | undefined>();
|
const inputApiRef = useRef<InputApi | undefined>();
|
||||||
const shouldScrollToBottomRef = useRef(false);
|
const shouldScrollToBottomRef = useRef(true);
|
||||||
const [bottom, setBottom] = useState<HTMLDivElement | null>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const [messageBodyText, setMessageBodyText] = useState('');
|
const [messageBodyText, setMessageBodyText] = useState('');
|
||||||
const [showReactionPicker, setShowReactionPicker] = useState(false);
|
const [showReactionPicker, setShowReactionPicker] = useState(false);
|
||||||
|
|
||||||
|
const currentTab = useMemo<StoryViewsNRepliesTab>(() => {
|
||||||
|
return viewTarget === StoryViewTargetType.Replies
|
||||||
|
? StoryViewsNRepliesTab.Replies
|
||||||
|
: StoryViewsNRepliesTab.Views;
|
||||||
|
}, [viewTarget]);
|
||||||
|
|
||||||
|
const onTabChange = (tab: string) => {
|
||||||
|
onChangeViewTarget(
|
||||||
|
tab === StoryViewsNRepliesTab.Replies
|
||||||
|
? StoryViewTargetType.Replies
|
||||||
|
: StoryViewTargetType.Views
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const focusComposer = useCallback(() => {
|
const focusComposer = useCallback(() => {
|
||||||
if (inputApiRef.current) {
|
if (inputApiRef.current) {
|
||||||
inputApiRef.current.focus();
|
inputApiRef.current.focus();
|
||||||
|
@ -170,12 +195,16 @@ export const StoryViewsNRepliesModal = ({
|
||||||
|
|
||||||
let composerElement: JSX.Element | undefined;
|
let composerElement: JSX.Element | undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (replies.length && shouldScrollToBottomRef.current) {
|
if (
|
||||||
bottom?.scrollIntoView({ behavior: 'smooth' });
|
currentTab === StoryViewsNRepliesTab.Replies &&
|
||||||
|
replies.length &&
|
||||||
|
shouldScrollToBottomRef.current
|
||||||
|
) {
|
||||||
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
shouldScrollToBottomRef.current = false;
|
shouldScrollToBottomRef.current = false;
|
||||||
}
|
}
|
||||||
}, [bottom, replies.length]);
|
}, [currentTab, replies.length]);
|
||||||
|
|
||||||
if (canReply) {
|
if (canReply) {
|
||||||
composerElement = (
|
composerElement = (
|
||||||
|
@ -348,7 +377,7 @@ export const StoryViewsNRepliesModal = ({
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<div ref={setBottom} />
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (isGroupStory) {
|
} else if (isGroupStory) {
|
||||||
|
@ -414,23 +443,24 @@ export const StoryViewsNRepliesModal = ({
|
||||||
const tabsElement =
|
const tabsElement =
|
||||||
viewsElement && repliesElement ? (
|
viewsElement && repliesElement ? (
|
||||||
<Tabs
|
<Tabs
|
||||||
initialSelectedTab={Tab.Views}
|
selectedTab={currentTab}
|
||||||
|
onTabChange={onTabChange}
|
||||||
moduleClassName="StoryViewsNRepliesModal__tabs"
|
moduleClassName="StoryViewsNRepliesModal__tabs"
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
id: Tab.Views,
|
id: StoryViewsNRepliesTab.Views,
|
||||||
label: i18n('StoryViewsNRepliesModal__tab--views'),
|
label: i18n('StoryViewsNRepliesModal__tab--views'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: Tab.Replies,
|
id: StoryViewsNRepliesTab.Replies,
|
||||||
label: i18n('StoryViewsNRepliesModal__tab--replies'),
|
label: i18n('StoryViewsNRepliesModal__tab--replies'),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{({ selectedTab }) => (
|
{({ selectedTab }) => (
|
||||||
<>
|
<>
|
||||||
{selectedTab === Tab.Views && viewsElement}
|
{selectedTab === StoryViewsNRepliesTab.Views && viewsElement}
|
||||||
{selectedTab === Tab.Replies && (
|
{selectedTab === StoryViewsNRepliesTab.Replies && (
|
||||||
<>
|
<>
|
||||||
{repliesElement}
|
{repliesElement}
|
||||||
{composerElement}
|
{composerElement}
|
||||||
|
|
|
@ -11,19 +11,9 @@ type PropsType = {
|
||||||
children: (renderProps: { selectedTab: string }) => ReactNode;
|
children: (renderProps: { selectedTab: string }) => ReactNode;
|
||||||
} & TabsOptionsType;
|
} & TabsOptionsType;
|
||||||
|
|
||||||
export const Tabs = ({
|
export const Tabs = (props: PropsType): JSX.Element => {
|
||||||
children,
|
const { children, ...options } = props;
|
||||||
initialSelectedTab,
|
const { selectedTab, tabsHeaderElement } = useTabs(options);
|
||||||
moduleClassName,
|
|
||||||
onTabChange,
|
|
||||||
tabs,
|
|
||||||
}: PropsType): JSX.Element => {
|
|
||||||
const { selectedTab, tabsHeaderElement } = useTabs({
|
|
||||||
initialSelectedTab,
|
|
||||||
moduleClassName,
|
|
||||||
onTabChange,
|
|
||||||
tabs,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -12,33 +12,59 @@ type Tab = {
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TabsOptionsType = {
|
export type BaseTabsOptionsType = {
|
||||||
initialSelectedTab?: string;
|
|
||||||
moduleClassName?: string;
|
moduleClassName?: string;
|
||||||
onTabChange?: (selectedTab: string) => unknown;
|
|
||||||
tabs: Array<Tab>;
|
tabs: Array<Tab>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useTabs({
|
export type ControlledTabsOptionsType = BaseTabsOptionsType & {
|
||||||
initialSelectedTab,
|
selectedTab: string;
|
||||||
moduleClassName,
|
onTabChange: (selectedTab: string) => unknown;
|
||||||
onTabChange,
|
};
|
||||||
tabs,
|
|
||||||
}: TabsOptionsType): {
|
export type UncontrolledTabsOptionsType = BaseTabsOptionsType & {
|
||||||
|
initialSelectedTab?: string;
|
||||||
|
onTabChange?: (selectedTab: string) => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TabsOptionsType =
|
||||||
|
| ControlledTabsOptionsType
|
||||||
|
| UncontrolledTabsOptionsType;
|
||||||
|
|
||||||
|
type TabsProps = {
|
||||||
selectedTab: string;
|
selectedTab: string;
|
||||||
tabsHeaderElement: JSX.Element;
|
tabsHeaderElement: JSX.Element;
|
||||||
} {
|
};
|
||||||
assertDev(tabs.length, 'Tabs needs more than 1 tab present');
|
|
||||||
|
|
||||||
const getClassName = getClassNamesFor('Tabs', moduleClassName);
|
export function useTabs(options: TabsOptionsType): TabsProps {
|
||||||
|
assertDev(options.tabs.length, 'Tabs needs more than 1 tab present');
|
||||||
|
|
||||||
const [selectedTab, setSelectedTab] = useState<string>(
|
const getClassName = getClassNamesFor('Tabs', options.moduleClassName);
|
||||||
initialSelectedTab || tabs[0].id
|
|
||||||
);
|
let selectedTab: string;
|
||||||
|
let onChange: (selectedTab: string) => void;
|
||||||
|
|
||||||
|
if ('selectedTab' in options) {
|
||||||
|
selectedTab = options.selectedTab;
|
||||||
|
onChange = options.onTabChange;
|
||||||
|
} else {
|
||||||
|
// useTabs should always be either controlled or uncontrolled.
|
||||||
|
// This is enforced by the type system.
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const [tabState, setTabState] = useState<string>(
|
||||||
|
options.initialSelectedTab || options.tabs[0].id
|
||||||
|
);
|
||||||
|
|
||||||
|
selectedTab = tabState;
|
||||||
|
onChange = (newTab: string) => {
|
||||||
|
setTabState(newTab);
|
||||||
|
options.onTabChange?.(newTab);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const tabsHeaderElement = (
|
const tabsHeaderElement = (
|
||||||
<div className={getClassName('')}>
|
<div className={getClassName('')}>
|
||||||
{tabs.map(({ id, label }) => (
|
{options.tabs.map(({ id, label }) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
getClassName('__tab'),
|
getClassName('__tab'),
|
||||||
|
@ -46,12 +72,11 @@ export function useTabs({
|
||||||
)}
|
)}
|
||||||
key={id}
|
key={id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedTab(id);
|
onChange(id);
|
||||||
onTabChange?.(id);
|
|
||||||
}}
|
}}
|
||||||
onKeyUp={(e: KeyboardEvent) => {
|
onKeyUp={(e: KeyboardEvent) => {
|
||||||
if (e.target === e.currentTarget && e.keyCode === 13) {
|
if (e.target === e.currentTarget && e.keyCode === 13) {
|
||||||
setSelectedTab(id);
|
onChange(id);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5296,6 +5296,7 @@ export class ConversationModel extends window.Backbone
|
||||||
notificationService.add({
|
notificationService.add({
|
||||||
senderTitle,
|
senderTitle,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
storyId: message.get('storyId'),
|
||||||
notificationIconUrl,
|
notificationIconUrl,
|
||||||
isExpiringMessage,
|
isExpiringMessage,
|
||||||
message: message.getNotificationText(),
|
message: message.getNotificationText(),
|
||||||
|
|
|
@ -2853,10 +2853,34 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const isFirstRun = false;
|
const isFirstRun = false;
|
||||||
await this.modifyTargetMessage(conversation, isFirstRun);
|
await this.modifyTargetMessage(conversation, isFirstRun);
|
||||||
|
|
||||||
|
const storyId = this.get('storyId');
|
||||||
const isGroupStoryReply =
|
const isGroupStoryReply =
|
||||||
isGroup(conversation.attributes) && this.get('storyId');
|
isGroup(conversation.attributes) && storyId != null;
|
||||||
|
|
||||||
if (isMessageUnread(this.attributes) && !isGroupStoryReply) {
|
let shouldNotify = true;
|
||||||
|
|
||||||
|
if (!isMessageUnread(this.attributes)) {
|
||||||
|
shouldNotify = false;
|
||||||
|
} else if (isGroupStoryReply) {
|
||||||
|
const match = window.reduxStore.getState().stories.stories.find(story => {
|
||||||
|
return story.messageId === storyId;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceUuid = match?.sourceUuid;
|
||||||
|
const userUuid = window.textsecure.storage.user.getUuid();
|
||||||
|
|
||||||
|
const weAreSource =
|
||||||
|
sourceUuid != null &&
|
||||||
|
userUuid != null &&
|
||||||
|
sourceUuid === userUuid.toString();
|
||||||
|
|
||||||
|
// TODO: Check if we're someone else who has replied/reacted to this story
|
||||||
|
if (!weAreSource) {
|
||||||
|
shouldNotify = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldNotify) {
|
||||||
await conversation.notify(this);
|
await conversation.notify(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,12 +18,13 @@ import type { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
type NotificationDataType = Readonly<{
|
type NotificationDataType = Readonly<{
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
storyId?: string;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
senderTitle: string;
|
senderTitle: string;
|
||||||
message: string;
|
message: string;
|
||||||
notificationIconUrl?: undefined | string;
|
notificationIconUrl?: undefined | string;
|
||||||
isExpiringMessage: boolean;
|
isExpiringMessage: boolean;
|
||||||
reaction: {
|
reaction?: {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
targetAuthorUuid: string;
|
targetAuthorUuid: string;
|
||||||
targetTimestamp: number;
|
targetTimestamp: number;
|
||||||
|
@ -268,6 +269,7 @@ class NotificationService extends EventEmitter {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
conversationId,
|
conversationId,
|
||||||
|
storyId,
|
||||||
messageId,
|
messageId,
|
||||||
senderTitle,
|
senderTitle,
|
||||||
message,
|
message,
|
||||||
|
@ -340,7 +342,7 @@ class NotificationService extends EventEmitter {
|
||||||
message: notificationMessage,
|
message: notificationMessage,
|
||||||
silent: !shouldPlayNotificationSound,
|
silent: !shouldPlayNotificationSound,
|
||||||
onNotificationClick: () => {
|
onNotificationClick: () => {
|
||||||
this.emit('click', conversationId, messageId);
|
this.emit('click', conversationId, messageId, storyId);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import type {
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
import type { NoopActionType } from './noop';
|
import type { NoopActionType } from './noop';
|
||||||
import type { StateType as RootStateType } from '../reducer';
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
import type { StoryViewType } from '../../types/Stories';
|
import type { StoryViewTargetType, StoryViewType } from '../../types/Stories';
|
||||||
import type { SyncType } from '../../jobs/helpers/syncHelpers';
|
import type { SyncType } from '../../jobs/helpers/syncHelpers';
|
||||||
import type { UUIDStringType } from '../../types/UUID';
|
import type { UUIDStringType } from '../../types/UUID';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
@ -86,7 +86,7 @@ export type SelectedStoryDataType = {
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
numStories: number;
|
numStories: number;
|
||||||
shouldShowDetailsModal: boolean;
|
viewTarget?: StoryViewTargetType;
|
||||||
storyViewMode: StoryViewModeType;
|
storyViewMode: StoryViewModeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -815,13 +815,13 @@ const getSelectedStoryDataForConversationId = (
|
||||||
|
|
||||||
export type ViewUserStoriesActionCreatorType = (opts: {
|
export type ViewUserStoriesActionCreatorType = (opts: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
shouldShowDetailsModal?: boolean;
|
viewTarget?: StoryViewTargetType;
|
||||||
storyViewMode?: StoryViewModeType;
|
storyViewMode?: StoryViewModeType;
|
||||||
}) => unknown;
|
}) => unknown;
|
||||||
|
|
||||||
const viewUserStories: ViewUserStoriesActionCreatorType = ({
|
const viewUserStories: ViewUserStoriesActionCreatorType = ({
|
||||||
conversationId,
|
conversationId,
|
||||||
shouldShowDetailsModal = false,
|
viewTarget,
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
}): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> => {
|
}): ThunkAction<void, RootStateType, unknown, ViewStoryActionType> => {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
|
@ -851,7 +851,7 @@ const viewUserStories: ViewUserStoriesActionCreatorType = ({
|
||||||
currentIndex,
|
currentIndex,
|
||||||
messageId: story.messageId,
|
messageId: story.messageId,
|
||||||
numStories,
|
numStories,
|
||||||
shouldShowDetailsModal,
|
viewTarget,
|
||||||
storyViewMode: inferredStoryViewMode,
|
storyViewMode: inferredStoryViewMode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -866,7 +866,7 @@ type ViewStoryOptionsType =
|
||||||
storyId: string;
|
storyId: string;
|
||||||
storyViewMode: StoryViewModeType;
|
storyViewMode: StoryViewModeType;
|
||||||
viewDirection?: StoryViewDirectionType;
|
viewDirection?: StoryViewDirectionType;
|
||||||
shouldShowDetailsModal?: boolean;
|
viewTarget?: StoryViewTargetType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ViewStoryActionCreatorType = (
|
export type ViewStoryActionCreatorType = (
|
||||||
|
@ -889,12 +889,7 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { viewTarget, storyId, storyViewMode, viewDirection } = opts;
|
||||||
shouldShowDetailsModal = false,
|
|
||||||
storyId,
|
|
||||||
storyViewMode,
|
|
||||||
viewDirection,
|
|
||||||
} = opts;
|
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const { stories } = state.stories;
|
const { stories } = state.stories;
|
||||||
|
@ -934,7 +929,7 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
currentIndex,
|
currentIndex,
|
||||||
messageId: storyId,
|
messageId: storyId,
|
||||||
numStories,
|
numStories,
|
||||||
shouldShowDetailsModal,
|
viewTarget,
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -955,7 +950,6 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
currentIndex: nextIndex,
|
currentIndex: nextIndex,
|
||||||
messageId: nextStory.messageId,
|
messageId: nextStory.messageId,
|
||||||
numStories,
|
numStories,
|
||||||
shouldShowDetailsModal: false,
|
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -973,7 +967,6 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
currentIndex: nextIndex,
|
currentIndex: nextIndex,
|
||||||
messageId: nextStory.messageId,
|
messageId: nextStory.messageId,
|
||||||
numStories,
|
numStories,
|
||||||
shouldShowDetailsModal: false,
|
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1022,7 +1015,6 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
nextSelectedStoryData.currentIndex
|
nextSelectedStoryData.currentIndex
|
||||||
].messageId,
|
].messageId,
|
||||||
numStories: nextSelectedStoryData.numStories,
|
numStories: nextSelectedStoryData.numStories,
|
||||||
shouldShowDetailsModal: false,
|
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1080,7 +1072,6 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
messageId: nextSelectedStoryData.storiesByConversationId[0].messageId,
|
messageId: nextSelectedStoryData.storiesByConversationId[0].messageId,
|
||||||
numStories: nextSelectedStoryData.numStories,
|
numStories: nextSelectedStoryData.numStories,
|
||||||
shouldShowDetailsModal: false,
|
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1115,7 +1106,6 @@ const viewStory: ViewStoryActionCreatorType = (
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
messageId: nextSelectedStoryData.storiesByConversationId[0].messageId,
|
messageId: nextSelectedStoryData.storiesByConversationId[0].messageId,
|
||||||
numStories: nextSelectedStoryData.numStories,
|
numStories: nextSelectedStoryData.numStories,
|
||||||
shouldShowDetailsModal: false,
|
|
||||||
storyViewMode,
|
storyViewMode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -117,7 +117,7 @@ export function SmartStoryViewer(): JSX.Element | null {
|
||||||
recentEmojis={recentEmojis}
|
recentEmojis={recentEmojis}
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
replyState={replyState}
|
replyState={replyState}
|
||||||
shouldShowDetailsModal={selectedStoryData.shouldShowDetailsModal}
|
viewTarget={selectedStoryData.viewTarget}
|
||||||
showToast={showToast}
|
showToast={showToast}
|
||||||
skinTone={skinTone}
|
skinTone={skinTone}
|
||||||
story={storyView}
|
story={storyView}
|
||||||
|
|
|
@ -148,7 +148,7 @@ describe('both/state/ducks/stories', () => {
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
messageId: storyId,
|
messageId: storyId,
|
||||||
numStories: 1,
|
numStories: 1,
|
||||||
shouldShowDetailsModal: false,
|
viewTarget: undefined,
|
||||||
storyViewMode: StoryViewModeType.All,
|
storyViewMode: StoryViewModeType.All,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -179,7 +179,6 @@ describe('both/state/ducks/stories', () => {
|
||||||
currentIndex: 1,
|
currentIndex: 1,
|
||||||
messageId: storyId2,
|
messageId: storyId2,
|
||||||
numStories: 3,
|
numStories: 3,
|
||||||
shouldShowDetailsModal: false,
|
|
||||||
storyViewMode: StoryViewModeType.User,
|
storyViewMode: StoryViewModeType.User,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -209,7 +208,6 @@ describe('both/state/ducks/stories', () => {
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
messageId: storyId1,
|
messageId: storyId1,
|
||||||
numStories: 3,
|
numStories: 3,
|
||||||
shouldShowDetailsModal: false,
|
|
||||||
storyViewMode: StoryViewModeType.User,
|
storyViewMode: StoryViewModeType.User,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -282,7 +280,6 @@ describe('both/state/ducks/stories', () => {
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
messageId: storyId3,
|
messageId: storyId3,
|
||||||
numStories: 1,
|
numStories: 1,
|
||||||
shouldShowDetailsModal: false,
|
|
||||||
storyViewMode: StoryViewModeType.Unread,
|
storyViewMode: StoryViewModeType.Unread,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -440,7 +437,6 @@ describe('both/state/ducks/stories', () => {
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
messageId: storyId2,
|
messageId: storyId2,
|
||||||
numStories: 2,
|
numStories: 2,
|
||||||
shouldShowDetailsModal: false,
|
|
||||||
storyViewMode: StoryViewModeType.All,
|
storyViewMode: StoryViewModeType.All,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -477,7 +473,6 @@ describe('both/state/ducks/stories', () => {
|
||||||
currentIndex: 0,
|
currentIndex: 0,
|
||||||
messageId: storyId1,
|
messageId: storyId1,
|
||||||
numStories: 2,
|
numStories: 2,
|
||||||
shouldShowDetailsModal: false,
|
|
||||||
storyViewMode: StoryViewModeType.All,
|
storyViewMode: StoryViewModeType.All,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -112,6 +112,12 @@ export enum StoryViewDirectionType {
|
||||||
Previous = 'Previous',
|
Previous = 'Previous',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum StoryViewTargetType {
|
||||||
|
Details = 'Details',
|
||||||
|
Views = 'Views',
|
||||||
|
Replies = 'Replies',
|
||||||
|
}
|
||||||
|
|
||||||
// Type of stories to view before closing the viewer
|
// Type of stories to view before closing the viewer
|
||||||
// All = All the stories in order
|
// All = All the stories in order
|
||||||
// Single = A single story. Like when clicking on a qouted story
|
// Single = A single story. Like when clicking on a qouted story
|
||||||
|
|
|
@ -15,27 +15,6 @@
|
||||||
"updated": "2018-09-18T19:19:27.699Z",
|
"updated": "2018-09-18T19:19:27.699Z",
|
||||||
"reasonDetail": "Part of runtime library for C++ transpiled code"
|
"reasonDetail": "Part of runtime library for C++ transpiled code"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/AddCaptionModal.tsx",
|
|
||||||
"line": " const scrollerRef = React.useRef<HTMLDivElement>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2022-10-03T16:06:12.837Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/CompositionInput.tsx",
|
|
||||||
"line": " const scrollerRefInner = React.useRef<HTMLDivElement>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2022-10-03T16:06:12.837Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/CompositionTextArea.tsx",
|
|
||||||
"line": " const inputApiRef = React.useRef<InputApi | undefined>();",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2022-10-03T16:06:12.837Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "components/mp3lameencoder/lib/Mp3LameEncoder.js",
|
"path": "components/mp3lameencoder/lib/Mp3LameEncoder.js",
|
||||||
|
@ -8858,6 +8837,13 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2021-05-05T23:11:22.692Z"
|
"updated": "2021-05-05T23:11:22.692Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/AddCaptionModal.tsx",
|
||||||
|
"line": " const scrollerRef = React.useRef<HTMLDivElement>(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2022-10-03T16:06:12.837Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/AvatarTextEditor.tsx",
|
"path": "ts/components/AvatarTextEditor.tsx",
|
||||||
|
@ -9029,6 +9015,20 @@
|
||||||
"updated": "2022-06-25T00:06:19.860Z",
|
"updated": "2022-06-25T00:06:19.860Z",
|
||||||
"reasonDetail": "Not used for DOM manipulation"
|
"reasonDetail": "Not used for DOM manipulation"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/CompositionInput.tsx",
|
||||||
|
"line": " const scrollerRefInner = React.useRef<HTMLDivElement>(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2022-10-03T16:06:12.837Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/CompositionTextArea.tsx",
|
||||||
|
"line": " const inputApiRef = React.useRef<InputApi | undefined>();",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2022-10-03T16:06:12.837Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/ContactPills.tsx",
|
"path": "ts/components/ContactPills.tsx",
|
||||||
|
@ -9316,13 +9316,6 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2022-08-04T00:52:01.080Z"
|
"updated": "2022-08-04T00:52:01.080Z"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/StoryViewsNRepliesModal.tsx",
|
|
||||||
"line": " const shouldScrollToBottomRef = useRef(false);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2022-09-22T03:07:22.153Z"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/TextAttachment.tsx",
|
"path": "ts/components/TextAttachment.tsx",
|
||||||
|
@ -9690,5 +9683,19 @@
|
||||||
"line": " message.innerHTML = window.SignalContext.i18n('optimizingApplication');",
|
"line": " message.innerHTML = window.SignalContext.i18n('optimizingApplication');",
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-09-17T21:02:59.414Z"
|
"updated": "2021-09-17T21:02:59.414Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/StoryViewsNRepliesModal.tsx",
|
||||||
|
"line": " const shouldScrollToBottomRef = useRef(true);",
|
||||||
|
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
|
||||||
|
"updated": "2022-10-05T18:51:56.411Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/StoryViewsNRepliesModal.tsx",
|
||||||
|
"line": " const bottomRef = useRef<HTMLDivElement>(null);",
|
||||||
|
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
|
||||||
|
"updated": "2022-10-05T18:51:56.411Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue