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",
"description": "Context menu item to help debugging"
},
"StoryDetailsModal__download-attachment": {
"message": "Download attachment",
"description": "Context menu item to help debugging"
},
"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.",
"description": "Instructions on how to enable read receipts"
@ -5947,6 +5951,10 @@
"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"
},
"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": {
"message": "Story",
"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 {
border-top: 1px solid $color-gray-75;
}

View file

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

View file

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

View file

@ -88,6 +88,7 @@ export type PropsType = {
hasViewReceiptSetting: boolean;
hasViewsCapability: boolean;
i18n: LocalizerType;
isInternalUser?: boolean;
group: Pick<ConversationType, 'left'> | undefined;
onClose: () => unknown;
onReact: (emoji: string) => unknown;
@ -120,6 +121,7 @@ export function StoryViewsNRepliesModal({
hasViewReceiptSetting,
hasViewsCapability,
i18n,
isInternalUser,
group,
onClose,
onReact,
@ -325,6 +327,7 @@ export function StoryViewsNRepliesModal({
<ReplyOrReactionMessage
key={reply.id}
i18n={i18n}
isInternalUser={isInternalUser}
reply={reply}
deleteGroupStoryReply={() => setDeleteReplyId(reply.id)}
deleteGroupStoryReplyForEveryone={() =>
@ -501,6 +504,7 @@ export function StoryViewsNRepliesModal({
type ReplyOrReactionMessageProps = {
i18n: LocalizerType;
isInternalUser?: boolean;
reply: ReplyType;
deleteGroupStoryReply: (replyId: string) => void;
deleteGroupStoryReplyForEveryone: (replyId: string) => void;
@ -513,6 +517,7 @@ type ReplyOrReactionMessageProps = {
function ReplyOrReactionMessage({
i18n,
isInternalUser,
reply,
deleteGroupStoryReply,
deleteGroupStoryReplyForEveryone,
@ -595,23 +600,31 @@ function ReplyOrReactionMessage({
);
};
const menuOptions = [
{
icon: 'module-message__context--icon module-message__context__delete-message',
label: i18n('icu:StoryViewsNRepliesModal__delete-reply'),
onClick: () => deleteGroupStoryReply(reply.id),
},
{
icon: 'module-message__context--icon module-message__context__delete-message-for-everyone',
label: i18n('icu:StoryViewsNRepliesModal__delete-reply-for-everyone'),
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={[
{
icon: 'module-message__context--icon module-message__context__delete-message',
label: i18n('icu:StoryViewsNRepliesModal__delete-reply'),
onClick: () => deleteGroupStoryReply(reply.id),
},
{
icon: 'module-message__context--icon module-message__context__delete-message-for-everyone',
label: i18n('icu:StoryViewsNRepliesModal__delete-reply-for-everyone'),
onClick: () => deleteGroupStoryReplyForEveryone(reply.id),
},
]}
>
<ContextMenu i18n={i18n} key={reply.id} menuOptions={menuOptions}>
{({ openMenu, menuNode }) => (
<>
{renderContent(openMenu)}

View file

@ -74,6 +74,13 @@ export const getUsernamesEnabled = createSelector(
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
export const getStoriesEnabled = createSelector(
getItems,

View file

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