Add more debug tools for stories

This commit is contained in:
Fedor Indutny 2022-11-22 14:33:15 -08:00 committed by GitHub
parent 4d1cd05888
commit 1bff385805
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 116 additions and 30 deletions

View file

@ -5907,6 +5907,10 @@
"message": "Copy timestamp", "message": "Copy timestamp",
"description": "Context menu item to help debugging" "description": "Context menu item to help debugging"
}, },
"StoryDetailsModal__download-attachment": {
"message": "Download attachment",
"description": "Context menu item to help debugging"
},
"StoryViewsNRepliesModal__read-receipts-off": { "StoryViewsNRepliesModal__read-receipts-off": {
"message": "Enable view receipts to see whos viewed your stories. Open the Signal app on your mobile device and navigate to Settings > Stories.", "message": "Enable view receipts to see whos viewed your stories. Open the Signal app on your mobile device and navigate to Settings > Stories.",
"description": "Instructions on how to enable read receipts" "description": "Instructions on how to enable read receipts"
@ -5947,6 +5951,10 @@
"messageformat": "Delete for everyone", "messageformat": "Delete for everyone",
"description": "Shown as a menu item in the context menu of a story reply, to the author of the reply, for deleting the reply for everyone" "description": "Shown as a menu item in the context menu of a story reply, to the author of the reply, for deleting the reply for everyone"
}, },
"icu:StoryViewsNRepliesModal__copy-reply-timestamp": {
"messageformat": "Copy timestamp",
"description": "Shown for internal users as a menu item in the context menu of a story reply, to the author of the reply, for copying the reply timestamp"
},
"StoryListItem__label": { "StoryListItem__label": {
"message": "Story", "message": "Story",
"description": "aria-label for the story list button" "description": "aria-label for the story list button"

View file

@ -8160,4 +8160,20 @@ button.module-image__border-overlay:focus {
); );
} }
} }
&__copy-timestamp::before {
@include light-theme {
@include color-svg(
'../images/icons/v2/copy-outline-24.svg',
$color-black
);
}
@include dark-theme {
@include color-svg(
'../images/icons/v2/copy-outline-24.svg',
$color-gray-15
);
}
}
} }

View file

@ -41,6 +41,15 @@
} }
} }
&__download-icon {
@include dark-theme {
@include color-svg('../images/icons/v2/arrow-down-24.svg', $color-white);
}
@include light-theme {
@include color-svg('../images/icons/v2/arrow-down-24.svg', $color-black);
}
}
&__contact-container { &__contact-container {
border-top: 1px solid $color-gray-75; border-top: 1px solid $color-gray-75;
} }

View file

