Onboarding story

This commit is contained in:
Josh Perez 2022-11-08 21:38:19 -05:00 committed by GitHub
parent 94f318ea08
commit 19a42ed719
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 725 additions and 143 deletions

View file

@ -399,6 +399,7 @@ async function prepareUrl(
serverUrl: config.get<string>('serverUrl'), serverUrl: config.get<string>('serverUrl'),
storageUrl: config.get<string>('storageUrl'), storageUrl: config.get<string>('storageUrl'),
updatesUrl: config.get<string>('updatesUrl'), updatesUrl: config.get<string>('updatesUrl'),
resourcesUrl: config.get<string>('resourcesUrl'),
cdnUrl0: config.get<ConfigType>('cdn').get<string>('0'), cdnUrl0: config.get<ConfigType>('cdn').get<string>('0'),
cdnUrl2: config.get<ConfigType>('cdn').get<string>('2'), cdnUrl2: config.get<ConfigType>('cdn').get<string>('2'),
certificateAuthority: config.get<string>('certificateAuthority'), certificateAuthority: config.get<string>('certificateAuthority'),

View file

@ -9,6 +9,7 @@
}, },
"contentProxyUrl": "http://contentproxy.signal.org:443", "contentProxyUrl": "http://contentproxy.signal.org:443",
"updatesUrl": "https://updates2.signal.org/desktop", "updatesUrl": "https://updates2.signal.org/desktop",
"resourcesUrl": "https://updates2.signal.org",
"updatesPublicKey": "05fd7dd3de7149dc0a127909fee7de0f7620ddd0de061b37a2c303e37de802a401", "updatesPublicKey": "05fd7dd3de7149dc0a127909fee7de0f7620ddd0de061b37a2c303e37de802a401",
"sfuUrl": "https://sfu.voip.signal.org/", "sfuUrl": "https://sfu.voip.signal.org/",
"updatesEnabled": false, "updatesEnabled": false,

View file

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.66184 2.45477C9.38212 1.67172 10.6179 1.67172 11.3382 2.45477L11.4676 2.59553C11.8981 3.06346 12.5416 3.27257 13.1649 3.147L13.3524 3.10923C14.3954 2.8991 15.3951 3.62547 15.5176 4.68233L15.5396 4.87232C15.6128 5.50387 16.0105 6.05133 16.5885 6.31608L16.7624 6.39573C17.7297 6.83878 18.1116 8.01406 17.5895 8.94106L17.4956 9.1077C17.1836 9.66165 17.1836 10.3383 17.4956 10.8923L17.5895 11.0589C18.1116 11.9859 17.7297 13.1612 16.7624 13.6043L16.5885 13.6839C16.0105 13.9487 15.6128 14.4961 15.5396 15.1277L15.5176 15.3177C15.3951 16.3745 14.3954 17.1009 13.3524 16.8908L13.1649 16.853C12.5416 16.7274 11.8981 16.9365 11.4676 17.4045L11.3382 17.5452C10.6179 18.3283 9.38212 18.3283 8.66184 17.5452L8.53236 17.4045C8.10194 16.9365 7.45837 16.7274 6.83511 16.853L6.64762 16.8908C5.60464 17.1009 4.60489 16.3745 4.48243 15.3177L4.46042 15.1277C4.38724 14.4961 3.98949 13.9487 3.41146 13.6839L3.23757 13.6043C2.27027 13.1612 1.8884 11.9859 2.41054 11.0589L2.50441 10.8923C2.81642 10.3383 2.81642 9.66165 2.50441 9.1077L2.41054 8.94106C1.8884 8.01406 2.27027 6.83878 3.23757 6.39573L3.41146 6.31608C3.98949 6.05133 4.38724 5.50387 4.46042 4.87232L4.48243 4.68233C4.60489 3.62546 5.60464 2.8991 6.64762 3.10923L6.83511 3.147C7.45837 3.27257 8.10194 3.06346 8.53236 2.59553L8.66184 2.45477Z" fill="#2C6BED"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1264 8.15304L13.1725 7.19922L9.07735 11.2948L7.13023 9.34766L6.17639 10.3015L9.07739 13.202L14.1264 8.15304Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -169,7 +169,7 @@ message AccountRecord {
optional bool displayBadgesOnProfile = 23; optional bool displayBadgesOnProfile = 23;
optional bool keepMutedChatsArchived = 25; optional bool keepMutedChatsArchived = 25;
optional bool hasSetMyStoriesPrivacy = 26; optional bool hasSetMyStoriesPrivacy = 26;
reserved /* hasViewedOnboardingStory */ 27; optional bool hasViewedOnboardingStory = 27;
reserved 28; // deprecatedStoriesDisabled reserved 28; // deprecatedStoriesDisabled
optional bool storiesDisabled = 29; optional bool storiesDisabled = 29;
optional OptionalBool storyViewReceiptsEnabled = 30; optional OptionalBool storyViewReceiptsEnabled = 30;

View file

@ -30,6 +30,7 @@ const WebAPI = initializeWebAPI({
url: config.serverUrl, url: config.serverUrl,
storageUrl: config.storageUrl, storageUrl: config.storageUrl,
updatesUrl: config.updatesUrl, updatesUrl: config.updatesUrl,
resourcesUrl: config.resourcesUrl,
directoryConfig: config.directoryConfig, directoryConfig: config.directoryConfig,
cdnUrlObject: { cdnUrlObject: {
0: config.cdnUrl0, 0: config.cdnUrl0,

View file

@ -160,4 +160,18 @@
border-color: $color-ultramarine-dawn; border-color: $color-ultramarine-dawn;
} }
} }
&--signal-official {
.module-Avatar__contents {
align-items: center;
background-color: $color-ultramarine;
display: flex;
justify-content: center;
}
.module-Avatar__image {
height: 66%;
width: 66%;
}
}
} }

View file

@ -58,6 +58,8 @@
&--title { &--title {
@include font-body-1-bold; @include font-body-1-bold;
color: $color-gray-05; color: $color-gray-05;
display: flex;
align-items: center;
} }
&--timestamp, &--timestamp,
@ -175,4 +177,12 @@
width: 12px; width: 12px;
@include color-svg('../images/icons/v2/chevron-right-20.svg', $color-white); @include color-svg('../images/icons/v2/chevron-right-20.svg', $color-white);
} }
&__signal-official {
background: url('../images/icons/v2/official-20.svg') no-repeat center;
display: inline-block;
height: 16px;
margin-left: 6px;
width: 16px;
}
} }

View file

@ -28,6 +28,7 @@ import { sleep } from './util/sleep';
import { isNotNil } from './util/isNotNil'; import { isNotNil } from './util/isNotNil';
import { MINUTE, SECOND } from './util/durations'; import { MINUTE, SECOND } from './util/durations';
import { getUuidsForE164s } from './util/getUuidsForE164s'; import { getUuidsForE164s } from './util/getUuidsForE164s';
import { SIGNAL_ACI, SIGNAL_AVATAR_PATH } from './types/Conversation';
type ConvoMatchType = type ConvoMatchType =
| { | {
@ -129,8 +130,8 @@ const {
export function start(): void { export function start(): void {
const conversations = new window.Whisper.ConversationCollection(); const conversations = new window.Whisper.ConversationCollection();
window.getConversations = () => conversations;
window.ConversationController = new ConversationController(conversations); window.ConversationController = new ConversationController(conversations);
window.getConversations = () => conversations;
} }
export class ConversationController { export class ConversationController {
@ -144,6 +145,8 @@ export class ConversationController {
private _combineConversationsQueue = new PQueue({ concurrency: 1 }); private _combineConversationsQueue = new PQueue({ concurrency: 1 });
private _signalConversationId: undefined | string;
constructor(private _conversations: ConversationModelCollectionType) { constructor(private _conversations: ConversationModelCollectionType) {
const debouncedUpdateUnreadCount = debounce( const debouncedUpdateUnreadCount = debounce(
this.updateUnreadCount.bind(this), this.updateUnreadCount.bind(this),
@ -406,6 +409,43 @@ export class ConversationController {
return conversation; return conversation;
} }
getOrCreateSignalConversation(): ConversationModel {
const conversation = this.getOrCreate(SIGNAL_ACI, 'private', {
muteExpiresAt: Number.MAX_SAFE_INTEGER,
profileAvatar: { path: SIGNAL_AVATAR_PATH },
profileName: 'Signal',
profileSharing: true,
});
this._signalConversationId = conversation.id;
return conversation;
}
getSignalConversationId(): string {
if (this._signalConversationId) {
return this._signalConversationId;
}
let conversation = this.get(SIGNAL_ACI);
if (!conversation) {
conversation = this.getOrCreateSignalConversation();
}
this._signalConversationId = conversation.id;
return conversation.id;
}
isSignalConversation(uuidOrId: string): boolean {
if (uuidOrId === SIGNAL_ACI) {
return true;
}
return this.getSignalConversationId() === uuidOrId;
}
areWePrimaryDevice(): boolean { areWePrimaryDevice(): boolean {
const ourDeviceId = window.textsecure.storage.user.getDeviceId(); const ourDeviceId = window.textsecure.storage.user.getDeviceId();

View file

@ -156,6 +156,7 @@ 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'; import { StoryViewModeType, StoryViewTargetType } from './types/Stories';
import { downloadOnboardingStory } from './util/downloadOnboardingStory';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
@ -1031,6 +1032,7 @@ export async function startApp(): Promise<void> {
(async () => { (async () => {
menuOptions = await window.SignalContext.getMenuOptions(); menuOptions = await window.SignalContext.getMenuOptions();
})(), })(),
downloadOnboardingStory(),
]); ]);
await window.ConversationController.checkForConflicts(); await window.ConversationController.checkForConflicts();
} catch (error) { } catch (error) {

View file

@ -25,6 +25,7 @@ import { getBadgeImageFileLocalPath } from '../badges/getBadgeImageFileLocalPath
import { getInitials } from '../util/getInitials'; import { getInitials } from '../util/getInitials';
import { isBadgeVisible } from '../badges/isBadgeVisible'; import { isBadgeVisible } from '../badges/isBadgeVisible';
import { shouldBlurAvatar } from '../util/shouldBlurAvatar'; import { shouldBlurAvatar } from '../util/shouldBlurAvatar';
import { SIGNAL_AVATAR_PATH } from '../types/Conversation';
export enum AvatarBlur { export enum AvatarBlur {
NoBlur, NoBlur,
@ -295,7 +296,10 @@ export const Avatar: FunctionComponent<Props> = ({
'module-Avatar', 'module-Avatar',
Boolean(storyRing) && 'module-Avatar--with-story', Boolean(storyRing) && 'module-Avatar--with-story',
storyRing === HasStories.Unread && 'module-Avatar--with-story--unread', storyRing === HasStories.Unread && 'module-Avatar--with-story--unread',
className className,
avatarPath === SIGNAL_AVATAR_PATH
? 'module-Avatar--signal-official'
: undefined
)} )}
style={{ style={{
minWidth: size, minWidth: size,

View file

@ -92,6 +92,7 @@ export type OwnProps = Readonly<{
) => unknown; ) => unknown;
compositionApi?: MutableRefObject<CompositionAPIType>; compositionApi?: MutableRefObject<CompositionAPIType>;
conversationId: string; conversationId: string;
uuid?: string;
draftAttachments: ReadonlyArray<AttachmentDraftType>; draftAttachments: ReadonlyArray<AttachmentDraftType>;
errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType; errorDialogAudioRecorderType?: ErrorDialogAudioRecorderType;
errorRecording: (e: ErrorDialogAudioRecorderType) => unknown; errorRecording: (e: ErrorDialogAudioRecorderType) => unknown;
@ -101,6 +102,7 @@ export type OwnProps = Readonly<{
isFetchingUUID?: boolean; isFetchingUUID?: boolean;
isGroupV1AndDisabled?: boolean; isGroupV1AndDisabled?: boolean;
isMissingMandatoryProfileSharing?: boolean; isMissingMandatoryProfileSharing?: boolean;
isSignalConversation?: boolean;
recordingState: RecordingState; recordingState: RecordingState;
isSMSOnly?: boolean; isSMSOnly?: boolean;
left?: boolean; left?: boolean;
@ -176,6 +178,7 @@ export const CompositionArea = ({
processAttachments, processAttachments,
removeAttachment, removeAttachment,
theme, theme,
isSignalConversation,
// AttachmentList // AttachmentList
draftAttachments, draftAttachments,
@ -481,6 +484,11 @@ export const CompositionArea = ({
}; };
}, [setLarge]); }, [setLarge]);
if (isSignalConversation) {
// TODO DESKTOP-4547
return <div />;
}
if ( if (
isBlocked || isBlocked ||
areWePending || areWePending ||

View file

@ -30,8 +30,11 @@ export type PropsType = {
addStoryData: AddStoryData; addStoryData: AddStoryData;
deleteStoryForEveryone: (story: StoryViewType) => unknown; deleteStoryForEveryone: (story: StoryViewType) => unknown;
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
hasViewReceiptSetting: boolean;
hiddenStories: Array<ConversationStoryType>; hiddenStories: Array<ConversationStoryType>;
i18n: LocalizerType; i18n: LocalizerType;
isStoriesSettingsVisible: boolean;
isViewingStory: boolean;
me: ConversationType; me: ConversationType;
myStories: Array<MyStoryType>; myStories: Array<MyStoryType>;
onForwardStory: (storyId: string) => unknown; onForwardStory: (storyId: string) => unknown;
@ -46,19 +49,19 @@ export type PropsType = {
stories: Array<ConversationStoryType>; stories: Array<ConversationStoryType>;
toggleHideStories: (conversationId: string) => unknown; toggleHideStories: (conversationId: string) => unknown;
toggleStoriesView: () => unknown; toggleStoriesView: () => unknown;
viewUserStories: ViewUserStoriesActionCreatorType;
viewStory: ViewStoryActionCreatorType; viewStory: ViewStoryActionCreatorType;
isViewingStory: boolean; viewUserStories: ViewUserStoriesActionCreatorType;
isStoriesSettingsVisible: boolean;
hasViewReceiptSetting: boolean;
}; };
export const Stories = ({ export const Stories = ({
addStoryData, addStoryData,
deleteStoryForEveryone, deleteStoryForEveryone,
getPreferredBadge, getPreferredBadge,
hasViewReceiptSetting,
hiddenStories, hiddenStories,
i18n, i18n,
isStoriesSettingsVisible,
isViewingStory,
me, me,
myStories, myStories,
onForwardStory, onForwardStory,
@ -73,11 +76,8 @@ export const Stories = ({
stories, stories,
toggleHideStories, toggleHideStories,
toggleStoriesView, toggleStoriesView,
viewUserStories,
viewStory, viewStory,
isViewingStory, viewUserStories,
isStoriesSettingsVisible,
hasViewReceiptSetting,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const width = getWidthFromPreferredWidth(preferredWidthFromStorage, { const width = getWidthFromPreferredWidth(preferredWidthFromStorage, {
requiresFullWidth: true, requiresFullWidth: true,

View file

@ -3,15 +3,16 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
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 { ConversationType } from '../state/ducks/conversations';
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 { SIGNAL_ACI } from '../types/Conversation';
import { StoryViewTargetType, HasStories } from '../types/Stories';
import { MessageTimestamp } from './conversation/MessageTimestamp'; import { MessageTimestamp } from './conversation/MessageTimestamp';
import { StoryImage } from './StoryImage'; import { StoryImage } from './StoryImage';
@ -97,6 +98,8 @@ export const StoryListItem = ({
const { firstName, title } = sender; const { firstName, title } = sender;
const isSignalOfficial = sender.uuid === SIGNAL_ACI;
let avatarStoryRing: HasStories | undefined; let avatarStoryRing: HasStories | undefined;
if (attachment) { if (attachment) {
avatarStoryRing = isUnread ? HasStories.Unread : HasStories.Read; avatarStoryRing = isUnread ? HasStories.Unread : HasStories.Read;
@ -109,12 +112,7 @@ export const StoryListItem = ({
repliesElement = <div className="StoryListItem__info--replies--others" />; repliesElement = <div className="StoryListItem__info--replies--others" />;
} }
return ( const menuOptions = [
<>
<ContextMenu
aria-label={i18n('StoryListItem__label')}
i18n={i18n}
menuOptions={[
{ {
icon: 'StoryListItem__icon--hide', icon: 'StoryListItem__icon--hide',
label: isHidden label: isHidden
@ -128,7 +126,10 @@ export const StoryListItem = ({
} }
}, },
}, },
{ ];
if (!isSignalOfficial) {
menuOptions.push({
icon: 'StoryListItem__icon--info', icon: 'StoryListItem__icon--info',
label: i18n('StoryListItem__info'), label: i18n('StoryListItem__info'),
onClick: () => onClick: () =>
@ -136,13 +137,21 @@ export const StoryListItem = ({
conversationId, conversationId,
viewTarget: StoryViewTargetType.Details, viewTarget: StoryViewTargetType.Details,
}), }),
}, });
{
menuOptions.push({
icon: 'StoryListItem__icon--chat', icon: 'StoryListItem__icon--chat',
label: i18n('StoryListItem__go-to-chat'), label: i18n('StoryListItem__go-to-chat'),
onClick: () => onGoToConversation(conversationId), onClick: () => onGoToConversation(conversationId),
}, });
]} }
return (
<>
<ContextMenu
aria-label={i18n('StoryListItem__label')}
i18n={i18n}
menuOptions={menuOptions}
moduleClassName={classNames('StoryListItem', { moduleClassName={classNames('StoryListItem', {
'StoryListItem--hidden': isHidden, 'StoryListItem--hidden': isHidden,
})} })}
@ -162,13 +171,18 @@ export const StoryListItem = ({
<> <>
<div className="StoryListItem__info--title"> <div className="StoryListItem__info--title">
{group ? group.title : title} {group ? group.title : title}
{isSignalOfficial && (
<span className="StoryListItem__signal-official" />
)}
</div> </div>
{!isSignalOfficial && (
<MessageTimestamp <MessageTimestamp
i18n={i18n} i18n={i18n}
isRelativeTime isRelativeTime
module="StoryListItem__info--timestamp" module="StoryListItem__info--timestamp"
timestamp={timestamp} timestamp={timestamp}
/> />
)}
</> </>
{repliesElement} {repliesElement}
</div> </div>

View file

@ -69,6 +69,7 @@ export type PropsType = {
hasAllStoriesMuted: boolean; hasAllStoriesMuted: boolean;
hasViewReceiptSetting: boolean; hasViewReceiptSetting: boolean;
i18n: LocalizerType; i18n: LocalizerType;
isSignalConversation?: boolean;
loadStoryReplies: (conversationId: string, messageId: string) => unknown; loadStoryReplies: (conversationId: string, messageId: string) => unknown;
markStoryRead: (mId: string) => unknown; markStoryRead: (mId: string) => unknown;
numStories: number; numStories: number;
@ -121,6 +122,7 @@ export const StoryViewer = ({
hasAllStoriesMuted, hasAllStoriesMuted,
hasViewReceiptSetting, hasViewReceiptSetting,
i18n, i18n,
isSignalConversation,
loadStoryReplies, loadStoryReplies,
markStoryRead, markStoryRead,
numStories, numStories,
@ -454,9 +456,12 @@ export const StoryViewer = ({
const isSent = Boolean(sendState); const isSent = Boolean(sendState);
const contextMenuOptions: ReadonlyArray<ContextMenuOptionType<unknown>> = let contextMenuOptions:
isSent | ReadonlyArray<ContextMenuOptionType<unknown>>
? [ | undefined;
if (isSent) {
contextMenuOptions = [
{ {
icon: 'StoryListItem__icon--info', icon: 'StoryListItem__icon--info',
label: i18n('StoryListItem__info'), label: i18n('StoryListItem__info'),
@ -467,8 +472,9 @@ export const StoryViewer = ({
label: i18n('StoryListItem__delete'), label: i18n('StoryListItem__delete'),
onClick: () => setConfirmDeleteStory(story), onClick: () => setConfirmDeleteStory(story),
}, },
] ];
: [ } else if (!isSignalConversation) {
contextMenuOptions = [
{ {
icon: 'StoryListItem__icon--info', icon: 'StoryListItem__icon--info',
label: i18n('StoryListItem__info'), label: i18n('StoryListItem__info'),
@ -495,6 +501,7 @@ export const StoryViewer = ({
}, },
}, },
]; ];
}
return ( return (
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}> <FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
@ -685,6 +692,7 @@ export const StoryViewer = ({
} }
type="button" type="button"
/> />
{contextMenuOptions && (
<ContextMenu <ContextMenu
aria-label={i18n('MyStories__more')} aria-label={i18n('MyStories__more')}
i18n={i18n} i18n={i18n}
@ -693,6 +701,7 @@ export const StoryViewer = ({
onMenuShowingChanged={setIsShowingContextMenu} onMenuShowingChanged={setIsShowingContextMenu}
theme={Theme.Dark} theme={Theme.Dark}
/> />
)}
</div> </div>
</div> </div>
<div className="StoryViewer__progress"> <div className="StoryViewer__progress">

View file

@ -11,6 +11,7 @@ import { getClassNamesFor } from '../../util/getClassNamesFor';
export type PropsType = { export type PropsType = {
contactNameColor?: ContactNameColorType; contactNameColor?: ContactNameColorType;
firstName?: string; firstName?: string;
isSignalConversation?: boolean;
module?: string; module?: string;
preferFirstName?: boolean; preferFirstName?: boolean;
title: string; title: string;
@ -19,6 +20,7 @@ export type PropsType = {
export const ContactName = ({ export const ContactName = ({
contactNameColor, contactNameColor,
firstName, firstName,
isSignalConversation,
module, module,
preferFirstName, preferFirstName,
title, title,
@ -41,6 +43,9 @@ export const ContactName = ({
dir="auto" dir="auto"
> >
<Emojify text={text} /> <Emojify text={text} />
{isSignalConversation && (
<span className="StoryListItem__signal-official" />
)}
</span> </span>
); );
}; };

View file

@ -27,6 +27,7 @@ import { getMuteOptions } from '../../util/getMuteOptions';
import * as expirationTimer from '../../util/expirationTimer'; import * as expirationTimer from '../../util/expirationTimer';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
import { isInSystemContacts } from '../../util/isInSystemContacts'; import { isInSystemContacts } from '../../util/isInSystemContacts';
import { isConversationMuted } from '../../util/isConversationMuted';
import { import {
useStartCallShortcuts, useStartCallShortcuts,
useKeyboardShortcuts, useKeyboardShortcuts,
@ -47,6 +48,7 @@ export type PropsDataType = {
outgoingCallButtonStyle: OutgoingCallButtonStyle; outgoingCallButtonStyle: OutgoingCallButtonStyle;
showBackButton?: boolean; showBackButton?: boolean;
isSMSOnly?: boolean; isSMSOnly?: boolean;
isSignalConversation?: boolean;
theme: ThemeType; theme: ThemeType;
} & Pick< } & Pick<
ConversationType, ConversationType,
@ -329,6 +331,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
isArchived, isArchived,
isMissingMandatoryProfileSharing, isMissingMandatoryProfileSharing,
isPinned, isPinned,
isSignalConversation,
left, left,
markedUnread, markedUnread,
muteExpiresAt, muteExpiresAt,
@ -347,10 +350,37 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
const muteOptions = getMuteOptions(muteExpiresAt, i18n); const muteOptions = getMuteOptions(muteExpiresAt, i18n);
// eslint-disable-next-line @typescript-eslint/no-explicit-any const muteTitle = <span>{i18n('muteNotificationsTitle')}</span>;
const disappearingTitle = i18n('icu:disappearingMessages') as any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (isSignalConversation) {
const muteTitle = i18n('muteNotificationsTitle') as any; const isMuted = muteExpiresAt && isConversationMuted({ muteExpiresAt });
return (
<ContextMenu id={triggerId}>
<SubMenu hoverDelay={1} title={muteTitle} rtl>
{isMuted ? (
<MenuItem
onClick={() => {
onSetMuteNotifications(0);
}}
>
{i18n('unmute')}
</MenuItem>
) : (
<MenuItem
onClick={() => {
onSetMuteNotifications(Number.MAX_SAFE_INTEGER);
}}
>
{i18n('muteAlways')}
</MenuItem>
)}
</SubMenu>
</ContextMenu>
);
}
const disappearingTitle = <span>{i18n('icu:disappearingMessages')}</span>;
const isGroup = type === 'group'; const isGroup = type === 'group';
const disableTimerChanges = Boolean( const disableTimerChanges = Boolean(
@ -545,6 +575,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
i18n, i18n,
id, id,
isSMSOnly, isSMSOnly,
isSignalConversation,
onOutgoingAudioCallInConversation, onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation, onOutgoingVideoCallInConversation,
onSetDisappearingMessages, onSetDisappearingMessages,
@ -594,7 +625,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
> >
{this.renderBackButton()} {this.renderBackButton()}
{this.renderHeader()} {this.renderHeader()}
{!isSMSOnly && ( {!isSMSOnly && !isSignalConversation && (
<OutgoingCallButtons <OutgoingCallButtons
announcementsOnly={announcementsOnly} announcementsOnly={announcementsOnly}
areWeAdmin={areWeAdmin} areWeAdmin={areWeAdmin}

View file

@ -25,6 +25,7 @@ export type Props = {
id: string; id: string;
i18n: LocalizerType; i18n: LocalizerType;
isMe: boolean; isMe: boolean;
isSignalConversation?: boolean;
membersCount?: number; membersCount?: number;
name?: string; name?: string;
phoneNumber?: string; phoneNumber?: string;
@ -111,6 +112,7 @@ export const ConversationHero = ({
hasStories, hasStories,
id, id,
isMe, isMe,
isSignalConversation,
membersCount, membersCount,
sharedGroupNames = [], sharedGroupNames = [],
name, name,
@ -185,7 +187,14 @@ export const ConversationHero = ({
title={title} title={title}
/> />
<h1 className="module-conversation-hero__profile-name"> <h1 className="module-conversation-hero__profile-name">
{isMe ? i18n('noteToSelf') : <ContactName title={title} />} {isMe ? (
i18n('noteToSelf')
) : (
<ContactName
isSignalConversation={isSignalConversation}
title={title}
/>
)}
</h1> </h1>
{about && !isMe && ( {about && !isMe && (
<div className="module-about__container"> <div className="module-about__container">
@ -207,7 +216,8 @@ export const ConversationHero = ({
)} )}
</div> </div>
) : null} ) : null}
{renderMembershipRow({ {!isSignalConversation &&
renderMembershipRow({
acceptedMessageRequest, acceptedMessageRequest,
conversationType, conversationType,
i18n, i18n,
@ -218,9 +228,11 @@ export const ConversationHero = ({
phoneNumber, phoneNumber,
sharedGroupNames, sharedGroupNames,
})} })}
{!isSignalConversation && (
<div className="module-conversation-hero__linkNotification"> <div className="module-conversation-hero__linkNotification">
{i18n('messageHistoryUnsynced')} {i18n('messageHistoryUnsynced')}
</div> </div>
)}
</div> </div>
{isShowingMessageRequestWarning && ( {isShowingMessageRequestWarning && (
<ConfirmationDialog <ConfirmationDialog

View file

@ -13,6 +13,7 @@ import type { BadgeType } from '../../badges/types';
import type { LocalizerType, ThemeType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import { ContactName } from '../conversation/ContactName'; import { ContactName } from '../conversation/ContactName';
import { About } from '../conversation/About'; import { About } from '../conversation/About';
import { isSignalConversation } from '../../util/isSignalConversation';
export type ContactListItemConversationType = Pick< export type ContactListItemConversationType = Pick<
ConversationType, ConversationType,
@ -31,6 +32,7 @@ export type ContactListItemConversationType = Pick<
| 'unblurredAvatarPath' | 'unblurredAvatarPath'
| 'username' | 'username'
| 'e164' | 'e164'
| 'uuid'
>; >;
type PropsDataType = ContactListItemConversationType & { type PropsDataType = ContactListItemConversationType & {
@ -63,13 +65,18 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
title, title,
type, type,
unblurredAvatarPath, unblurredAvatarPath,
uuid,
}) { }) {
const headerName = isMe ? ( const headerName = isMe ? (
<span className={HEADER_CONTACT_NAME_CLASS_NAME}> <span className={HEADER_CONTACT_NAME_CLASS_NAME}>
{i18n('noteToSelf')} {i18n('noteToSelf')}
</span> </span>
) : ( ) : (
<ContactName module={HEADER_CONTACT_NAME_CLASS_NAME} title={title} /> <ContactName
isSignalConversation={isSignalConversation({ id, uuid })}
module={HEADER_CONTACT_NAME_CLASS_NAME}
title={title}
/>
); );
const messageText = const messageText =

View file

@ -18,6 +18,7 @@ import { TypingAnimation } from '../conversation/TypingAnimation';
import type { LocalizerType, ThemeType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import type { ConversationType } from '../../state/ducks/conversations'; import type { ConversationType } from '../../state/ducks/conversations';
import type { BadgeType } from '../../badges/types'; import type { BadgeType } from '../../badges/types';
import { isSignalConversation } from '../../util/isSignalConversation';
const MESSAGE_STATUS_ICON_CLASS_NAME = `${MESSAGE_TEXT_CLASS_NAME}__status-icon`; const MESSAGE_STATUS_ICON_CLASS_NAME = `${MESSAGE_TEXT_CLASS_NAME}__status-icon`;
@ -58,6 +59,7 @@ export type PropsData = Pick<
| 'typingContactId' | 'typingContactId'
| 'unblurredAvatarPath' | 'unblurredAvatarPath'
| 'unreadCount' | 'unreadCount'
| 'uuid'
> & { > & {
badge?: BadgeType; badge?: BadgeType;
}; };
@ -96,6 +98,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
typingContactId, typingContactId,
unblurredAvatarPath, unblurredAvatarPath,
unreadCount, unreadCount,
uuid,
}) { }) {
const isMuted = Boolean(muteExpiresAt && Date.now() < muteExpiresAt); const isMuted = Boolean(muteExpiresAt && Date.now() < muteExpiresAt);
const headerName = ( const headerName = (
@ -105,7 +108,11 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
{i18n('noteToSelf')} {i18n('noteToSelf')}
</span> </span>
) : ( ) : (
<ContactName module={HEADER_CONTACT_NAME_CLASS_NAME} title={title} /> <ContactName
module={HEADER_CONTACT_NAME_CLASS_NAME}
isSignalConversation={isSignalConversation({ id, uuid })}
title={title}
/>
)} )}
{isMuted && <div className={`${HEADER_NAME_CLASS_NAME}__mute-icon`} />} {isMuted && <div className={`${HEADER_NAME_CLASS_NAME}__mute-icon`} />}
</> </>

View file

@ -130,6 +130,7 @@ import { getConversationIdForLogging } from '../util/idForLogging';
import { getSendTarget } from '../util/getSendTarget'; import { getSendTarget } from '../util/getSendTarget';
import { getRecipients } from '../util/getRecipients'; import { getRecipients } from '../util/getRecipients';
import { validateConversation } from '../util/validateConversation'; import { validateConversation } from '../util/validateConversation';
import { isSignalConversation } from '../util/isSignalConversation';
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -3357,6 +3358,10 @@ export class ConversationModel extends window.Backbone
return; return;
} }
if (isSignalConversation(this.attributes)) {
return;
}
if (hasUserInitiatedMessages) { if (hasUserInitiatedMessages) {
await this.maybeRemoveUniversalTimer(); await this.maybeRemoveUniversalTimer();
return; return;
@ -3920,6 +3925,10 @@ export class ConversationModel extends window.Backbone
return; return;
} }
if (isSignalConversation(this.attributes)) {
return;
}
const now = timestamp || Date.now(); const now = timestamp || Date.now();
log.info( log.info(
@ -4446,6 +4455,10 @@ export class ConversationModel extends window.Backbone
): Promise<boolean | null | MessageModel | void> { ): Promise<boolean | null | MessageModel | void> {
const isSetByOther = providedSource || providedSentAt !== undefined; const isSetByOther = providedSource || providedSentAt !== undefined;
if (isSignalConversation(this.attributes)) {
return;
}
if (isGroupV2(this.attributes)) { if (isGroupV2(this.attributes)) {
if (isSetByOther) { if (isSetByOther) {
throw new Error( throw new Error(
@ -5086,6 +5099,9 @@ export class ConversationModel extends window.Backbone
getAbsoluteAvatarPath(): string | undefined { getAbsoluteAvatarPath(): string | undefined {
const avatarPath = this.getAvatarPath(); const avatarPath = this.getAvatarPath();
if (isSignalConversation(this.attributes)) {
return avatarPath;
}
return avatarPath ? getAbsoluteAttachmentPath(avatarPath) : undefined; return avatarPath ? getAbsoluteAttachmentPath(avatarPath) : undefined;
} }
@ -5199,6 +5215,10 @@ export class ConversationModel extends window.Backbone
// [X] dontNotifyForMentionsIfMuted // [X] dontNotifyForMentionsIfMuted
// [x] firstUnregisteredAt // [x] firstUnregisteredAt
captureChange(logMessage: string): void { captureChange(logMessage: string): void {
if (isSignalConversation(this.attributes)) {
return;
}
log.info('storageService[captureChange]', logMessage, this.idForLogging()); log.info('storageService[captureChange]', logMessage, this.idForLogging());
this.set({ needsStorageServiceSync: true }); this.set({ needsStorageServiceSync: true });

View file

@ -79,6 +79,10 @@ export class ProfileService {
); );
} }
if (window.ConversationController.isSignalConversation(conversationId)) {
return;
}
if (this.isPaused) { if (this.isPaused) {
throw new Error( throw new Error(
`ProfileService.get: Cannot add job to paused queue for conversation ${preCheckConversation.idForLogging()}` `ProfileService.get: Cannot add job to paused queue for conversation ${preCheckConversation.idForLogging()}`

View file

@ -63,6 +63,7 @@ import type {
} from '../sql/Interface'; } from '../sql/Interface';
import { MY_STORIES_ID } from '../types/Stories'; import { MY_STORIES_ID } from '../types/Stories';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import { isSignalConversation } from '../util/isSignalConversation';
type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier; type IManifestRecordIdentifier = Proto.ManifestRecord.IIdentifier;
@ -238,6 +239,10 @@ async function generateManifest(
let identifierType; let identifierType;
let storageRecord; let storageRecord;
if (isSignalConversation(conversation.attributes)) {
continue;
}
const conversationType = typeofConversation(conversation.attributes); const conversationType = typeofConversation(conversation.attributes);
if (conversationType === ConversationTypes.Me) { if (conversationType === ConversationTypes.Me) {
storageRecord = new Proto.StorageRecord(); storageRecord = new Proto.StorageRecord();

View file

@ -52,6 +52,7 @@ import type {
import dataInterface from '../sql/Client'; import dataInterface from '../sql/Client';
import { MY_STORIES_ID, StorySendMode } from '../types/Stories'; import { MY_STORIES_ID, StorySendMode } from '../types/Stories';
import * as RemoteConfig from '../RemoteConfig'; import * as RemoteConfig from '../RemoteConfig';
import { findAndDeleteOnboardingStoryIfExists } from '../util/findAndDeleteOnboardingStoryIfExists';
const MY_STORIES_BYTES = uuidToBytes(MY_STORIES_ID); const MY_STORIES_BYTES = uuidToBytes(MY_STORIES_ID);
@ -375,6 +376,13 @@ export function toAccountRecord(
accountRecord.hasSetMyStoriesPrivacy = hasSetMyStoriesPrivacy; accountRecord.hasSetMyStoriesPrivacy = hasSetMyStoriesPrivacy;
} }
const hasViewedOnboardingStory = window.storage.get(
'hasViewedOnboardingStory'
);
if (hasViewedOnboardingStory !== undefined) {
accountRecord.hasViewedOnboardingStory = hasViewedOnboardingStory;
}
const hasStoriesDisabled = window.storage.get('hasStoriesDisabled'); const hasStoriesDisabled = window.storage.get('hasStoriesDisabled');
accountRecord.storiesDisabled = hasStoriesDisabled === true; accountRecord.storiesDisabled = hasStoriesDisabled === true;
@ -1118,6 +1126,7 @@ export async function mergeAccountRecord(
displayBadgesOnProfile, displayBadgesOnProfile,
keepMutedChatsArchived, keepMutedChatsArchived,
hasSetMyStoriesPrivacy, hasSetMyStoriesPrivacy,
hasViewedOnboardingStory,
storiesDisabled, storiesDisabled,
storyViewReceiptsEnabled, storyViewReceiptsEnabled,
} = accountRecord; } = accountRecord;
@ -1313,6 +1322,16 @@ export async function mergeAccountRecord(
window.storage.put('displayBadgesOnProfile', Boolean(displayBadgesOnProfile)); window.storage.put('displayBadgesOnProfile', Boolean(displayBadgesOnProfile));
window.storage.put('keepMutedChatsArchived', Boolean(keepMutedChatsArchived)); window.storage.put('keepMutedChatsArchived', Boolean(keepMutedChatsArchived));
window.storage.put('hasSetMyStoriesPrivacy', Boolean(hasSetMyStoriesPrivacy)); window.storage.put('hasSetMyStoriesPrivacy', Boolean(hasSetMyStoriesPrivacy));
{
const hasViewedOnboardingStoryBool = Boolean(hasViewedOnboardingStory);
window.storage.put(
'hasViewedOnboardingStory',
hasViewedOnboardingStoryBool
);
if (hasViewedOnboardingStoryBool) {
findAndDeleteOnboardingStoryIfExists();
}
}
{ {
const hasStoriesDisabled = Boolean(storiesDisabled); const hasStoriesDisabled = Boolean(storiesDisabled);
window.storage.put('hasStoriesDisabled', hasStoriesDisabled); window.storage.put('hasStoriesDisabled', hasStoriesDisabled);

View file

@ -16,6 +16,7 @@ import { isNotNil } from '../util/isNotNil';
import { strictAssert } from '../util/assert'; import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull'; import { dropNull } from '../util/dropNull';
import { isGroup } from '../util/whatTypeOfConversation'; import { isGroup } from '../util/whatTypeOfConversation';
import { SIGNAL_ACI } from '../types/Conversation';
let storyData: let storyData:
| Array< | Array<
@ -146,9 +147,10 @@ async function repairUnexpiredStories(): Promise<void> {
const storiesWithExpiry = storyData const storiesWithExpiry = storyData
.filter( .filter(
story => story =>
!story.expirationStartTimestamp || story.sourceUuid !== SIGNAL_ACI &&
(!story.expirationStartTimestamp ||
!story.expireTimer || !story.expireTimer ||
story.expireTimer > DAY_AS_SECONDS story.expireTimer > DAY_AS_SECONDS)
) )
.map(story => ({ .map(story => ({
...story, ...story,

View file

@ -20,6 +20,7 @@ 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';
import { SIGNAL_ACI } from '../../types/Conversation';
import dataInterface from '../../sql/Client'; import dataInterface from '../../sql/Client';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog'; import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
@ -31,6 +32,7 @@ import { deleteStoryForEveryone as doDeleteStoryForEveryone } from '../../util/d
import { deleteGroupStoryReplyForEveryone as doDeleteGroupStoryReplyForEveryone } from '../../util/deleteGroupStoryReplyForEveryone'; import { deleteGroupStoryReplyForEveryone as doDeleteGroupStoryReplyForEveryone } from '../../util/deleteGroupStoryReplyForEveryone';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend'; import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import { markOnboardingStoryAsRead } from '../../util/markOnboardingStoryAsRead';
import { markViewed } from '../../services/MessageUpdater'; import { markViewed } from '../../services/MessageUpdater';
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads'; import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
import { replaceIndex } from '../../util/replaceIndex'; import { replaceIndex } from '../../util/replaceIndex';
@ -345,9 +347,19 @@ function markStoryRead(
return; return;
} }
const isSignalOnboardingStory = message.get('sourceUuid') === SIGNAL_ACI;
if (isSignalOnboardingStory) {
markOnboardingStoryAsRead();
return;
}
const storyReadDate = Date.now(); const storyReadDate = Date.now();
message.set(markViewed(message.attributes, storyReadDate)); message.set(markViewed(message.attributes, storyReadDate));
window.Signal.Data.saveMessage(message.attributes, {
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
});
const viewedReceipt = { const viewedReceipt = {
messageId, messageId,

View file

@ -61,6 +61,7 @@ import type { AccountSelectorType } from './accounts';
import { getAccountSelector } from './accounts'; import { getAccountSelector } from './accounts';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { TimelineMessageLoadingState } from '../../util/timelineUtil'; import { TimelineMessageLoadingState } from '../../util/timelineUtil';
import { isSignalConversation } from '../../util/isSignalConversation';
import { reduce } from '../../util/iterables'; import { reduce } from '../../util/iterables';
let placeholderContact: ConversationType; let placeholderContact: ConversationType;
@ -296,6 +297,10 @@ export const _getLeftPaneLists = (
}; };
} }
if (isSignalConversation(conversation)) {
continue;
}
// We always show pinned conversations // We always show pinned conversations
if (conversation.isPinned) { if (conversation.isPinned) {
pinnedConversations.push(conversation); pinnedConversations.push(conversation);
@ -450,6 +455,7 @@ function hasDisplayInfo(conversation: ConversationType): boolean {
function canComposeConversation(conversation: ConversationType): boolean { function canComposeConversation(conversation: ConversationType): boolean {
return Boolean( return Boolean(
!isSignalConversation(conversation) &&
!conversation.isBlocked && !conversation.isBlocked &&
!isConversationUnregistered(conversation) && !isConversationUnregistered(conversation) &&
hasDisplayInfo(conversation) && hasDisplayInfo(conversation) &&
@ -462,6 +468,7 @@ export const getAllComposableConversations = createSelector(
(conversationLookup: ConversationLookupType): Array<ConversationType> => (conversationLookup: ConversationLookupType): Array<ConversationType> =>
Object.values(conversationLookup).filter( Object.values(conversationLookup).filter(
conversation => conversation =>
!isSignalConversation(conversation) &&
!conversation.isBlocked && !conversation.isBlocked &&
!conversation.isGroupV1AndDisabled && !conversation.isGroupV1AndDisabled &&
!isConversationUnregistered(conversation) && !isConversationUnregistered(conversation) &&

View file

@ -97,6 +97,7 @@ import { DAY, HOUR, SECOND } from '../../util/durations';
import { getStoryReplyText } from '../../util/getStoryReplyText'; import { getStoryReplyText } from '../../util/getStoryReplyText';
import { isIncoming, isOutgoing, isStory } from '../../messages/helpers'; import { isIncoming, isOutgoing, isStory } from '../../messages/helpers';
import { calculateExpirationTimestamp } from '../../util/expirationTimer'; import { calculateExpirationTimestamp } from '../../util/expirationTimer';
import { isSignalConversation } from '../../util/isSignalConversation';
export { isIncoming, isOutgoing, isStory }; export { isIncoming, isOutgoing, isStory };
@ -1648,6 +1649,10 @@ function canReplyOrReact(
return false; return false;
} }
if (isSignalConversation(conversation)) {
return false;
}
if (isOutgoing(message)) { if (isOutgoing(message)) {
return ( return (
isMessageJustForMe(sendStateByConversationId, ourConversationId) || isMessageJustForMe(sendStateByConversationId, ourConversationId) ||

View file

@ -38,6 +38,7 @@ import { getStoriesEnabled } from './items';
import { calculateExpirationTimestamp } from '../../util/expirationTimer'; import { calculateExpirationTimestamp } from '../../util/expirationTimer';
import { getMessageIdForLogging } from '../../util/idForLogging'; import { getMessageIdForLogging } from '../../util/idForLogging';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { SIGNAL_ACI } from '../../types/Conversation';
export const getStoriesState = (state: StateType): StoriesStateType => export const getStoriesState = (state: StateType): StoriesStateType =>
state.stories; state.stories;
@ -67,6 +68,20 @@ function sortByRecencyAndUnread(
storyA: ConversationStoryType, storyA: ConversationStoryType,
storyB: ConversationStoryType storyB: ConversationStoryType
): number { ): number {
if (
storyA.storyView.sender.uuid === SIGNAL_ACI &&
storyA.storyView.isUnread
) {
return -1;
}
if (
storyB.storyView.sender.uuid === SIGNAL_ACI &&
storyB.storyView.isUnread
) {
return 1;
}
if (storyA.storyView.isUnread && storyB.storyView.isUnread) { if (storyA.storyView.isUnread && storyB.storyView.isUnread) {
return storyA.storyView.timestamp > storyB.storyView.timestamp ? -1 : 1; return storyA.storyView.timestamp > storyB.storyView.timestamp ? -1 : 1;
} }
@ -160,6 +175,7 @@ export function getStoryView(
'profileName', 'profileName',
'sharedGroupNames', 'sharedGroupNames',
'title', 'title',
'uuid',
]); ]);
const { const {
@ -330,9 +346,13 @@ export const getStories = createSelector(
return; return;
} }
// if for some reason this story is already experied (bug) // if for some reason this story is already expired (bug)
// log it and skip it // log it and skip it. Unless it's the onboarding story, that story
if ((calculateExpirationTimestamp(story) ?? 0) < Date.now()) { // doesn't have an expiration until it is viewed.
if (
!story.sourceUuid === SIGNAL_ACI &&
(calculateExpirationTimestamp(story) ?? 0) < Date.now()
) {
const messageIdForLogging = getMessageIdForLogging({ const messageIdForLogging = getMessageIdForLogging({
...pick(story, 'type', 'sourceUuid', 'sourceDevice'), ...pick(story, 'type', 'sourceUuid', 'sourceDevice'),
sent_at: story.timestamp, sent_at: story.timestamp,

View file

@ -28,6 +28,7 @@ import {
getRecentlyInstalledStickerPack, getRecentlyInstalledStickerPack,
getRecentStickers, getRecentStickers,
} from '../selectors/stickers'; } from '../selectors/stickers';
import { isSignalConversation } from '../../util/isSignalConversation';
type ExternalProps = { type ExternalProps = {
id: string; id: string;
@ -122,6 +123,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
...conversation, ...conversation,
conversationType: conversation.type, conversationType: conversation.type,
isSMSOnly: Boolean(isConversationSMSOnly(conversation)), isSMSOnly: Boolean(isConversationSMSOnly(conversation)),
isSignalConversation: isSignalConversation(conversation),
isFetchingUUID: conversation.isFetchingUUID, isFetchingUUID: conversation.isFetchingUUID,
isMissingMandatoryProfileSharing: isMissingMandatoryProfileSharing:
isMissingRequiredProfileSharing(conversation), isMissingRequiredProfileSharing(conversation),

View file

@ -24,6 +24,7 @@ import { isConversationSMSOnly } from '../../util/isConversationSMSOnly';
import { mapDispatchToProps } from '../actions'; import { mapDispatchToProps } from '../actions';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { isSignalConversation } from '../../util/isSignalConversation';
export type OwnProps = { export type OwnProps = {
id: string; id: string;
@ -118,6 +119,7 @@ const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
isMissingMandatoryProfileSharing: isMissingMandatoryProfileSharing:
isMissingRequiredProfileSharing(conversation), isMissingRequiredProfileSharing(conversation),
isSMSOnly: isConversationSMSOnly(conversation), isSMSOnly: isConversationSMSOnly(conversation),
isSignalConversation: isSignalConversation(conversation),
i18n: getIntl(state), i18n: getIntl(state),
showBackButton: state.conversations.selectedConversationPanelDepth > 0, showBackButton: state.conversations.selectedConversationPanelDepth > 0,
outgoingCallButtonStyle: getOutgoingCallButtonStyle(conversation, state), outgoingCallButtonStyle: getOutgoingCallButtonStyle(conversation, state),

View file

@ -10,6 +10,7 @@ import type { StateType } from '../reducer';
import { getPreferredBadgeSelector } from '../selectors/badges'; import { getPreferredBadgeSelector } from '../selectors/badges';
import { getIntl, getTheme } from '../selectors/user'; import { getIntl, getTheme } from '../selectors/user';
import { getHasStoriesSelector } from '../selectors/stories'; import { getHasStoriesSelector } from '../selectors/stories';
import { isSignalConversation } from '../../util/isSignalConversation';
type ExternalProps = { type ExternalProps = {
id: string; id: string;
@ -30,6 +31,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
conversationType: conversation.type, conversationType: conversation.type,
hasStories: getHasStoriesSelector(state)(id), hasStories: getHasStoriesSelector(state)(id),
badge: getPreferredBadgeSelector(state)(conversation.badges), badge: getPreferredBadgeSelector(state)(conversation.badges),
isSignalConversation: isSignalConversation(conversation),
theme: getTheme(state), theme: getTheme(state),
}; };
}; };

View file

@ -25,6 +25,7 @@ import {
getStoryByIdSelector, getStoryByIdSelector,
} from '../selectors/stories'; } from '../selectors/stories';
import { isInFullScreenCall } from '../selectors/calling'; import { isInFullScreenCall } from '../selectors/calling';
import { isSignalConversation } from '../../util/isSignalConversation';
import { renderEmojiPicker } from './renderEmojiPicker'; import { renderEmojiPicker } from './renderEmojiPicker';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { useActions as useEmojisActions } from '../ducks/emojis'; import { useActions as useEmojisActions } from '../ducks/emojis';
@ -92,6 +93,9 @@ export function SmartStoryViewer(): JSX.Element | null {
hasAllStoriesMuted={hasAllStoriesMuted} hasAllStoriesMuted={hasAllStoriesMuted}
hasViewReceiptSetting={hasViewReceiptSetting} hasViewReceiptSetting={hasViewReceiptSetting}
i18n={i18n} i18n={i18n}
isSignalConversation={isSignalConversation({
id: conversationStory.conversationId,
})}
numStories={selectedStoryData.numStories} numStories={selectedStoryData.numStories}
onHideStory={toggleHideStories} onHideStory={toggleHideStories}
onGoToConversation={senderId => { onGoToConversation={senderId => {

View file

@ -480,6 +480,8 @@ const URL_CALLS = {
getGroupAvatarUpload: 'v1/groups/avatar/form', getGroupAvatarUpload: 'v1/groups/avatar/form',
getGroupCredentials: 'v1/certificate/auth/group', getGroupCredentials: 'v1/certificate/auth/group',
getIceServers: 'v1/accounts/turn', getIceServers: 'v1/accounts/turn',
getOnboardingStoryManifest:
'dynamic/desktop/stories/onboarding/manifest.json',
getStickerPackUpload: 'v1/sticker/pack/form', getStickerPackUpload: 'v1/sticker/pack/form',
groupLog: 'v1/groups/logs', groupLog: 'v1/groups/logs',
groupJoinedAtVersion: 'v1/groups/joined_at_version', groupJoinedAtVersion: 'v1/groups/joined_at_version',
@ -542,6 +544,7 @@ type InitializeOptionsType = {
url: string; url: string;
storageUrl: string; storageUrl: string;
updatesUrl: string; updatesUrl: string;
resourcesUrl: string;
cdnUrlObject: { cdnUrlObject: {
readonly '0': string; readonly '0': string;
readonly [propName: string]: string; readonly [propName: string]: string;
@ -815,6 +818,10 @@ export type WebAPIType = {
options: GroupCredentialsType options: GroupCredentialsType
) => Promise<void>; ) => Promise<void>;
deleteUsername: (abortSignal?: AbortSignal) => Promise<void>; deleteUsername: (abortSignal?: AbortSignal) => Promise<void>;
downloadOnboardingStories: (
version: string,
imageFiles: Array<string>
) => Promise<Array<Uint8Array>>;
getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<Uint8Array>; getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<Uint8Array>;
getAvatar: (path: string) => Promise<Uint8Array>; getAvatar: (path: string) => Promise<Uint8Array>;
getDevices: () => Promise<GetDevicesResultType>; getDevices: () => Promise<GetDevicesResultType>;
@ -846,6 +853,10 @@ export type WebAPIType = {
options?: { accessKey?: string } options?: { accessKey?: string }
) => Promise<ServerKeysType>; ) => Promise<ServerKeysType>;
getMyKeys: (uuidKind: UUIDKind) => Promise<number>; getMyKeys: (uuidKind: UUIDKind) => Promise<number>;
getOnboardingStoryManifest: () => Promise<{
version: string;
languages: Record<string, Array<string>>;
}>;
getProfile: ( getProfile: (
identifier: string, identifier: string,
options: GetProfileOptionsType options: GetProfileOptionsType
@ -1030,6 +1041,7 @@ export function initialize({
url, url,
storageUrl, storageUrl,
updatesUrl, updatesUrl,
resourcesUrl,
directoryConfig, directoryConfig,
cdnUrlObject, cdnUrlObject,
certificateAuthority, certificateAuthority,
@ -1046,6 +1058,9 @@ export function initialize({
if (!is.string(updatesUrl)) { if (!is.string(updatesUrl)) {
throw new Error('WebAPI.initialize: Invalid updatesUrl'); throw new Error('WebAPI.initialize: Invalid updatesUrl');
} }
if (!is.string(resourcesUrl)) {
throw new Error('WebAPI.initialize: Invalid updatesUrl (general)');
}
if (!is.object(cdnUrlObject)) { if (!is.object(cdnUrlObject)) {
throw new Error('WebAPI.initialize: Invalid cdnUrlObject'); throw new Error('WebAPI.initialize: Invalid cdnUrlObject');
} }
@ -1141,26 +1156,23 @@ export function initialize({
// Thanks, function hoisting! // Thanks, function hoisting!
return { return {
getSocketStatus,
checkSockets,
onOnline,
onOffline,
reconnect,
registerRequestHandler,
unregisterRequestHandler,
onHasStoriesDisabledChange,
authenticate, authenticate,
logout,
cdsLookup, cdsLookup,
checkAccountExistence, checkAccountExistence,
checkSockets,
confirmCode, confirmCode,
confirmUsername,
createGroup, createGroup,
deleteUsername, deleteUsername,
finishRegistration, downloadOnboardingStories,
fetchLinkPreviewImage, fetchLinkPreviewImage,
fetchLinkPreviewMetadata, fetchLinkPreviewMetadata,
finishRegistration,
getAccountForUsername,
getAttachment, getAttachment,
getAvatar, getAvatar,
getBadgeImageFile,
getBoostBadgesFromServer,
getConfig, getConfig,
getDevices, getDevices,
getGroup, getGroup,
@ -1174,44 +1186,49 @@ export function initialize({
getKeysForIdentifier, getKeysForIdentifier,
getKeysForIdentifierUnauth, getKeysForIdentifierUnauth,
getMyKeys, getMyKeys,
getOnboardingStoryManifest,
getProfile, getProfile,
getAccountForUsername,
getProfileUnauth, getProfileUnauth,
getBadgeImageFile,
getBoostBadgesFromServer,
getProvisioningResource, getProvisioningResource,
getSenderCertificate, getSenderCertificate,
getSocketStatus,
getSticker, getSticker,
getStickerPackManifest, getStickerPackManifest,
getStorageCredentials, getStorageCredentials,
getStorageManifest, getStorageManifest,
getStorageRecords, getStorageRecords,
logout,
makeProxiedRequest, makeProxiedRequest,
makeSfuRequest, makeSfuRequest,
modifyGroup, modifyGroup,
modifyStorageRecords, modifyStorageRecords,
onHasStoriesDisabledChange,
onOffline,
onOnline,
postBatchIdentityCheck, postBatchIdentityCheck,
putAttachment, putAttachment,
putProfile, putProfile,
putStickers, putStickers,
reserveUsername, reconnect,
confirmUsername,
registerCapabilities, registerCapabilities,
registerKeys, registerKeys,
registerRequestHandler,
registerSupportForUnauthenticatedDelivery, registerSupportForUnauthenticatedDelivery,
reportMessage, reportMessage,
requestVerificationSMS, requestVerificationSMS,
requestVerificationVoice, requestVerificationVoice,
reserveUsername,
sendChallengeResponse,
sendMessages, sendMessages,
sendMessagesUnauth, sendMessagesUnauth,
sendWithSenderKey, sendWithSenderKey,
setSignedPreKey, setSignedPreKey,
startRegistration, startRegistration,
unregisterRequestHandler,
updateDeviceName, updateDeviceName,
uploadAvatar, uploadAvatar,
uploadGroupAvatar, uploadGroupAvatar,
whoami, whoami,
sendChallengeResponse,
}; };
function _ajax( function _ajax(
@ -1406,6 +1423,20 @@ export function initialize({
})) as StorageServiceCredentials; })) as StorageServiceCredentials;
} }
async function getOnboardingStoryManifest() {
const res = await _ajax({
call: 'getOnboardingStoryManifest',
host: resourcesUrl,
httpType: 'GET',
responseType: 'json',
});
return res as {
version: string;
languages: Record<string, Array<string>>;
};
}
async function getStorageManifest( async function getStorageManifest(
options: StorageServiceCallOptionsType = {} options: StorageServiceCallOptionsType = {}
): Promise<Uint8Array> { ): Promise<Uint8Array> {
@ -1644,6 +1675,28 @@ export function initialize({
}); });
} }
async function downloadOnboardingStories(
manifestVersion: string,
imageFiles: Array<string>
): Promise<Array<Uint8Array>> {
return Promise.all(
imageFiles.map(fileName =>
_outerAjax(
`${resourcesUrl}/static/desktop/stories/onboarding/${manifestVersion}/${fileName}.jpg`,
{
certificateAuthority,
contentType: 'application/octet-stream',
proxyUrl,
responseType: 'bytes',
timeout: 0,
type: 'GET',
version,
}
)
)
);
}
async function getBoostBadgesFromServer( async function getBoostBadgesFromServer(
userLanguages: ReadonlyArray<string> userLanguages: ReadonlyArray<string>
): Promise<unknown> { ): Promise<unknown> {

View file

@ -1,8 +1,9 @@
// Copyright 2018-2021 Signal Messenger, LLC // Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { computeHash } from '../Crypto';
import type { ConversationAttributesType } from '../model-types.d'; import type { ConversationAttributesType } from '../model-types.d';
import { UUID } from './UUID';
import { computeHash } from '../Crypto';
export type BuildAvatarUpdaterOptions = Readonly<{ export type BuildAvatarUpdaterOptions = Readonly<{
deleteAttachmentData: (path: string) => Promise<void>; deleteAttachmentData: (path: string) => Promise<void>;
@ -87,3 +88,6 @@ export async function deleteExternalFiles(
await deleteAttachmentData(profileAvatar.path); await deleteAttachmentData(profileAvatar.path);
} }
} }
export const SIGNAL_ACI = UUID.cast('11111111-1111-4111-8111-111111111111');
export const SIGNAL_AVATAR_PATH = 'images/icon_256.png';

View file

@ -49,6 +49,7 @@ export const rendererConfigSchema = z.object({
storageUrl: configRequiredStringSchema, storageUrl: configRequiredStringSchema,
theme: themeSettingSchema, theme: themeSettingSchema,
updatesUrl: configRequiredStringSchema, updatesUrl: configRequiredStringSchema,
resourcesUrl: configRequiredStringSchema,
userDataPath: configRequiredStringSchema, userDataPath: configRequiredStringSchema,
version: configRequiredStringSchema, version: configRequiredStringSchema,
directoryConfig: directoryConfigSchema, directoryConfig: directoryConfigSchema,

View file

@ -65,8 +65,10 @@ export type StorageAccessType = {
defaultConversationColor: DefaultConversationColorType; defaultConversationColor: DefaultConversationColorType;
customColors: CustomColorsItemType; customColors: CustomColorsItemType;
device_name: string; device_name: string;
existingOnboardingStoryMessageIds: Array<string> | undefined;
hasRegisterSupportForUnauthenticatedDelivery: boolean; hasRegisterSupportForUnauthenticatedDelivery: boolean;
hasSetMyStoriesPrivacy: boolean; hasSetMyStoriesPrivacy: boolean;
hasViewedOnboardingStory: boolean;
hasStoriesDisabled: boolean; hasStoriesDisabled: boolean;
storyViewReceiptsEnabled: boolean; storyViewReceiptsEnabled: boolean;
identityKeyMap: IdentityKeyMap; identityKeyMap: IdentityKeyMap;

View file

@ -91,6 +91,7 @@ export type StoryViewType = {
| 'profileName' | 'profileName'
| 'sharedGroupNames' | 'sharedGroupNames'
| 'title' | 'title'
| 'uuid'
>; >;
sendState?: Array<StorySendStateType>; sendState?: Array<StorySendStateType>;
timestamp: number; timestamp: number;

View file

@ -0,0 +1,136 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AttachmentType } from '../types/Attachment';
import type { MessageAttributesType } from '../model-types.d';
import type { MessageModel } from '../models/messages';
import * as log from '../logging/log';
import { IMAGE_JPEG } from '../types/MIME';
import { ReadStatus } from '../messages/MessageReadStatus';
import { SeenStatus } from '../MessageSeenStatus';
import { UUID } from '../types/UUID';
import { findAndDeleteOnboardingStoryIfExists } from './findAndDeleteOnboardingStoryIfExists';
import { runStorageServiceSyncJob } from '../services/storage';
import { saveNewMessageBatcher } from './messageBatcher';
import { strictAssert } from './assert';
// * Check if we've viewed onboarding story. Short circuit.
// * Run storage service sync (just in case) and check again.
// * If it has been viewed and it's downloaded on this device, delete & return.
// * Check if we've already downloaded the onboarding story.
// * Download onboarding story, create db entry, mark as downloaded.
// * If story has been viewed mark as viewed on AccountRecord.
// * If we viewed it >24 hours ago, delete.
export async function downloadOnboardingStory(): Promise<void> {
const hasViewedOnboardingStory = window.storage.get(
'hasViewedOnboardingStory'
);
if (hasViewedOnboardingStory) {
await findAndDeleteOnboardingStoryIfExists();
return;
}
runStorageServiceSyncJob();
window.Whisper.events.once(
'storageService:syncComplete',
continueDownloadingOnboardingStory
);
}
async function continueDownloadingOnboardingStory(): Promise<void> {
const { server } = window.textsecure;
strictAssert(server, 'server not initialized');
const hasViewedOnboardingStory = window.storage.get(
'hasViewedOnboardingStory'
);
if (hasViewedOnboardingStory) {
await findAndDeleteOnboardingStoryIfExists();
return;
}
const existingOnboardingStoryMessageIds = window.storage.get(
'existingOnboardingStoryMessageIds'
);
if (existingOnboardingStoryMessageIds) {
log.info('downloadOnboardingStory: has existingOnboardingStoryMessageIds');
return;
}
const userLocale = window.i18n.getLocale();
const manifest = await server.getOnboardingStoryManifest();
log.info('downloadOnboardingStory: got manifest version:', manifest.version);
const imageFilenames =
userLocale in manifest.languages
? manifest.languages[userLocale]
: manifest.languages.en;
const imageBuffers = await server.downloadOnboardingStories(
manifest.version,
imageFilenames
);
log.info('downloadOnboardingStory: downloaded stories:', imageBuffers.length);
const attachments: Array<AttachmentType> = await Promise.all(
imageBuffers.map(data => {
const attachment: AttachmentType = {
contentType: IMAGE_JPEG,
data,
size: data.byteLength,
};
return window.Signal.Migrations.processNewAttachment(attachment);
})
);
const signalConversation =
await window.ConversationController.getOrCreateSignalConversation();
const storyMessages: Array<MessageModel> = attachments.map(
(attachment, index) => {
const timestamp = Date.now() + index;
const partialMessage: MessageAttributesType = {
attachments: [attachment],
canReplyToStory: false,
conversationId: signalConversation.id,
id: UUID.generate().toString(),
readStatus: ReadStatus.Unread,
received_at: window.Signal.Util.incrementMessageCounter(),
received_at_ms: timestamp,
seenStatus: SeenStatus.Unseen,
sent_at: timestamp,
serverTimestamp: timestamp,
sourceUuid: signalConversation.get('uuid'),
timestamp,
type: 'story',
};
return new window.Whisper.Message(partialMessage);
}
);
await Promise.all(
storyMessages.map(message => saveNewMessageBatcher.add(message.attributes))
);
// Sync to redux
storyMessages.forEach(message => {
message.trigger('change');
});
window.storage.put(
'existingOnboardingStoryMessageIds',
storyMessages.map(message => message.id)
);
log.info('downloadOnboardingStory: done');
}

View file

@ -0,0 +1,45 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as log from '../logging/log';
import { getMessageById } from '../messages/getMessageById';
import { calculateExpirationTimestamp } from './expirationTimer';
export async function findAndDeleteOnboardingStoryIfExists(): Promise<void> {
const existingOnboardingStoryMessageIds = window.storage.get(
'existingOnboardingStoryMessageIds'
);
if (!existingOnboardingStoryMessageIds) {
return;
}
const hasExpired = await (async () => {
for (const id of existingOnboardingStoryMessageIds) {
// eslint-disable-next-line no-await-in-loop
const message = await getMessageById(id);
if (!message) {
continue;
}
const expires = calculateExpirationTimestamp(message.attributes) ?? 0;
return expires < Date.now();
}
return true;
})();
if (!hasExpired) {
log.info(
'findAndDeleteOnboardingStoryIfExists: current msg has not expired'
);
return;
}
log.info('findAndDeleteOnboardingStoryIfExists: removing onboarding stories');
await window.Signal.Data.removeMessages(existingOnboardingStoryMessageIds);
window.storage.put('existingOnboardingStoryMessageIds', undefined);
}

View file

@ -0,0 +1,17 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { SIGNAL_ACI } from '../types/Conversation';
export function isSignalConversation(conversation: {
id: string;
uuid?: string;
}): boolean {
const { id, uuid } = conversation;
if (uuid) {
return uuid === SIGNAL_ACI;
}
return window.ConversationController.isSignalConversation(id);
}

View file

@ -0,0 +1,48 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { DAY } from './durations';
import { getMessageById } from '../messages/getMessageById';
import { isNotNil } from './isNotNil';
import { markViewed } from '../services/MessageUpdater';
import { storageServiceUploadJob } from '../services/storage';
export async function markOnboardingStoryAsRead(): Promise<void> {
const existingOnboardingStoryMessageIds = window.storage.get(
'existingOnboardingStoryMessageIds'
);
if (!existingOnboardingStoryMessageIds) {
return;
}
const messages = await Promise.all(
existingOnboardingStoryMessageIds.map(getMessageById)
);
const storyReadDate = Date.now();
const messageAttributes = messages
.map(message => {
if (!message) {
return;
}
message.set({
expireTimer: DAY,
});
message.set(markViewed(message.attributes, storyReadDate));
return message.attributes;
})
.filter(isNotNil);
window.Signal.Data.saveMessages(messageAttributes, {
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
});
window.storage.put('hasViewedOnboardingStory', true);
storageServiceUploadJob();
}

View file

@ -26,6 +26,7 @@ window.WebAPI = window.textsecure.WebAPI.initialize({
url: config.serverUrl, url: config.serverUrl,
storageUrl: config.storageUrl, storageUrl: config.storageUrl,
updatesUrl: config.updatesUrl, updatesUrl: config.updatesUrl,
resourcesUrl: config.resourcesUrl,
directoryConfig: config.directoryConfig, directoryConfig: config.directoryConfig,
cdnUrlObject: { cdnUrlObject: {
0: config.cdnUrl0, 0: config.cdnUrl0,