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'),
storageUrl: config.get<string>('storageUrl'),
updatesUrl: config.get<string>('updatesUrl'),
resourcesUrl: config.get<string>('resourcesUrl'),
cdnUrl0: config.get<ConfigType>('cdn').get<string>('0'),
cdnUrl2: config.get<ConfigType>('cdn').get<string>('2'),
certificateAuthority: config.get<string>('certificateAuthority'),

View file

@ -9,6 +9,7 @@
},
"contentProxyUrl": "http://contentproxy.signal.org:443",
"updatesUrl": "https://updates2.signal.org/desktop",
"resourcesUrl": "https://updates2.signal.org",
"updatesPublicKey": "05fd7dd3de7149dc0a127909fee7de0f7620ddd0de061b37a2c303e37de802a401",
"sfuUrl": "https://sfu.voip.signal.org/",
"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 keepMutedChatsArchived = 25;
optional bool hasSetMyStoriesPrivacy = 26;
reserved /* hasViewedOnboardingStory */ 27;
optional bool hasViewedOnboardingStory = 27;
reserved 28; // deprecatedStoriesDisabled
optional bool storiesDisabled = 29;
optional OptionalBool storyViewReceiptsEnabled = 30;

View file

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

View file

@ -160,4 +160,18 @@
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 {
@include font-body-1-bold;
color: $color-gray-05;
display: flex;
align-items: center;
}
&--timestamp,
@ -175,4 +177,12 @@
width: 12px;
@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 { MINUTE, SECOND } from './util/durations';
import { getUuidsForE164s } from './util/getUuidsForE164s';
import { SIGNAL_ACI, SIGNAL_AVATAR_PATH } from './types/Conversation';
type ConvoMatchType =
| {
@ -129,8 +130,8 @@ const {
export function start(): void {
const conversations = new window.Whisper.ConversationCollection();
window.getConversations = () => conversations;
window.ConversationController = new ConversationController(conversations);
window.getConversations = () => conversations;
}
export class ConversationController {
@ -144,6 +145,8 @@ export class ConversationController {
private _combineConversationsQueue = new PQueue({ concurrency: 1 });
private _signalConversationId: undefined | string;
constructor(private _conversations: ConversationModelCollectionType) {
const debouncedUpdateUnreadCount = debounce(
this.updateUnreadCount.bind(this),
@ -406,6 +409,43 @@ export class ConversationController {
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 {
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 { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate';
import { StoryViewModeType, StoryViewTargetType } from './types/Stories';
import { downloadOnboardingStory } from './util/downloadOnboardingStory';
const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000;
@ -1031,6 +1032,7 @@ export async function startApp(): Promise<void> {
(async () => {
menuOptions = await window.SignalContext.getMenuOptions();
})(),
downloadOnboardingStory(),
]);
await window.ConversationController.checkForConflicts();
} catch (error) {

View file

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

View file

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

View file

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

View file

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

View file

@ -69,6 +69,7 @@ export type PropsType = {
hasAllStoriesMuted: boolean;
hasViewReceiptSetting: boolean;
i18n: LocalizerType;
isSignalConversation?: boolean;
loadStoryReplies: (conversationId: string, messageId: string) => unknown;
markStoryRead: (mId: string) => unknown;
numStories: number;
@ -121,6 +122,7 @@ export const StoryViewer = ({
hasAllStoriesMuted,
hasViewReceiptSetting,
i18n,
isSignalConversation,
loadStoryReplies,
markStoryRead,
numStories,
@ -454,47 +456,52 @@ export const StoryViewer = ({
const isSent = Boolean(sendState);
const contextMenuOptions: ReadonlyArray<ContextMenuOptionType<unknown>> =
isSent
? [
{
icon: 'StoryListItem__icon--info',
label: i18n('StoryListItem__info'),
onClick: () => setCurrentViewTarget(StoryViewTargetType.Details),
},
{
icon: 'StoryListItem__icon--delete',
label: i18n('StoryListItem__delete'),
onClick: () => setConfirmDeleteStory(story),
},
]
: [
{
icon: 'StoryListItem__icon--info',
label: i18n('StoryListItem__info'),
onClick: () => setCurrentViewTarget(StoryViewTargetType.Details),
},
{
icon: 'StoryListItem__icon--hide',
label: isHidden
? i18n('StoryListItem__unhide')
: i18n('StoryListItem__hide'),
onClick: () => {
if (isHidden) {
onHideStory(conversationId);
} else {
setHasConfirmHideStory(true);
}
},
},
{
icon: 'StoryListItem__icon--chat',
label: i18n('StoryListItem__go-to-chat'),
onClick: () => {
onGoToConversation(conversationId);
},
},
];
let contextMenuOptions:
| ReadonlyArray<ContextMenuOptionType<unknown>>
| undefined;
if (isSent) {
contextMenuOptions = [
{
icon: 'StoryListItem__icon--info',
label: i18n('StoryListItem__info'),
onClick: () => setCurrentViewTarget(StoryViewTargetType.Details),
},
{
icon: 'StoryListItem__icon--delete',
label: i18n('StoryListItem__delete'),
onClick: () => setConfirmDeleteStory(story),
},
];
} else if (!isSignalConversation) {
contextMenuOptions = [
{
icon: 'StoryListItem__icon--info',
label: i18n('StoryListItem__info'),
onClick: () => setCurrentViewTarget(StoryViewTargetType.Details),
},
{
icon: 'StoryListItem__icon--hide',
label: isHidden
? i18n('StoryListItem__unhide')
: i18n('StoryListItem__hide'),
onClick: () => {
if (isHidden) {
onHideStory(conversationId);
} else {
setHasConfirmHideStory(true);
}
},
},
{
icon: 'StoryListItem__icon--chat',
label: i18n('StoryListItem__go-to-chat'),
onClick: () => {
onGoToConversation(conversationId);
},
},
];
}
return (
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
@ -685,14 +692,16 @@ export const StoryViewer = ({
}
type="button"
/>
<ContextMenu
aria-label={i18n('MyStories__more')}
i18n={i18n}
menuOptions={contextMenuOptions}
moduleClassName="StoryViewer__more"
onMenuShowingChanged={setIsShowingContextMenu}
theme={Theme.Dark}
/>
{contextMenuOptions && (
<ContextMenu
aria-label={i18n('MyStories__more')}
i18n={i18n}
menuOptions={contextMenuOptions}
moduleClassName="StoryViewer__more"
onMenuShowingChanged={setIsShowingContextMenu}
theme={Theme.Dark}
/>
)}
</div>
</div>
<div className="StoryViewer__progress">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,9 @@
// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { computeHash } from '../Crypto';
import type { ConversationAttributesType } from '../model-types.d';
import { UUID } from './UUID';
import { computeHash } from '../Crypto';
export type BuildAvatarUpdaterOptions = Readonly<{
deleteAttachmentData: (path: string) => Promise<void>;
@ -87,3 +88,6 @@ export async function deleteExternalFiles(
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,
theme: themeSettingSchema,
updatesUrl: configRequiredStringSchema,
resourcesUrl: configRequiredStringSchema,
userDataPath: configRequiredStringSchema,
version: configRequiredStringSchema,
directoryConfig: directoryConfigSchema,

View file

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

View file

@ -91,6 +91,7 @@ export type StoryViewType = {
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'uuid'
>;
sendState?: Array<StorySendStateType>;
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,
storageUrl: config.storageUrl,
updatesUrl: config.updatesUrl,
resourcesUrl: config.resourcesUrl,
directoryConfig: config.directoryConfig,
cdnUrlObject: {
0: config.cdnUrl0,