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