diff --git a/ts/components/session/calling/DraggableCallContainer.tsx b/ts/components/session/calling/DraggableCallContainer.tsx
index 04b0e9194..194e662eb 100644
--- a/ts/components/session/calling/DraggableCallContainer.tsx
+++ b/ts/components/session/calling/DraggableCallContainer.tsx
@@ -135,7 +135,7 @@ export const DraggableCallContainer = () => {
autoPlay={true}
isVideoMuted={remoteStreamVideoIsMuted}
/>
- {remoteStreamVideoIsMuted && (
+ {remoteStreamVideoIsMuted && ongoingCallPubkey && (
diff --git a/ts/components/session/calling/InConversationCallContainer.tsx b/ts/components/session/calling/InConversationCallContainer.tsx
index 87fa01175..e0b5f1d67 100644
--- a/ts/components/session/calling/InConversationCallContainer.tsx
+++ b/ts/components/session/calling/InConversationCallContainer.tsx
@@ -158,7 +158,7 @@ export const InConversationCallContainer = () => {
videoRefRemote.current.muted = true;
}
- if (!ongoingCallWithFocused) {
+ if (!ongoingCallWithFocused || !ongoingCallPubkey) {
return null;
}
diff --git a/ts/components/session/calling/IncomingCallDialog.tsx b/ts/components/session/calling/IncomingCallDialog.tsx
index 3e6dd5500..d812044a1 100644
--- a/ts/components/session/calling/IncomingCallDialog.tsx
+++ b/ts/components/session/calling/IncomingCallDialog.tsx
@@ -6,6 +6,7 @@ import _ from 'underscore';
import { useConversationUsername } from '../../../hooks/useParamSelector';
import { ed25519Str } from '../../../session/onions/onionPath';
import { CallManager } from '../../../session/utils';
+import { callTimeoutMs } from '../../../session/utils/calling/CallManager';
import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/call';
import { Avatar, AvatarSize } from '../../Avatar';
import { SessionButton, SessionButtonColor } from '../SessionButton';
@@ -24,12 +25,10 @@ export const CallWindow = styled.div`
border: var(--session-border);
`;
-const IncomingCallAvatatContainer = styled.div`
+const IncomingCallAvatarContainer = styled.div`
padding: 0 0 2rem 0;
`;
-const timeoutMs = 60000;
-
export const IncomingCallDialog = () => {
const hasIncomingCall = useSelector(getHasIncomingCall);
const incomingCallFromPubkey = useSelector(getHasIncomingCallFrom);
@@ -42,11 +41,11 @@ export const IncomingCallDialog = () => {
window.log.info(
`call missed with ${ed25519Str(
incomingCallFromPubkey
- )} as dialog was not interacted with for ${timeoutMs} ms`
+ )} as the dialog was not interacted with for ${callTimeoutMs} ms`
);
await CallManager.USER_rejectIncomingCallRequest(incomingCallFromPubkey);
}
- }, timeoutMs);
+ }, callTimeoutMs);
}
return () => {
@@ -70,16 +69,16 @@ export const IncomingCallDialog = () => {
}
};
const from = useConversationUsername(incomingCallFromPubkey);
- if (!hasIncomingCall) {
+ if (!hasIncomingCall || !incomingCallFromPubkey) {
return null;
}
if (hasIncomingCall) {
return (
-
+
-
+
, dateBreak, unreadIndicator];
}
- if (messageProps.message?.messageType === 'missed-call-notification') {
- const msgProps = messageProps.message.props as PropsForMissedCallNotification;
+ if (messageProps.message?.messageType === 'call-notification') {
+ const msgProps = messageProps.message.props as PropsForCallNotification;
- return [
- ,
- dateBreak,
- unreadIndicator,
- ];
+ return [, dateBreak, unreadIndicator];
}
if (!messageProps) {
diff --git a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx
index 591a55860..786e08349 100644
--- a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx
+++ b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx
@@ -75,7 +75,7 @@ export const SessionQuotedMessageComposition = () => {
margin={'var(--margins-xs)'}
>
{window.i18n('replyingToMessage')}
-
+
diff --git a/ts/components/session/icon/Icons.tsx b/ts/components/session/icon/Icons.tsx
index 2079d9427..b83b15bd0 100644
--- a/ts/components/session/icon/Icons.tsx
+++ b/ts/components/session/icon/Icons.tsx
@@ -3,6 +3,9 @@ export type SessionIconType =
| 'arrow'
| 'bell'
| 'brand'
+ | 'callIncoming'
+ | 'callMissed'
+ | 'callOutgoing'
| 'caret'
| 'chatBubble'
| 'check'
@@ -95,6 +98,24 @@ export const icons = {
viewBox: '0 0 404.085 448.407',
ratio: 1,
},
+ callIncoming: {
+ path:
+ 'M14.414 7l3.293-3.293a1 1 0 00-1.414-1.414L13 5.586V4a1 1 0 10-2 0v4.003a.996.996 0 00.617.921A.997.997 0 0012 9h4a1 1 0 100-2h-1.586zM2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z',
+ viewBox: '0 0 20 20',
+ ratio: 1,
+ },
+ callOutgoing: {
+ path:
+ 'M17.924 2.617a.997.997 0 00-.215-.322l-.004-.004A.997.997 0 0017 2h-4a1 1 0 100 2h1.586l-3.293 3.293a1 1 0 001.414 1.414L16 5.414V7a1 1 0 102 0V3a.997.997 0 00-.076-.383zM2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z',
+ viewBox: '0 0 20 20',
+ ratio: 1,
+ },
+ callMissed: {
+ path:
+ 'M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3zM16.707 3.293a1 1 0 010 1.414L15.414 6l1.293 1.293a1 1 0 01-1.414 1.414L14 7.414l-1.293 1.293a1 1 0 11-1.414-1.414L12.586 6l-1.293-1.293a1 1 0 011.414-1.414L14 4.586l1.293-1.293a1 1 0 011.414 0z',
+ viewBox: '0 0 20 20',
+ ratio: 1,
+ },
caret: {
path: 'M127.5 191.25L255 63.75L0 63.75L127.5 191.25Z',
viewBox: '-200 -200 640 640',
diff --git a/ts/components/session/registration/RegistrationUserDetails.tsx b/ts/components/session/registration/RegistrationUserDetails.tsx
index abf5905ba..f54aebf75 100644
--- a/ts/components/session/registration/RegistrationUserDetails.tsx
+++ b/ts/components/session/registration/RegistrationUserDetails.tsx
@@ -20,6 +20,7 @@ const DisplayNameInput = (props: {
maxLength={MAX_USERNAME_LENGTH}
onValueChanged={props.onDisplayNameChanged}
onEnterPressed={props.handlePressEnter}
+ inputDataTestId="display-name-input"
/>
);
};
@@ -41,6 +42,7 @@ const RecoveryPhraseInput = (props: {
enableShowHide={true}
onValueChanged={props.onSeedChanged}
onEnterPressed={props.handlePressEnter}
+ inputDataTestId="recovery-phrase-input"
/>
);
};
diff --git a/ts/components/session/registration/SignInTab.tsx b/ts/components/session/registration/SignInTab.tsx
index ed77d61ed..d23918749 100644
--- a/ts/components/session/registration/SignInTab.tsx
+++ b/ts/components/session/registration/SignInTab.tsx
@@ -39,6 +39,7 @@ const RestoreUsingRecoveryPhraseButton = (props: { onRecoveryButtonClicked: () =
buttonType={SessionButtonType.BrandOutline}
buttonColor={SessionButtonColor.Green}
text={window.i18n('restoreUsingRecoveryPhrase')}
+ dataTestId="restore-using-recovery"
/>
);
};
diff --git a/ts/components/session/settings/section/CategoryPrivacy.tsx b/ts/components/session/settings/section/CategoryPrivacy.tsx
index e876bbc2b..7b8d87a34 100644
--- a/ts/components/session/settings/section/CategoryPrivacy.tsx
+++ b/ts/components/session/settings/section/CategoryPrivacy.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
// tslint:disable-next-line: no-submodule-imports
import useUpdate from 'react-use/lib/useUpdate';
+import { CallManager } from '../../../../session/utils';
import { sessionPassword, updateConfirmModal } from '../../../../state/ducks/modalDialog';
import { toggleMessageRequests } from '../../../../state/ducks/userConfig';
import { getIsMessageRequestsEnabled } from '../../../../state/selectors/userConfig';
@@ -23,6 +24,7 @@ const toggleCallMediaPermissions = async (triggerUIUpdate: () => void) => {
onClickOk: async () => {
await window.toggleCallMediaPermissionsTo(true);
triggerUIUpdate();
+ CallManager.onTurnedOnCallMediaPermissions();
},
onClickCancel: async () => {
await window.toggleCallMediaPermissionsTo(false);
diff --git a/ts/hooks/useDisableDrag.ts b/ts/hooks/useDisableDrag.ts
new file mode 100644
index 000000000..b0844f4b3
--- /dev/null
+++ b/ts/hooks/useDisableDrag.ts
@@ -0,0 +1,13 @@
+import { useCallback } from 'react';
+
+/**
+ * This memoized function just returns a callback which can be used to disable the onDragStart event
+ */
+export const useDisableDrag = () => {
+ const cb = useCallback((e: any) => {
+ e.preventDefault();
+ return false;
+ }, []);
+
+ return cb;
+};
diff --git a/ts/hooks/useEncryptedFileFetch.ts b/ts/hooks/useEncryptedFileFetch.ts
index b71f51168..07d6c4bca 100644
--- a/ts/hooks/useEncryptedFileFetch.ts
+++ b/ts/hooks/useEncryptedFileFetch.ts
@@ -6,7 +6,7 @@ import {
} from '../session/crypto/DecryptedAttachmentsManager';
import { perfEnd, perfStart } from '../session/utils/Performance';
-export const useEncryptedFileFetch = (url: string, contentType: string) => {
+export const useEncryptedFileFetch = (url: string, contentType: string, isAvatar: boolean) => {
// tslint:disable-next-line: no-bitwise
const [urlToLoad, setUrlToLoad] = useState('');
const [loading, setLoading] = useState(false);
@@ -16,7 +16,7 @@ export const useEncryptedFileFetch = (url: string, contentType: string) => {
async function fetchUrl() {
perfStart(`getDecryptedMediaUrl-${url}`);
- const decryptedUrl = await getDecryptedMediaUrl(url, contentType);
+ const decryptedUrl = await getDecryptedMediaUrl(url, contentType, isAvatar);
perfEnd(`getDecryptedMediaUrl-${url}`, `getDecryptedMediaUrl-${url}`);
if (mountedRef.current) {
diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts
index e9ab569d4..c60f8b353 100644
--- a/ts/hooks/useParamSelector.ts
+++ b/ts/hooks/useParamSelector.ts
@@ -39,3 +39,16 @@ export function useOurConversationUsername() {
export function useIsMe(pubkey?: string) {
return pubkey && pubkey === UserUtils.getOurPubKeyStrFromCache();
}
+
+export function useIsClosedGroup(convoId?: string) {
+ return useSelector((state: StateType) => {
+ if (!convoId) {
+ return false;
+ }
+ const convo = state.conversations.conversationLookup[convoId];
+ if (!convo) {
+ return false;
+ }
+ return (convo.isGroup && !convo.isPublic) || false;
+ });
+}
diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts
index fad5af236..044aba6a3 100644
--- a/ts/interactions/conversationInteractions.ts
+++ b/ts/interactions/conversationInteractions.ts
@@ -331,7 +331,7 @@ export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) {
return;
}
- const decryptedAvatarUrl = await getDecryptedMediaUrl(currentAttachmentPath, IMAGE_JPEG);
+ const decryptedAvatarUrl = await getDecryptedMediaUrl(currentAttachmentPath, IMAGE_JPEG, true);
if (!decryptedAvatarUrl) {
window.log.warn('Could not decrypt avatar stored locally..');
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts
index 7a95a9979..20307bf5b 100644
--- a/ts/models/conversation.ts
+++ b/ts/models/conversation.ts
@@ -50,6 +50,7 @@ import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsMana
import { IMAGE_JPEG } from '../types/MIME';
import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils';
import { getLatestTimestampOffset } from '../session/snode_api/SNodeAPI';
+import { createLastMessageUpdate } from '../types/Conversation';
export enum ConversationTypeEnum {
GROUP = 'group',
@@ -89,7 +90,7 @@ export interface ConversationAttributes {
sessionRestoreSeen?: boolean;
is_medium_group?: boolean;
type: string;
- avatarPointer?: any;
+ avatarPointer?: string;
avatar?: any;
/* Avatar hash is currently used for opengroupv2. it's sha256 hash of the base64 avatar data. */
avatarHash?: string;
@@ -131,7 +132,7 @@ export interface ConversationAttributesOptionals {
sessionRestoreSeen?: boolean;
is_medium_group?: boolean;
type: string;
- avatarPointer?: any;
+ avatarPointer?: string;
avatar?: any;
avatarHash?: string;
server?: any;
@@ -900,9 +901,9 @@ export class ConversationModel extends Backbone.Model {
const lastMessageJSON = lastMessageModel ? lastMessageModel.toJSON() : null;
const lastMessageStatusModel = lastMessageModel
? lastMessageModel.getMessagePropStatus()
- : null;
- const lastMessageUpdate = window.Signal.Types.Conversation.createLastMessageUpdate({
- currentTimestamp: this.get('active_at') || null,
+ : undefined;
+ const lastMessageUpdate = createLastMessageUpdate({
+ currentTimestamp: this.get('active_at'),
lastMessage: lastMessageJSON,
lastMessageStatus: lastMessageStatusModel,
lastMessageNotificationText: lastMessageModel ? lastMessageModel.getNotificationText() : null,
@@ -1057,6 +1058,8 @@ export class ConversationModel extends Backbone.Model {
);
const unreadCount = await this.getUnreadCount();
this.set({ unreadCount });
+ this.updateLastMessage();
+
await this.commit();
return model;
}
@@ -1487,7 +1490,7 @@ export class ConversationModel extends Backbone.Model {
const avatarUrl = this.getAvatarPath();
const noIconUrl = 'images/session/session_icon_32.png';
if (avatarUrl) {
- const decryptedAvatarUrl = await getDecryptedMediaUrl(avatarUrl, IMAGE_JPEG);
+ const decryptedAvatarUrl = await getDecryptedMediaUrl(avatarUrl, IMAGE_JPEG, true);
if (!decryptedAvatarUrl) {
window.log.warn('Could not decrypt avatar stored locally for getNotificationIcon..');
diff --git a/ts/models/message.ts b/ts/models/message.ts
index 43b5a7f07..b4f0c7a9e 100644
--- a/ts/models/message.ts
+++ b/ts/models/message.ts
@@ -88,7 +88,7 @@ export class MessageModel extends Backbone.Model {
const propsForGroupInvitation = this.getPropsForGroupInvitation();
const propsForGroupNotification = this.getPropsForGroupNotification();
const propsForTimerNotification = this.getPropsForTimerNotification();
- const isMissedCall = this.get('isMissedCall');
+ const callNotificationType = this.get('callNotificationType');
const messageProps: MessageModelPropsWithoutConvoProps = {
propsForMessage: this.getPropsForMessage(),
};
@@ -105,9 +105,9 @@ export class MessageModel extends Backbone.Model {
messageProps.propsForTimerNotification = propsForTimerNotification;
}
- if (isMissedCall) {
- messageProps.propsForMissedCall = {
- isMissedCall,
+ if (callNotificationType) {
+ messageProps.propsForCallNotification = {
+ notificationType: callNotificationType,
messageId: this.id,
receivedAt: this.get('received_at') || Date.now(),
isUnread: this.isUnread(),
@@ -239,6 +239,21 @@ export class MessageModel extends Backbone.Model {
getConversationController().getContactProfileNameOrShortenedPubKey(dataExtraction.source)
);
}
+ if (this.get('callNotificationType')) {
+ const displayName = getConversationController().getContactProfileNameOrShortenedPubKey(
+ this.get('conversationId')
+ );
+ const callNotificationType = this.get('callNotificationType');
+ if (callNotificationType === 'missed-call') {
+ return window.i18n('callMissed', displayName);
+ }
+ if (callNotificationType === 'started-call') {
+ return window.i18n('startedACall', displayName);
+ }
+ if (callNotificationType === 'answered-a-call') {
+ return window.i18n('answeredACall', displayName);
+ }
+ }
return this.get('body');
}
@@ -498,7 +513,7 @@ export class MessageModel extends Backbone.Model {
return undefined;
}
- if (this.isDataExtractionNotification()) {
+ if (this.isDataExtractionNotification() || this.get('callNotificationType')) {
return undefined;
}
diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts
index f880be5be..643b511d2 100644
--- a/ts/models/messageType.ts
+++ b/ts/models/messageType.ts
@@ -1,6 +1,6 @@
import _ from 'underscore';
import { v4 as uuidv4 } from 'uuid';
-import { PropsForMessageWithConvoProps } from '../state/ducks/conversations';
+import { CallNotificationType, PropsForMessageWithConvoProps } from '../state/ducks/conversations';
import { AttachmentTypeWithPath } from '../types/Attachment';
export type MessageModelType = 'incoming' | 'outgoing';
@@ -109,7 +109,7 @@ export interface MessageAttributes {
*/
isDeleted?: boolean;
- isMissedCall?: boolean;
+ callNotificationType?: CallNotificationType;
}
export interface DataExtractionNotificationMsg {
@@ -179,7 +179,7 @@ export interface MessageAttributesOptionals {
direction?: any;
messageHash?: string;
isDeleted?: boolean;
- isMissedCall?: boolean;
+ callNotificationType?: CallNotificationType;
}
/**
diff --git a/ts/receiver/callMessage.ts b/ts/receiver/callMessage.ts
index cbf0d4891..a8d099312 100644
--- a/ts/receiver/callMessage.ts
+++ b/ts/receiver/callMessage.ts
@@ -74,19 +74,19 @@ export async function handleCallMessage(
if (type === SignalService.CallMessage.Type.ANSWER) {
await removeFromCache(envelope);
- await CallManager.handleCallTypeAnswer(sender, callMessage);
+ await CallManager.handleCallTypeAnswer(sender, callMessage, sentTimestamp);
return;
}
if (type === SignalService.CallMessage.Type.ICE_CANDIDATES) {
await removeFromCache(envelope);
- await CallManager.handleCallTypeIceCandidates(sender, callMessage);
+ await CallManager.handleCallTypeIceCandidates(sender, callMessage, sentTimestamp);
return;
}
await removeFromCache(envelope);
// if this another type of call message, just add it to the manager
- await CallManager.handleOtherCallTypes(sender, callMessage);
+ await CallManager.handleOtherCallTypes(sender, callMessage, sentTimestamp);
}
diff --git a/ts/session/crypto/DecryptedAttachmentsManager.ts b/ts/session/crypto/DecryptedAttachmentsManager.ts
index 54b22b390..41f3c8433 100644
--- a/ts/session/crypto/DecryptedAttachmentsManager.ts
+++ b/ts/session/crypto/DecryptedAttachmentsManager.ts
@@ -11,20 +11,29 @@ import * as fse from 'fs-extra';
import { decryptAttachmentBuffer } from '../../types/Attachment';
import { DURATION } from '../constants';
-// FIXME.
-// add a way to remove the blob when the attachment file path is removed (message removed?)
-// do not hardcode the password
-const urlToDecryptedBlobMap = new Map();
+const urlToDecryptedBlobMap = new Map<
+ string,
+ { decrypted: string; lastAccessTimestamp: number; forceRetain: boolean }
+>();
const urlToDecryptingPromise = new Map>();
export const cleanUpOldDecryptedMedias = () => {
const currentTimestamp = Date.now();
let countCleaned = 0;
let countKept = 0;
+ let keptAsAvatars = 0;
+
window?.log?.info('Starting cleaning of medias blobs...');
for (const iterator of urlToDecryptedBlobMap) {
- // if the last access is older than one hour, revoke the url and remove it.
- if (iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.HOURS * 1) {
+ if (
+ iterator[1].forceRetain &&
+ iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.DAYS * 7
+ ) {
+ // keep forceRetained items for at most 7 days
+ keptAsAvatars++;
+ } else if (iterator[1].lastAccessTimestamp < currentTimestamp - DURATION.HOURS * 1) {
+ // if the last access is older than one hour, revoke the url and remove it.
+
URL.revokeObjectURL(iterator[1].decrypted);
urlToDecryptedBlobMap.delete(iterator[0]);
countCleaned++;
@@ -32,10 +41,16 @@ export const cleanUpOldDecryptedMedias = () => {
countKept++;
}
}
- window?.log?.info(`Clean medias blobs: cleaned/kept: ${countCleaned}:${countKept}`);
+ window?.log?.info(
+ `Clean medias blobs: cleaned/kept/keptAsAvatars: ${countCleaned}:${countKept}:${keptAsAvatars}`
+ );
};
-export const getDecryptedMediaUrl = async (url: string, contentType: string): Promise => {
+export const getDecryptedMediaUrl = async (
+ url: string,
+ contentType: string,
+ isAvatar: boolean
+): Promise => {
if (!url) {
return url;
}
@@ -50,11 +65,13 @@ export const getDecryptedMediaUrl = async (url: string, contentType: string): Pr
// if it's not, the hook caller has to fallback to setting the img src as an url to the file instead and load it
if (urlToDecryptedBlobMap.has(url)) {
// refresh the last access timestamp so we keep the one being currently in use
- const existingObjUrl = urlToDecryptedBlobMap.get(url)?.decrypted as string;
+ const existing = urlToDecryptedBlobMap.get(url);
+ const existingObjUrl = existing?.decrypted as string;
urlToDecryptedBlobMap.set(url, {
decrypted: existingObjUrl,
lastAccessTimestamp: Date.now(),
+ forceRetain: existing?.forceRetain || false,
});
// typescript does not realize that the has above makes sure the get is not undefined
@@ -80,6 +97,7 @@ export const getDecryptedMediaUrl = async (url: string, contentType: string): Pr
urlToDecryptedBlobMap.set(url, {
decrypted: obj,
lastAccessTimestamp: Date.now(),
+ forceRetain: isAvatar,
});
}
urlToDecryptingPromise.delete(url);
diff --git a/ts/session/utils/RingingManager.ts b/ts/session/utils/RingingManager.ts
index f97dd1db3..5357afb3d 100644
--- a/ts/session/utils/RingingManager.ts
+++ b/ts/session/utils/RingingManager.ts
@@ -20,6 +20,10 @@ function startRinging() {
void ringingAudio.play();
}
+export function getIsRinging() {
+ return currentlyRinging;
+}
+
export function setIsRinging(isRinging: boolean) {
if (!currentlyRinging && isRinging) {
startRinging();
diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts
index 6132a98df..02bfcd26c 100644
--- a/ts/session/utils/User.ts
+++ b/ts/session/utils/User.ts
@@ -5,6 +5,7 @@ import { KeyPair } from '../../../libtextsecure/libsignal-protocol';
import { PubKey } from '../types';
import { fromHexToArray, toHex } from './String';
import { getConversationController } from '../conversations';
+import { LokiProfile } from '../../types/Message';
export type HexKeyPair = {
pubKey: string;
@@ -93,13 +94,7 @@ export function setSignWithRecoveryPhrase(isLinking: boolean) {
window.textsecure.storage.user.setSignWithRecoveryPhrase(isLinking);
}
-export interface OurLokiProfile {
- displayName: string;
- avatarPointer: string;
- profileKey: Uint8Array | null;
-}
-
-export function getOurProfile(): OurLokiProfile | undefined {
+export function getOurProfile(): LokiProfile | undefined {
try {
// Secondary devices have their profile stored
// in their primary device's conversation
diff --git a/ts/session/utils/calling/CallManager.ts b/ts/session/utils/calling/CallManager.ts
index b84c0e776..22517e615 100644
--- a/ts/session/utils/calling/CallManager.ts
+++ b/ts/session/utils/calling/CallManager.ts
@@ -1,7 +1,6 @@
import _ from 'lodash';
import { MessageUtils, ToastUtils, UserUtils } from '../';
import { getCallMediaPermissionsSettings } from '../../../components/session/settings/SessionSettings';
-import { getConversationById } from '../../../data/data';
import { MessageModelType } from '../../../models/messageType';
import { SignalService } from '../../../protobuf';
import { openConversationWithMessages } from '../../../state/ducks/conversations';
@@ -21,15 +20,18 @@ import { PubKey } from '../../types';
import { v4 as uuidv4 } from 'uuid';
import { PnServer } from '../../../pushnotification';
-import { setIsRinging } from '../RingingManager';
+import { getIsRinging, setIsRinging } from '../RingingManager';
import { getBlackSilenceMediaStream } from './Silence';
import { getMessageQueue } from '../..';
import { MessageSender } from '../../sending';
+import { DURATION } from '../../constants';
// tslint:disable: function-name
export type InputItem = { deviceId: string; label: string };
+export const callTimeoutMs = 30000;
+
/**
* This uuid is set only once we accepted a call or started one.
*/
@@ -88,10 +90,19 @@ export function removeVideoEventsListener(uniqueId: string) {
callVideoListeners();
}
+type CachedCallMessageType = {
+ type: SignalService.CallMessage.Type;
+ sdps: Array;
+ sdpMLineIndexes: Array;
+ sdpMids: Array;
+ uuid: string;
+ timestamp: number;
+};
+
/**
* This field stores all the details received about a specific call with the same uuid. It is a per pubkey and per call cache.
*/
-const callCache = new Map>>();
+const callCache = new Map>>();
let peerConnection: RTCPeerConnection | null;
let dataChannel: RTCDataChannel | null;
@@ -293,6 +304,8 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
if (sender?.track) {
sender.track.enabled = false;
}
+ const silence = getBlackSilenceMediaStream().getAudioTracks()[0];
+ sender?.replaceTrack(silence);
// do the same changes locally
localStream?.getAudioTracks().forEach(t => {
t.stop();
@@ -322,10 +335,14 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
return s.track?.kind === audioTrack.kind;
});
window.log.info('replacing audio track');
-
+ // we actually do not need to toggle the track here, as toggling it here unmuted here locally (so we start to hear ourselves)
+ // do the same changes locally
+ localStream?.getAudioTracks().forEach(t => {
+ t.stop();
+ localStream?.removeTrack(t);
+ });
if (audioSender) {
await audioSender.replaceTrack(audioTrack);
- // we actually do not need to toggle the track here, as toggling it here unmuted here locally (so we start to hear ourselves)
} else {
throw new Error('Failed to get sender for selectAudioInputByDeviceId ');
}
@@ -439,22 +456,38 @@ export async function USER_callRecipient(recipient: string) {
return;
}
await updateConnectedDevices();
+ const now = Date.now();
window?.log?.info(`starting call with ${ed25519Str(recipient)}..`);
- window.inboxStore?.dispatch(startingCallWith({ pubkey: recipient }));
+ window.inboxStore?.dispatch(
+ startingCallWith({
+ pubkey: recipient,
+ })
+ );
if (peerConnection) {
throw new Error('USER_callRecipient peerConnection is already initialized ');
}
currentCallUUID = uuidv4();
+ const justCreatedCallUUID = currentCallUUID;
peerConnection = createOrGetPeerConnection(recipient);
// send a pre offer just to wake up the device on the remote side
const preOfferMsg = new CallMessage({
- timestamp: Date.now(),
+ timestamp: now,
type: SignalService.CallMessage.Type.PRE_OFFER,
uuid: currentCallUUID,
});
window.log.info('Sending preOffer message to ', ed25519Str(recipient));
-
+ const calledConvo = getConversationController().get(recipient);
+ await calledConvo?.addSingleMessage({
+ conversationId: calledConvo.id,
+ source: UserUtils.getOurPubKeyStrFromCache(),
+ type: 'outgoing',
+ sent_at: now,
+ received_at: now,
+ expireTimer: 0,
+ callNotificationType: 'started-call',
+ unread: 0,
+ });
// we do it manually as the sendToPubkeyNonDurably rely on having a message saved to the db for MessageSentSuccess
// which is not the case for a pre offer message (the message only exists in memory)
const rawMessage = await MessageUtils.toRawMessage(PubKey.cast(recipient), preOfferMsg);
@@ -464,6 +497,17 @@ export async function USER_callRecipient(recipient: string) {
await openMediaDevicesAndAddTracks();
setIsRinging(true);
await createOfferAndSendIt(recipient);
+
+ // close and end the call if callTimeoutMs is reached ans still not connected
+ global.setTimeout(async () => {
+ if (justCreatedCallUUID === currentCallUUID && getIsRinging()) {
+ window.log.info(
+ 'calling timeout reached. hanging up the call we started:',
+ justCreatedCallUUID
+ );
+ await USER_hangup(recipient);
+ }
+ }, callTimeoutMs);
}
const iceCandidates: Array = new Array();
@@ -762,6 +806,18 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
await peerConnection.addIceCandidate(candicate);
}
}
+ const now = Date.now();
+ const callerConvo = getConversationController().get(fromSender);
+ await callerConvo?.addSingleMessage({
+ conversationId: callerConvo.id,
+ source: UserUtils.getOurPubKeyStrFromCache(),
+ type: 'incoming',
+ sent_at: now,
+ received_at: now,
+ expireTimer: 0,
+ callNotificationType: 'answered-a-call',
+ unread: 0,
+ });
await buildAnswerAndSendIt(fromSender);
}
@@ -809,6 +865,7 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) {
if (ongoingCallWith && ongoingCallStatus && ongoingCallWith === fromSender) {
closeVideoCall();
}
+ await addMissedCallMessage(fromSender, Date.now());
}
async function sendCallMessageAndSync(callmessage: CallMessage, user: string) {
@@ -907,6 +964,20 @@ export function isCallRejected(uuid: string) {
return rejectedCallUUIDS.has(uuid);
}
+function getCachedMessageFromCallMessage(
+ callMessage: SignalService.CallMessage,
+ envelopeTimestamp: number
+) {
+ return {
+ type: callMessage.type,
+ sdps: callMessage.sdps,
+ sdpMLineIndexes: callMessage.sdpMLineIndexes,
+ sdpMids: callMessage.sdpMids,
+ uuid: callMessage.uuid,
+ timestamp: envelopeTimestamp,
+ };
+}
+
export async function handleCallTypeOffer(
sender: string,
callMessage: SignalService.CallMessage,
@@ -920,6 +991,9 @@ export async function handleCallTypeOffer(
window.log.info('handling callMessage OFFER with uuid: ', remoteCallUUID);
if (!getCallMediaPermissionsSettings()) {
+ const cachedMsg = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp);
+ pushCallMessageToCallCache(sender, remoteCallUUID, cachedMsg);
+
await handleMissedCall(sender, incomingOfferTimestamp, true);
return;
}
@@ -979,8 +1053,9 @@ export async function handleCallTypeOffer(
}
setIsRinging(true);
}
+ const cachedMessage = getCachedMessageFromCallMessage(callMessage, incomingOfferTimestamp);
- pushCallMessageToCallCache(sender, remoteCallUUID, callMessage);
+ pushCallMessageToCallCache(sender, remoteCallUUID, cachedMessage);
} catch (err) {
window.log?.error(`Error handling offer message ${err}`);
}
@@ -991,7 +1066,7 @@ export async function handleMissedCall(
incomingOfferTimestamp: number,
isBecauseOfCallPermission: boolean
) {
- const incomingCallConversation = await getConversationById(sender);
+ const incomingCallConversation = getConversationController().get(sender);
setIsRinging(false);
if (!isBecauseOfCallPermission) {
ToastUtils.pushedMissedCall(
@@ -1007,26 +1082,30 @@ export async function handleMissedCall(
);
}
+ await addMissedCallMessage(sender, incomingOfferTimestamp);
+ return;
+}
+
+async function addMissedCallMessage(callerPubkey: string, sentAt: number) {
+ const incomingCallConversation = getConversationController().get(callerPubkey);
+
await incomingCallConversation?.addSingleMessage({
- conversationId: incomingCallConversation.id,
- source: sender,
+ conversationId: callerPubkey,
+ source: callerPubkey,
type: 'incoming' as MessageModelType,
- sent_at: incomingOfferTimestamp,
+ sent_at: sentAt,
received_at: Date.now(),
expireTimer: 0,
- isMissedCall: true,
+ callNotificationType: 'missed-call',
unread: 1,
});
- incomingCallConversation?.updateLastMessage();
-
- return;
}
function getOwnerOfCallUUID(callUUID: string) {
for (const deviceKey of callCache.keys()) {
for (const callUUIDEntry of callCache.get(deviceKey) as Map<
string,
- Array
+ Array
>) {
if (callUUIDEntry[0] === callUUID) {
return deviceKey;
@@ -1036,7 +1115,11 @@ function getOwnerOfCallUUID(callUUID: string) {
return null;
}
-export async function handleCallTypeAnswer(sender: string, callMessage: SignalService.CallMessage) {
+export async function handleCallTypeAnswer(
+ sender: string,
+ callMessage: SignalService.CallMessage,
+ envelopeTimestamp: number
+) {
if (!callMessage.sdps || callMessage.sdps.length === 0) {
window.log.warn('cannot handle answered message without signal description proto sdps');
return;
@@ -1083,8 +1166,9 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe
} else {
window.log.info(`handling callMessage ANSWER from ${callMessageUUID}`);
}
+ const cachedMessage = getCachedMessageFromCallMessage(callMessage, envelopeTimestamp);
- pushCallMessageToCallCache(sender, callMessageUUID, callMessage);
+ pushCallMessageToCallCache(sender, callMessageUUID, cachedMessage);
if (!peerConnection) {
window.log.info('handleCallTypeAnswer without peer connection. Dropping');
@@ -1114,7 +1198,8 @@ export async function handleCallTypeAnswer(sender: string, callMessage: SignalSe
export async function handleCallTypeIceCandidates(
sender: string,
- callMessage: SignalService.CallMessage
+ callMessage: SignalService.CallMessage,
+ envelopeTimestamp: number
) {
if (!callMessage.sdps || callMessage.sdps.length === 0) {
window.log.warn('cannot handle iceCandicates message without candidates');
@@ -1126,8 +1211,9 @@ export async function handleCallTypeIceCandidates(
return;
}
window.log.info('handling callMessage ICE_CANDIDATES');
+ const cachedMessage = getCachedMessageFromCallMessage(callMessage, envelopeTimestamp);
- pushCallMessageToCallCache(sender, remoteCallUUID, callMessage);
+ pushCallMessageToCallCache(sender, remoteCallUUID, cachedMessage);
if (currentCallUUID && callMessage.uuid === currentCallUUID) {
await addIceCandidateToExistingPeerConnection(callMessage);
}
@@ -1155,13 +1241,18 @@ async function addIceCandidateToExistingPeerConnection(callMessage: SignalServic
}
// tslint:disable-next-line: no-async-without-await
-export async function handleOtherCallTypes(sender: string, callMessage: SignalService.CallMessage) {
+export async function handleOtherCallTypes(
+ sender: string,
+ callMessage: SignalService.CallMessage,
+ envelopeTimestamp: number
+) {
const remoteCallUUID = callMessage.uuid;
if (!remoteCallUUID || remoteCallUUID.length === 0) {
window.log.warn('handleOtherCallTypes has no valid uuid');
return;
}
- pushCallMessageToCallCache(sender, remoteCallUUID, callMessage);
+ const cachedMessage = getCachedMessageFromCallMessage(callMessage, envelopeTimestamp);
+ pushCallMessageToCallCache(sender, remoteCallUUID, cachedMessage);
}
function clearCallCacheFromPubkeyAndUUID(sender: string, callUUID: string) {
@@ -1181,7 +1272,7 @@ function createCallCacheForPubkeyAndUUID(sender: string, uuid: string) {
function pushCallMessageToCallCache(
sender: string,
uuid: string,
- callMessage: SignalService.CallMessage
+ callMessage: CachedCallMessageType
) {
createCallCacheForPubkeyAndUUID(sender, uuid);
callCache
@@ -1189,3 +1280,23 @@ function pushCallMessageToCallCache(
?.get(uuid)
?.push(callMessage);
}
+
+/**
+ * Called when the settings of call media permissions is set to true from the settings page.
+ * Check for any recent offer and display it to the user if needed.
+ */
+export function onTurnedOnCallMediaPermissions() {
+ // this is not ideal as this might take the not latest sender from callCache
+ callCache.forEach((sender, key) => {
+ sender.forEach(msgs => {
+ for (const msg of msgs.reverse()) {
+ if (
+ msg.type === SignalService.CallMessage.Type.OFFER &&
+ Date.now() - msg.timestamp < DURATION.MINUTES * 1
+ ) {
+ window.inboxStore?.dispatch(incomingCall({ pubkey: key }));
+ }
+ }
+ });
+ });
+}
diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts
index 6124189f5..7971b4d44 100644
--- a/ts/state/ducks/conversations.ts
+++ b/ts/state/ducks/conversations.ts
@@ -17,8 +17,9 @@ import { QuotedAttachmentType } from '../../components/conversation/Quote';
import { perfEnd, perfStart } from '../../session/utils/Performance';
import { omit } from 'lodash';
-export type PropsForMissedCallNotification = {
- isMissedCall: boolean;
+export type CallNotificationType = 'missed-call' | 'started-call' | 'answered-a-call';
+export type PropsForCallNotification = {
+ notificationType: CallNotificationType;
messageId: string;
receivedAt: number;
isUnread: boolean;
@@ -30,7 +31,7 @@ export type MessageModelPropsWithoutConvoProps = {
propsForTimerNotification?: PropsForExpirationTimer;
propsForDataExtractionNotification?: PropsForDataExtractionNotification;
propsForGroupNotification?: PropsForGroupUpdate;
- propsForMissedCall?: PropsForMissedCallNotification;
+ propsForCallNotification?: PropsForCallNotification;
};
export type MessageModelPropsWithConvoProps = SortedMessageModelProps & {
diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts
index 7ee0fe576..85e21a34c 100644
--- a/ts/state/selectors/conversations.ts
+++ b/ts/state/selectors/conversations.ts
@@ -83,19 +83,6 @@ export const getSelectedConversationIsPublic = createSelector(
}
);
-const getConversationId = (_whatever: any, id: string) => id;
-
-export const getConversationById = createSelector(
- getConversations,
- getConversationId,
- (
- state: ConversationsStateType,
- convoId: string | undefined
- ): ReduxConversationType | undefined => {
- return convoId ? state.conversationLookup[convoId] : undefined;
- }
-);
-
export const getIsTypingEnabled = createSelector(
getConversations,
getSelectedConversationKey,
@@ -190,7 +177,7 @@ export type MessagePropsType =
| 'timer-notification'
| 'regular-message'
| 'unread-indicator'
- | 'missed-call-notification';
+ | 'call-notification';
export const getSortedMessagesTypesOfSelectedConversation = createSelector(
getSortedMessagesOfSelectedConversation,
@@ -257,14 +244,14 @@ export const getSortedMessagesTypesOfSelectedConversation = createSelector(
};
}
- if (msg.propsForMissedCall) {
+ if (msg.propsForCallNotification) {
return {
showUnreadIndicator: isFirstUnread,
showDateBreak,
message: {
- messageType: 'missed-call-notification',
+ messageType: 'call-notification',
props: {
- ...msg.propsForMissedCall,
+ ...msg.propsForCallNotification,
messageId: msg.propsForMessage.id,
},
},
diff --git a/ts/test/types/Conversation_test.ts b/ts/test/types/Conversation_test.ts
index fb3d27f6f..884f66e13 100644
--- a/ts/test/types/Conversation_test.ts
+++ b/ts/test/types/Conversation_test.ts
@@ -1,4 +1,5 @@
import { assert } from 'chai';
+import { LastMessageStatusType } from '../../state/ducks/conversations';
import * as Conversation from '../../types/Conversation';
import { IncomingMessage } from '../../types/Message';
@@ -9,8 +10,8 @@ describe('Conversation', () => {
const input = {};
const expected = {
lastMessage: '',
- lastMessageStatus: null,
- timestamp: null,
+ lastMessageStatus: undefined,
+ timestamp: undefined,
};
const actual = Conversation.createLastMessageUpdate(input);
@@ -21,7 +22,7 @@ describe('Conversation', () => {
it('should update last message text and timestamp', () => {
const input = {
currentTimestamp: 555,
- lastMessageStatus: 'read',
+ lastMessageStatus: 'read' as LastMessageStatusType,
lastMessage: {
type: 'outgoing',
conversationId: 'foo',
@@ -32,7 +33,7 @@ describe('Conversation', () => {
};
const expected = {
lastMessage: 'New outgoing message',
- lastMessageStatus: 'read',
+ lastMessageStatus: 'read' as LastMessageStatusType,
timestamp: 666,
};
@@ -60,7 +61,7 @@ describe('Conversation', () => {
};
const expected = {
lastMessage: 'Last message before expired',
- lastMessageStatus: null,
+ lastMessageStatus: undefined,
timestamp: 555,
};
diff --git a/ts/types/Conversation.ts b/ts/types/Conversation.ts
index 9f3289e3d..e8de28a70 100644
--- a/ts/types/Conversation.ts
+++ b/ts/types/Conversation.ts
@@ -1,9 +1,10 @@
+import { LastMessageStatusType } from '../state/ducks/conversations';
import { Message } from './Message';
interface ConversationLastMessageUpdate {
lastMessage: string;
- lastMessageStatus: string | null;
- timestamp: number | null;
+ lastMessageStatus: LastMessageStatusType;
+ timestamp: number | undefined;
}
export const createLastMessageUpdate = ({
@@ -14,14 +15,14 @@ export const createLastMessageUpdate = ({
}: {
currentTimestamp?: number;
lastMessage?: Message;
- lastMessageStatus?: string;
+ lastMessageStatus?: LastMessageStatusType;
lastMessageNotificationText?: string;
}): ConversationLastMessageUpdate => {
if (!lastMessage) {
return {
lastMessage: '',
- lastMessageStatus: null,
- timestamp: null,
+ lastMessageStatus: undefined,
+ timestamp: undefined,
};
}
@@ -35,7 +36,7 @@ export const createLastMessageUpdate = ({
return {
lastMessage: lastMessageNotificationText || '',
- lastMessageStatus: lastMessageStatus || null,
- timestamp: newTimestamp || null,
+ lastMessageStatus: lastMessageStatus || undefined,
+ timestamp: newTimestamp || undefined,
};
};
diff --git a/ts/types/Message.ts b/ts/types/Message.ts
index 82d41b78a..487833518 100644
--- a/ts/types/Message.ts
+++ b/ts/types/Message.ts
@@ -51,6 +51,6 @@ type MessageSchemaVersion5 = Partial<
export type LokiProfile = {
displayName: string;
- avatarPointer: string;
+ avatarPointer?: string;
profileKey: Uint8Array | null;
};
diff --git a/ts/util/attachmentsUtil.ts b/ts/util/attachmentsUtil.ts
index 5cdb01f4e..fc6f04d12 100644
--- a/ts/util/attachmentsUtil.ts
+++ b/ts/util/attachmentsUtil.ts
@@ -151,7 +151,7 @@ export const saveAttachmentToDisk = async ({
messageSender: string;
conversationId: string;
}) => {
- const decryptedUrl = await getDecryptedMediaUrl(attachment.url, attachment.contentType);
+ const decryptedUrl = await getDecryptedMediaUrl(attachment.url, attachment.contentType, false);
save({
attachment: { ...attachment, url: decryptedUrl },
document,