Notify story creator for replies

This commit is contained in:
Jamie Kyle 2022-10-11 10:59:02 -07:00 committed by GitHub
parent 512d655d32
commit 25bc16300c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 250 additions and 135 deletions

View file

@ -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();
} }

View file

@ -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,
}); });
}, },
}, },

View file

@ -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',

View file

@ -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 && (

View file

@ -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 = {};

View file

@ -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}

View file

@ -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 (
<> <>

View file

@ -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();
} }

View file

@ -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(),

View file

@ -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);
} }

View file

@ -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);
}, },
}); });
} }

View file

@ -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,
}, },
}); });

View file

@ -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}

View file

@ -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,
}, },
}); });

View file

@ -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

View file

@ -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"
} }
] ]