Onboarding story
This commit is contained in:
parent
94f318ea08
commit
19a42ed719
42 changed files with 725 additions and 143 deletions
|
@ -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'),
|
||||
|
|
|
@ -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,
|
||||
|
|
4
images/icons/v2/official-20.svg
Normal file
4
images/icons/v2/official-20.svg
Normal 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 |
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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`} />}
|
||||
</>
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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()}`
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) &&
|
||||
|
|
|
@ -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) ||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -49,6 +49,7 @@ export const rendererConfigSchema = z.object({
|
|||
storageUrl: configRequiredStringSchema,
|
||||
theme: themeSettingSchema,
|
||||
updatesUrl: configRequiredStringSchema,
|
||||
resourcesUrl: configRequiredStringSchema,
|
||||
userDataPath: configRequiredStringSchema,
|
||||
version: configRequiredStringSchema,
|
||||
directoryConfig: directoryConfigSchema,
|
||||
|
|
2
ts/types/Storage.d.ts
vendored
2
ts/types/Storage.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -91,6 +91,7 @@ export type StoryViewType = {
|
|||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
| 'uuid'
|
||||
>;
|
||||
sendState?: Array<StorySendStateType>;
|
||||
timestamp: number;
|
||||
|
|
136
ts/util/downloadOnboardingStory.ts
Normal file
136
ts/util/downloadOnboardingStory.ts
Normal 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');
|
||||
}
|
45
ts/util/findAndDeleteOnboardingStoryIfExists.ts
Normal file
45
ts/util/findAndDeleteOnboardingStoryIfExists.ts
Normal 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);
|
||||
}
|
17
ts/util/isSignalConversation.ts
Normal file
17
ts/util/isSignalConversation.ts
Normal 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);
|
||||
}
|
48
ts/util/markOnboardingStoryAsRead.ts
Normal file
48
ts/util/markOnboardingStoryAsRead.ts
Normal 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();
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue