Add more debug tools for stories
This commit is contained in:
parent
4d1cd05888
commit
1bff385805
8 changed files with 116 additions and 30 deletions
|
@ -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 who’s 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"
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
})}
|
||||
|
|
Loading…
Reference in a new issue