@ -15,6 +15,8 @@ import { SendStatus } from '../messages/MessageSendState';
import { Theme } from '../util/theme'; import { Theme } from '../util/theme';
import { formatDateTimeLong } from '../util/timestamp'; import { formatDateTimeLong } from '../util/timestamp';
import { DurationInSeconds } from '../util/durations'; import { DurationInSeconds } from '../util/durations';
import type { saveAttachment } from '../util/saveAttachment';
import type { AttachmentType } from '../types/Attachment';
import { ThemeType } from '../types/Util'; import { ThemeType } from '../types/Util';
import { Time } from './Time'; import { Time } from './Time';
import { groupBy } from '../util/mapUtil'; import { groupBy } from '../util/mapUtil';
@ -23,10 +25,12 @@ import { format as formatRelativeTime } from '../util/expirationTimer';
export type PropsType = { export type PropsType = {
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType; i18n: LocalizerType;
isInternalUser?: boolean;
onClose: () => unknown; onClose: () => unknown;
saveAttachment: typeof saveAttachment;
sender: StoryViewType['sender']; sender: StoryViewType['sender'];
sendState?: Array<StorySendStateType>; sendState?: Array<StorySendStateType>;
size?: number; attachment?: AttachmentType;
expirationTimestamp: number | undefined; expirationTimestamp: number | undefined;
timestamp: number; timestamp: number;
}; };
@ -62,12 +66,14 @@ function getI18nKey(sendStatus: SendStatus | undefined): string {
} }
export function StoryDetailsModal({ export function StoryDetailsModal({
attachment,
getPreferredBadge, getPreferredBadge,
i18n, i18n,
isInternalUser,
onClose, onClose,
saveAttachment,
sender, sender,
sendState, sendState,
size,
timestamp, timestamp,
expirationTimestamp, expirationTimestamp,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
@ -193,6 +199,26 @@ export function StoryDetailsModal({
? DurationInSeconds.fromMillis(expirationTimestamp - Date.now()) ? DurationInSeconds.fromMillis(expirationTimestamp - Date.now())
: undefined; : undefined;
const menuOptions = [
{
icon: 'StoryDetailsModal__copy-icon',
label: i18n('StoryDetailsModal__copy-timestamp'),
onClick: () => {
window.navigator.clipboard.writeText(String(timestamp));
},
},
];
if (isInternalUser && attachment) {
menuOptions.push({
icon: 'StoryDetailsModal__download-icon',
label: i18n('StoryDetailsModal__download-attachment'),
onClick: () => {
saveAttachment(attachment);
},
});
}
return ( return (
<Modal <Modal
modalName="StoryDetailsModal" modalName="StoryDetailsModal"
@ -205,15 +231,7 @@ export function StoryDetailsModal({
title={ title={
<ContextMenu <ContextMenu
i18n={i18n} i18n={i18n}
menuOptions={[ menuOptions={menuOptions}
{
icon: 'StoryDetailsModal__copy-icon',
label: i18n('StoryDetailsModal__copy-timestamp'),
onClick: () => {
window.navigator.clipboard.writeText(String(timestamp));
},
},
]}
moduleClassName="StoryDetailsModal__debugger" moduleClassName="StoryDetailsModal__debugger"
popperOptions={{ popperOptions={{
placement: 'bottom', placement: 'bottom',
@ -235,14 +253,14 @@ export function StoryDetailsModal({
]} ]}
/> />
</div> </div>
{size && ( {attachment && (
<div> <div>
<Intl <Intl
i18n={i18n} i18n={i18n}
id="StoryDetailsModal__file-size" id="StoryDetailsModal__file-size"
components={[ components={[
<span className="StoryDetailsModal__debugger__button__text"> <span className="StoryDetailsModal__debugger__button__text">
{formatFileSize(size)} {formatFileSize(attachment.size)}
</span>, </span>,
]} ]}
/> />

View file

@ -45,6 +45,7 @@ import { getAvatarColor } from '../types/Colors';
import { getStoryBackground } from '../util/getStoryBackground'; import { getStoryBackground } from '../util/getStoryBackground';
import { getStoryDuration } from '../util/getStoryDuration'; import { getStoryDuration } from '../util/getStoryDuration';
import { graphemeAwareSlice } from '../util/graphemeAwareSlice'; import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
import type { saveAttachment } from '../util/saveAttachment';
import { isVideoAttachment } from '../types/Attachment'; import { isVideoAttachment } from '../types/Attachment';
import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useRetryStorySend } from '../hooks/useRetryStorySend'; import { useRetryStorySend } from '../hooks/useRetryStorySend';
@ -75,6 +76,7 @@ export type PropsType = {
hasAllStoriesUnmuted: boolean; hasAllStoriesUnmuted: boolean;
hasViewReceiptSetting: boolean; hasViewReceiptSetting: boolean;
i18n: LocalizerType; i18n: LocalizerType;
isInternalUser?: boolean;
isSignalConversation?: boolean; isSignalConversation?: boolean;
isWindowActive: boolean; isWindowActive: boolean;
loadStoryReplies: (conversationId: string, messageId: string) => unknown; loadStoryReplies: (conversationId: string, messageId: string) => unknown;
@ -98,6 +100,7 @@ export type PropsType = {
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element; renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
replyState?: ReplyStateType; replyState?: ReplyStateType;
retrySend: (messageId: string) => unknown; retrySend: (messageId: string) => unknown;
saveAttachment: typeof saveAttachment;
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown; setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
showToast: ShowToastActionCreatorType; showToast: ShowToastActionCreatorType;
skinTone?: number; skinTone?: number;
@ -130,6 +133,7 @@ export function StoryViewer({
hasAllStoriesUnmuted, hasAllStoriesUnmuted,
hasViewReceiptSetting, hasViewReceiptSetting,
i18n, i18n,
isInternalUser,
isSignalConversation, isSignalConversation,
isWindowActive, isWindowActive,
loadStoryReplies, loadStoryReplies,
@ -148,6 +152,7 @@ export function StoryViewer({
renderEmojiPicker, renderEmojiPicker,
replyState, replyState,
retrySend, retrySend,
saveAttachment,
setHasAllStoriesUnmuted, setHasAllStoriesUnmuted,
showToast, showToast,
skinTone, skinTone,
@ -886,12 +891,14 @@ export function StoryViewer({
</div> </div>
{currentViewTarget === StoryViewTargetType.Details && ( {currentViewTarget === StoryViewTargetType.Details && (
<StoryDetailsModal <StoryDetailsModal
attachment={attachment}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
i18n={i18n} i18n={i18n}
isInternalUser={isInternalUser}
onClose={() => setCurrentViewTarget(null)} onClose={() => setCurrentViewTarget(null)}
saveAttachment={saveAttachment}
sender={story.sender} sender={story.sender}
sendState={sendState} sendState={sendState}
size={attachment?.size}
timestamp={timestamp} timestamp={timestamp}
expirationTimestamp={story.expirationTimestamp} expirationTimestamp={story.expirationTimestamp}
/> />
@ -905,6 +912,7 @@ export function StoryViewer({
hasViewReceiptSetting={hasViewReceiptSetting} hasViewReceiptSetting={hasViewReceiptSetting}
hasViewsCapability={isSent} hasViewsCapability={isSent}
i18n={i18n} i18n={i18n}
isInternalUser={isInternalUser}
group={group} group={group}
onClose={() => setCurrentViewTarget(null)} onClose={() => setCurrentViewTarget(null)}
onReact={emoji => { onReact={emoji => {

View file

@ -88,6 +88,7 @@ export type PropsType = {
hasViewReceiptSetting: boolean; hasViewReceiptSetting: boolean;
hasViewsCapability: boolean; hasViewsCapability: boolean;
i18n: LocalizerType; i18n: LocalizerType;
isInternalUser?: boolean;
group: Pick<ConversationType, 'left'> | undefined; group: Pick<ConversationType, 'left'> | undefined;
onClose: () => unknown; onClose: () => unknown;
onReact: (emoji: string) => unknown; onReact: (emoji: string) => unknown;
@ -120,6 +121,7 @@ export function StoryViewsNRepliesModal({
hasViewReceiptSetting, hasViewReceiptSetting,
hasViewsCapability, hasViewsCapability,
i18n, i18n,
isInternalUser,
group, group,
onClose, onClose,
onReact, onReact,
@ -325,6 +327,7 @@ export function StoryViewsNRepliesModal({
<ReplyOrReactionMessage <ReplyOrReactionMessage
key={reply.id} key={reply.id}
i18n={i18n} i18n={i18n}
isInternalUser={isInternalUser}
reply={reply} reply={reply}
deleteGroupStoryReply={() => setDeleteReplyId(reply.id)} deleteGroupStoryReply={() => setDeleteReplyId(reply.id)}
deleteGroupStoryReplyForEveryone={() => deleteGroupStoryReplyForEveryone={() =>
@ -501,6 +504,7 @@ export function StoryViewsNRepliesModal({
type ReplyOrReactionMessageProps = { type ReplyOrReactionMessageProps = {
i18n: LocalizerType; i18n: LocalizerType;
isInternalUser?: boolean;
reply: ReplyType; reply: ReplyType;
deleteGroupStoryReply: (replyId: string) => void; deleteGroupStoryReply: (replyId: string) => void;
deleteGroupStoryReplyForEveryone: (replyId: string) => void; deleteGroupStoryReplyForEveryone: (replyId: string) => void;
@ -513,6 +517,7 @@ type ReplyOrReactionMessageProps = {
function ReplyOrReactionMessage({ function ReplyOrReactionMessage({
i18n, i18n,
isInternalUser,
reply, reply,
deleteGroupStoryReply, deleteGroupStoryReply,
deleteGroupStoryReplyForEveryone, deleteGroupStoryReplyForEveryone,
@ -595,11 +600,7 @@ function ReplyOrReactionMessage({
); );
}; };
return reply.author.isMe && !reply.deletedForEveryone ? ( const menuOptions = [
<ContextMenu
i18n={i18n}
key={reply.id}
menuOptions={[
{ {
icon: 'module-message__context--icon module-message__context__delete-message', icon: 'module-message__context--icon module-message__context__delete-message',
label: i18n('icu:StoryViewsNRepliesModal__delete-reply'), label: i18n('icu:StoryViewsNRepliesModal__delete-reply'),
@ -610,8 +611,20 @@ function ReplyOrReactionMessage({
label: i18n('icu:StoryViewsNRepliesModal__delete-reply-for-everyone'), label: i18n('icu:StoryViewsNRepliesModal__delete-reply-for-everyone'),
onClick: () => deleteGroupStoryReplyForEveryone(reply.id), onClick: () => deleteGroupStoryReplyForEveryone(reply.id),
}, },
]} ];
>
if (isInternalUser) {
menuOptions.push({
icon: 'module-message__context--icon module-message__context__copy-timestamp',
label: i18n('icu:StoryViewsNRepliesModal__copy-reply-timestamp'),
onClick: () => {
window.navigator.clipboard.writeText(String(reply.timestamp));
},
});
}
return reply.author.isMe && !reply.deletedForEveryone ? (
<ContextMenu i18n={i18n} key={reply.id} menuOptions={menuOptions}>
{({ openMenu, menuNode }) => ( {({ openMenu, menuNode }) => (
<> <>
{renderContent(openMenu)} {renderContent(openMenu)}

View file

@ -74,6 +74,13 @@ export const getUsernamesEnabled = createSelector(
isRemoteConfigFlagEnabled(remoteConfig, 'desktop.usernames') isRemoteConfigFlagEnabled(remoteConfig, 'desktop.usernames')
); );
export const isInternalUser = createSelector(
getRemoteConfig,
(remoteConfig: ConfigMapType): boolean => {
return isRemoteConfigFlagEnabled(remoteConfig, 'desktop.internalUser');
}
);
// Note: ts/util/stories is the other place this check is done // Note: ts/util/stories is the other place this check is done
export const getStoriesEnabled = createSelector( export const getStoriesEnabled = createSelector(
getItems, getItems,

View file

@ -15,6 +15,7 @@ import {
getEmojiSkinTone, getEmojiSkinTone,
getHasStoryViewReceiptSetting, getHasStoryViewReceiptSetting,
getPreferredReactionEmoji, getPreferredReactionEmoji,
isInternalUser,
} from '../selectors/items'; } from '../selectors/items';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { getPreferredBadgeSelector } from '../selectors/badges'; import { getPreferredBadgeSelector } from '../selectors/badges';
@ -28,7 +29,9 @@ import { isInFullScreenCall } from '../selectors/calling';
import { isSignalConversation } from '../../util/isSignalConversation'; import { isSignalConversation } from '../../util/isSignalConversation';
import { renderEmojiPicker } from './renderEmojiPicker'; import { renderEmojiPicker } from './renderEmojiPicker';
import { retryMessageSend } from '../../util/retryMessageSend'; import { retryMessageSend } from '../../util/retryMessageSend';
import { saveAttachment } from '../../util/saveAttachment';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { asyncShouldNeverBeCalled } from '../../util/shouldNeverBeCalled';
import { useActions as useEmojisActions } from '../ducks/emojis'; import { useActions as useEmojisActions } from '../ducks/emojis';
import { useConversationsActions } from '../ducks/conversations'; import { useConversationsActions } from '../ducks/conversations';
import { useRecentEmojis } from '../selectors/emojis'; import { useRecentEmojis } from '../selectors/emojis';
@ -56,6 +59,8 @@ export function SmartStoryViewer(): JSX.Element | null {
SelectedStoryDataType | undefined SelectedStoryDataType | undefined
>(getSelectedStoryData); >(getSelectedStoryData);
const internalUser = useSelector<StateType, boolean>(isInternalUser);
strictAssert(selectedStoryData, 'StoryViewer: !selectedStoryData'); strictAssert(selectedStoryData, 'StoryViewer: !selectedStoryData');
const conversationSelector = useSelector<StateType, GetConversationByIdType>( const conversationSelector = useSelector<StateType, GetConversationByIdType>(
@ -97,6 +102,8 @@ export function SmartStoryViewer(): JSX.Element | null {
hasAllStoriesUnmuted={hasAllStoriesUnmuted} hasAllStoriesUnmuted={hasAllStoriesUnmuted}
hasViewReceiptSetting={hasViewReceiptSetting} hasViewReceiptSetting={hasViewReceiptSetting}
i18n={i18n} i18n={i18n}
isInternalUser={internalUser}
saveAttachment={internalUser ? saveAttachment : asyncShouldNeverBeCalled}
isSignalConversation={isSignalConversation({ isSignalConversation={isSignalConversation({
id: conversationStory.conversationId, id: conversationStory.conversationId,
})} })}