Merge branch 'clearnet' into message-requests

This commit is contained in:
warrickct 2021-11-24 16:25:07 +11:00
commit 4576f3cb32
45 changed files with 753 additions and 567 deletions

View File

@ -60,7 +60,6 @@ module.exports = {
getUnreadByConversation,
getUnreadCountByConversation,
getMessageBySender,
getMessageBySenderAndServerId,
getMessageBySenderAndServerTimestamp,
getMessageBySenderAndTimestamp,
getMessageIdsFromServerIds,
@ -2058,21 +2057,6 @@ function getMessageBySender({ source, sourceDevice, sentAt }) {
return map(rows, row => jsonToObject(row.json));
}
function getMessageBySenderAndServerId({ source, serverId }) {
const rows = globalInstance
.prepare(
`SELECT json FROM ${MESSAGES_TABLE} WHERE
source = $source AND
serverId = $serverId;`
)
.all({
source,
serverId,
});
return map(rows, row => jsonToObject(row.json));
}
function getMessageBySenderAndTimestamp({ source, timestamp }) {
const rows = globalInstance
.prepare(

View File

@ -26,13 +26,6 @@ message WebSocketRequestMessage {
optional uint64 id = 4;
}
message WebSocketResponseMessage {
optional uint64 id = 1;
optional uint32 status = 2;
optional string message = 3;
repeated string headers = 5;
optional bytes body = 4;
}
message WebSocketMessage {
enum Type {
@ -43,5 +36,4 @@ message WebSocketMessage {
optional Type type = 1;
optional WebSocketRequestMessage request = 2;
optional WebSocketResponseMessage response = 3;
}

View File

@ -23,6 +23,7 @@ type Props = {
base64Data?: string; // if this is not empty, it will be used to render the avatar with base64 encoded data
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
onAvatarClick?: () => void;
dataTestId?: string;
};
const Identicon = (props: Props) => {
@ -92,7 +93,7 @@ const AvatarImage = (props: {
};
const AvatarInner = (props: Props) => {
const { avatarPath, base64Data, size, memberAvatars, name } = props;
const { avatarPath, base64Data, size, memberAvatars, name, dataTestId } = props;
const [imageBroken, setImageBroken] = useState(false);
// contentType is not important
const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', '');
@ -122,6 +123,7 @@ const AvatarInner = (props: Props) => {
props.onAvatarClick?.();
}}
role="button"
data-testid={dataTestId}
>
{hasImage ? (
<AvatarImage

View File

@ -14,8 +14,6 @@ import {
getConversationHeaderProps,
getConversationHeaderTitleProps,
getCurrentNotificationSettingText,
getHasIncomingCall,
getHasOngoingCall,
getIsSelectedNoteToSelf,
getIsSelectedPrivate,
getSelectedConversation,
@ -40,6 +38,7 @@ import {
resetSelectedMessageIds,
} from '../../state/ducks/conversations';
import { callRecipient } from '../../interactions/conversationInteractions';
import { getHasIncomingCall, getHasOngoingCall } from '../../state/selectors/call';
export interface TimerOption {
name: string;
@ -107,7 +106,7 @@ const SelectionOverlay = () => {
return (
<div className="message-selection-overlay">
<div className="close-button">
<SessionIconButton iconType="exit" iconSize={'medium'} onClick={onCloseOverlay} />
<SessionIconButton iconType="exit" iconSize="medium" onClick={onCloseOverlay} />
</div>
<div className="button-group">
@ -145,7 +144,7 @@ const TripleDotsMenu = (props: { triggerId: string; showBackButton: boolean }) =
});
}}
>
<SessionIconButton iconType="ellipses" iconSize={'medium'} />
<SessionIconButton iconType="ellipses" iconSize="medium" />
</div>
);
};

View File

@ -45,7 +45,7 @@ const Sections = (props: Props & { selectedTab: TabType }) => {
const label =
type === 'media' ? window.i18n('mediaEmptyState') : window.i18n('documentsEmptyState');
return <EmptyState data-test="EmptyState" label={label} />;
return <EmptyState data-testid="EmptyState" label={label} />;
}
return (

View File

@ -82,7 +82,7 @@ export class EditProfileDialog extends React.Component<{}, State> {
: undefined;
return (
<div className="edit-profile-dialog">
<div className="edit-profile-dialog" data-testid="edit-profile-dialog">
<SessionWrapperModal
title={i18n('editProfileModalTitle')}
onClose={this.closeDialog}
@ -97,7 +97,10 @@ export class EditProfileDialog extends React.Component<{}, State> {
<div className="session-id-section">
<PillDivider text={window.i18n('yourSessionID')} />
<p className={classNames('text-selectable', 'session-id-section-display')}>
<p
className={classNames('text-selectable', 'session-id-section-display')}
data-testid="your-session-id"
>
{sessionID}
</p>
@ -182,10 +185,10 @@ export class EditProfileDialog extends React.Component<{}, State> {
{this.renderProfileHeader()}
<div className="profile-name-uneditable">
<p>{name}</p>
<p data-testid="your-profile-name">{name}</p>
<SessionIconButton
iconType="pencil"
iconSize={'medium'}
iconSize="medium"
onClick={() => {
this.setState({ mode: 'edit' });
}}

View File

@ -150,8 +150,9 @@ export const ModalStatusLight = (props: StatusLightType) => {
export const ActionPanelOnionStatusLight = (props: {
isSelected: boolean;
handleClick: () => void;
dataTestId?: string;
}) => {
const { isSelected, handleClick } = props;
const { isSelected, handleClick, dataTestId } = props;
const onionPathsCount = useSelector(getOnionPathsCount);
const firstPathLength = useSelector(getFirstOnionPathLength);
@ -179,6 +180,7 @@ export const ActionPanelOnionStatusLight = (props: {
glowStartDelay={0}
noScale={true}
isSelected={isSelected}
dataTestId={dataTestId}
/>
);
};

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { SessionIconButton, SessionIconType } from './icon';
import { SessionIconButton } from './icon';
import { Avatar, AvatarSize } from '../Avatar';
import { SessionToastContainer } from './SessionToastContainer';
import { getConversationController } from '../../session/conversations';
@ -96,47 +96,71 @@ const Section = (props: { type: SectionType; avatarPath?: string | null }) => {
onAvatarClick={handleClick}
name={userName}
pubkey={ourNumber}
dataTestId="leftpane-primary-avatar"
/>
);
}
const unreadToShow = type === SectionType.Message ? unreadMessageCount : undefined;
let iconType: SessionIconType;
switch (type) {
case SectionType.Message:
iconType = 'chatBubble';
break;
case SectionType.Contact:
iconType = 'users';
break;
case SectionType.Settings:
iconType = 'gear';
break;
case SectionType.Moon:
iconType = 'moon';
break;
default:
iconType = 'moon';
}
const iconColor = undefined;
return (
<>
{type === SectionType.PathIndicator ? (
<ActionPanelOnionStatusLight handleClick={handleClick} isSelected={isSelected} />
) : (
return (
<SessionIconButton
iconSize={'medium'}
iconType={iconType}
iconColor={iconColor}
iconSize="medium"
dataTestId="message-section"
iconType={'chatBubble'}
iconColor={undefined}
notificationCount={unreadToShow}
onClick={handleClick}
isSelected={isSelected}
/>
)}
</>
);
);
case SectionType.Contact:
return (
<SessionIconButton
iconSize="medium"
dataTestId="contact-section"
iconType={'users'}
iconColor={undefined}
notificationCount={unreadToShow}
onClick={handleClick}
isSelected={isSelected}
/>
);
case SectionType.Settings:
return (
<SessionIconButton
iconSize="medium"
dataTestId="settings-section"
iconType={'gear'}
iconColor={undefined}
notificationCount={unreadToShow}
onClick={handleClick}
isSelected={isSelected}
/>
);
case SectionType.PathIndicator:
return (
<ActionPanelOnionStatusLight
dataTestId="onion-status-section"
handleClick={handleClick}
isSelected={isSelected}
/>
);
default:
return (
<SessionIconButton
iconSize="medium"
iconType={'moon'}
dataTestId="theme-section"
iconColor={undefined}
notificationCount={unreadToShow}
onClick={handleClick}
isSelected={isSelected}
/>
);
}
};
const cleanUpMediasInterval = DURATION.MINUTES * 30;
@ -300,7 +324,10 @@ export const ActionsPanel = () => {
<ModalContainer />
<CallContainer />
<div className="module-left-pane__sections-container">
<div
className="module-left-pane__sections-container"
data-testid="leftpane-section-container"
>
<Section type={SectionType.Profile} avatarPath={ourPrimaryConversation.avatarPath} />
<Section type={SectionType.Message} />
<Section type={SectionType.Contact} />

View File

@ -58,7 +58,7 @@ const LeftPaneSettingsCategoryRow = (props: { item: any }) => {
<div>
{item.id === focusedSettingsSection && (
<SessionIcon iconSize={'medium'} iconType="chevron" iconRotation={270} />
<SessionIcon iconSize="medium" iconType="chevron" iconRotation={270} />
)}
</div>
</div>

View File

@ -26,6 +26,7 @@ import { PersistGate } from 'redux-persist/integration/react';
import { persistStore } from 'redux-persist';
import { TimerOptionsArray } from '../../state/ducks/timerOptions';
import { getEmptyStagedAttachmentsState } from '../../state/ducks/stagedAttachments';
import { initialCallState } from '../../state/ducks/call';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
@ -105,6 +106,7 @@ export class SessionInboxView extends React.Component<any, State> {
timerOptions,
},
stagedAttachments: getEmptyStagedAttachmentsState(),
call: initialCallState,
};
this.store = createStore(initialState);

View File

@ -108,7 +108,7 @@ export class SessionInput extends React.PureComponent<Props, State> {
return (
<SessionIconButton
iconType="eye"
iconSize={'medium'}
iconSize="medium"
onClick={() => {
this.setState({
forceShow: !this.state.forceShow,

View File

@ -72,7 +72,7 @@ export const SessionMemberListItem = (props: Props) => {
<span className="session-member-item__name">{name}</span>
</div>
<span className={classNames('session-member-item__checkmark', isSelected && 'selected')}>
<SessionIcon iconType="check" iconSize={'medium'} iconColor={Constants.UI.COLORS.GREEN} />
<SessionIcon iconType="check" iconSize="medium" iconColor={Constants.UI.COLORS.GREEN} />
</span>
</div>
);

View File

@ -21,7 +21,7 @@ export const SessionSearchInput = (props: Props) => {
return (
<div className="session-search-input">
<SessionIconButton iconSize={'medium'} iconType="search" />
<SessionIconButton iconSize="medium" iconType="search" />
<input
value={searchString}
onChange={e => onChange(e.target.value)}

View File

@ -1,11 +1,11 @@
import { SessionIconButton } from '../icon';
import { animation, contextMenu, Item, Menu } from 'react-contexify';
import { InputItem } from '../../../session/utils/CallManager';
import { setFullScreenCall } from '../../../state/ducks/conversations';
import { InputItem } from '../../../session/utils/calling/CallManager';
import { setFullScreenCall } from '../../../state/ducks/call';
import { CallManager, ToastUtils } from '../../../session/utils';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getHasOngoingCallWithPubkey } from '../../../state/selectors/conversations';
import { getHasOngoingCallWithPubkey } from '../../../state/selectors/call';
import { DropDownAndToggleButton } from '../icon/DropDownAndToggleButton';
import styled from 'styled-components';
@ -71,16 +71,16 @@ export const AudioInputButton = ({
export const AudioOutputButton = ({
currentConnectedAudioOutputs,
}: // isAudioOutputMuted,
// hideArrowIcon = false,
{
isAudioOutputMuted,
hideArrowIcon = false,
}: {
currentConnectedAudioOutputs: Array<InputItem>;
isAudioOutputMuted: boolean;
hideArrowIcon?: boolean;
}) => {
return (
<>
{/* <DropDownAndToggleButton
<DropDownAndToggleButton
iconType="volume"
isMuted={isAudioOutputMuted}
onMainButtonClick={() => {
@ -90,7 +90,7 @@ export const AudioOutputButton = ({
showAudioOutputMenu(currentConnectedAudioOutputs, e);
}}
hidePopoverArrow={hideArrowIcon}
/> */}
/>
<AudioOutputMenu
triggerId={audioOutputTriggerId}
@ -238,19 +238,19 @@ const showAudioInputMenu = (
});
};
// const showAudioOutputMenu = (
// currentConnectedAudioOutputs: Array<any>,
// e: React.MouseEvent<HTMLDivElement>
// ) => {
// if (currentConnectedAudioOutputs.length === 0) {
// ToastUtils.pushNoAudioOutputFound();
// return;
// }
// contextMenu.show({
// id: audioOutputTriggerId,
// event: e,
// });
// };
const showAudioOutputMenu = (
currentConnectedAudioOutputs: Array<any>,
e: React.MouseEvent<HTMLDivElement>
) => {
if (currentConnectedAudioOutputs.length === 0) {
ToastUtils.pushNoAudioOutputFound();
return;
}
contextMenu.show({
id: audioOutputTriggerId,
event: e,
});
};
const showVideoInputMenu = (
currentConnectedCameras: Array<InputItem>,
@ -300,22 +300,22 @@ const handleMicrophoneToggle = async (
}
};
// const handleSpeakerToggle = async (
// currentConnectedAudioOutputs: Array<InputItem>,
// isAudioOutputMuted: boolean
// ) => {
// if (!currentConnectedAudioOutputs.length) {
// ToastUtils.pushNoAudioInputFound();
const handleSpeakerToggle = async (
currentConnectedAudioOutputs: Array<InputItem>,
isAudioOutputMuted: boolean
) => {
if (!currentConnectedAudioOutputs.length) {
ToastUtils.pushNoAudioInputFound();
// return;
// }
// if (isAudioOutputMuted) {
// // selects the first one
// await CallManager.selectAudioOutputByDeviceId(currentConnectedAudioOutputs[0].deviceId);
// } else {
// await CallManager.selectAudioOutputByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID);
// }
// };
return;
}
if (isAudioOutputMuted) {
// selects the first one
await CallManager.selectAudioOutputByDeviceId(currentConnectedAudioOutputs[0].deviceId);
} else {
await CallManager.selectAudioOutputByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID);
}
};
const StyledCallWindowControls = styled.div`
position: absolute;

View File

@ -4,11 +4,11 @@ import { useDispatch, useSelector } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener';
import { setFullScreenCall } from '../../../state/ducks/conversations';
import { setFullScreenCall } from '../../../state/ducks/call';
import {
getCallIsInFullScreen,
getHasOngoingCallWithFocusedConvo,
} from '../../../state/selectors/conversations';
} from '../../../state/selectors/call';
import { CallWindowControls } from './CallButtons';
import { StyledVideoElement } from './DraggableCallContainer';

View File

@ -4,11 +4,8 @@ import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
import styled from 'styled-components';
import _ from 'underscore';
import {
getHasOngoingCall,
getHasOngoingCallWith,
getSelectedConversationKey,
} from '../../../state/selectors/conversations';
import { getSelectedConversationKey } from '../../../state/selectors/conversations';
import { getHasOngoingCall, getHasOngoingCallWith } from '../../../state/selectors/call';
import { openConversationWithMessages } from '../../../state/ducks/conversations';
import { Avatar, AvatarSize } from '../../Avatar';
import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener';

View File

@ -5,12 +5,13 @@ import styled from 'styled-components';
import _ from 'underscore';
import { UserUtils } from '../../../session/utils';
import {
getCallIsInFullScreen,
getHasOngoingCallWith,
getHasOngoingCallWithFocusedConvo,
getHasOngoingCallWithFocusedConvoIsOffering,
getHasOngoingCallWithFocusedConvosIsConnecting,
getHasOngoingCallWithPubkey,
} from '../../../state/selectors/conversations';
} from '../../../state/selectors/call';
import { StyledVideoElement } from './DraggableCallContainer';
import { Avatar, AvatarSize } from '../../Avatar';
@ -23,7 +24,7 @@ import {
import { useModuloWithTripleDots } from '../../../hooks/useModuloWithTripleDots';
import { CallWindowControls } from './CallButtons';
import { SessionSpinner } from '../SessionSpinner';
import { DEVICE_DISABLED_DEVICE_ID } from '../../../session/utils/CallManager';
import { DEVICE_DISABLED_DEVICE_ID } from '../../../session/utils/calling/CallManager';
const VideoContainer = styled.div`
height: 100%;
@ -119,6 +120,8 @@ export const VideoLoadingSpinner = (props: { fullWidth: boolean }) => {
export const InConversationCallContainer = () => {
const ongoingCallProps = useSelector(getHasOngoingCallWith);
const isInFullScreen = useSelector(getCallIsInFullScreen);
const ongoingCallPubkey = useSelector(getHasOngoingCallWithPubkey);
const ongoingCallWithFocused = useSelector(getHasOngoingCallWithFocusedConvo);
const ongoingCallUsername = ongoingCallProps?.profileName || ongoingCallProps?.name;
@ -156,14 +159,19 @@ export const InConversationCallContainer = () => {
if (videoRefRemote.current) {
if (currentSelectedAudioOutput === DEVICE_DISABLED_DEVICE_ID) {
videoRefLocal.current.muted = true;
videoRefRemote.current.muted = true;
} else {
// void videoRefLocal.current.setSinkId(currentSelectedAudioOutput);
videoRefLocal.current.muted = false;
void (videoRefRemote.current as any)?.setSinkId(currentSelectedAudioOutput);
videoRefRemote.current.muted = false;
}
}
}
if (isInFullScreen && videoRefRemote.current) {
// disable this video element so the one in fullscreen is the only one playing audio
videoRefRemote.current.muted = true;
}
if (!ongoingCallWithFocused) {
return null;
}

View File

@ -6,7 +6,7 @@ import _ from 'underscore';
import { useAvatarPath, useConversationUsername } from '../../../hooks/useParamSelector';
import { ed25519Str } from '../../../session/onions/onionPath';
import { CallManager } from '../../../session/utils';
import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/conversations';
import { getHasIncomingCall, getHasIncomingCallFrom } from '../../../state/selectors/call';
import { Avatar, AvatarSize } from '../../Avatar';
import { SessionButton, SessionButtonColor } from '../SessionButton';
import { SessionWrapperModal } from '../SessionWrapperModal';

View File

@ -125,16 +125,16 @@ export class SessionRecording extends React.Component<Props, State> {
{isRecording && (
<SessionIconButton
iconType="pause"
iconSize={'medium'}
iconSize="medium"
iconColor={Constants.UI.COLORS.DANGER_ALT}
onClick={actionPauseFn}
/>
)}
{actionPauseAudio && (
<SessionIconButton iconType="pause" iconSize={'medium'} onClick={actionPauseFn} />
<SessionIconButton iconType="pause" iconSize="medium" onClick={actionPauseFn} />
)}
{hasRecordingAndPaused && (
<SessionIconButton iconType="play" iconSize={'medium'} onClick={this.playAudio} />
<SessionIconButton iconType="play" iconSize="medium" onClick={this.playAudio} />
)}
{hasRecording && (
<SessionIconButton

View File

@ -133,7 +133,7 @@ const HeaderItem = () => {
<div className="group-settings-header">
<SessionIconButton
iconType="chevron"
iconSize={'medium'}
iconSize="medium"
iconRotation={270}
onClick={() => {
dispatch(closeRightPanel());

View File

@ -10,6 +10,7 @@ interface SProps extends SessionIconProps {
isSelected?: boolean;
isHidden?: boolean;
margin?: string;
dataTestId?: string;
}
const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props, ref) => {
@ -43,6 +44,7 @@ const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props,
ref={ref}
onClick={clickHandler}
style={{ display: isHidden ? 'none' : 'flex', margin: margin ? margin : '' }}
data-testid={props.dataTestId}
>
<SessionIcon
iconType={iconType}

View File

@ -1,10 +1,7 @@
import React from 'react';
import {
getHasIncomingCall,
getHasOngoingCall,
getNumberOfPinnedConversations,
} from '../../../state/selectors/conversations';
import { getHasIncomingCall, getHasOngoingCall } from '../../../state/selectors/call';
import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations';
import { getFocusedSection } from '../../../state/selectors/section';
import { Item, Submenu } from 'react-contexify';
import {

View File

@ -48,7 +48,7 @@ const SessionInfo = () => {
<div className="session-settings__version-info">
<span className="text-selectable">v{window.versionInfo.version}</span>
<span>
<SessionIconButton iconSize={'medium'} iconType="oxen" onClick={openOxenWebsite} />
<SessionIconButton iconSize="medium" iconType="oxen" onClick={openOxenWebsite} />
</span>
<span className="text-selectable">{window.versionInfo.commitHash}</span>
</div>

View File

@ -110,7 +110,6 @@ const channelsToMake = {
removeAllMessagesInConversation,
getMessageBySender,
getMessageBySenderAndServerId,
getMessageBySenderAndServerTimestamp,
getMessageBySenderAndTimestamp,
getMessageIdsFromServerIds,
@ -690,24 +689,6 @@ export async function getMessageBySender({
return new MessageModel(messages[0]);
}
export async function getMessageBySenderAndServerId({
source,
serverId,
}: {
source: string;
serverId: number;
}): Promise<MessageModel | null> {
const messages = await channels.getMessageBySenderAndServerId({
source,
serverId,
});
if (!messages || !messages.length) {
return null;
}
return new MessageModel(messages[0]);
}
export async function getMessageBySenderAndServerTimestamp({
source,
serverTimestamp,

View File

@ -2,17 +2,15 @@ import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
// tslint:disable-next-line: no-submodule-imports
import useMountedState from 'react-use/lib/useMountedState';
import { CallManager } from '../session/utils';
import {
addVideoEventsListener,
CallManagerOptionsType,
DEVICE_DISABLED_DEVICE_ID,
InputItem,
} from '../session/utils/CallManager';
import {
getCallIsInFullScreen,
getHasOngoingCallWithPubkey,
getSelectedConversationKey,
} from '../state/selectors/conversations';
removeVideoEventsListener,
} from '../session/utils/calling/CallManager';
import { getSelectedConversationKey } from '../state/selectors/conversations';
import { getCallIsInFullScreen, getHasOngoingCallWithPubkey } from '../state/selectors/call';
export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
const selectedConversationKey = useSelector(getSelectedConversationKey);
@ -43,7 +41,7 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
(onSame && ongoingCallPubkey === selectedConversationKey) ||
(!onSame && ongoingCallPubkey !== selectedConversationKey)
) {
CallManager.addVideoEventsListener(uniqueId, (options: CallManagerOptionsType) => {
addVideoEventsListener(uniqueId, (options: CallManagerOptionsType) => {
const {
audioInputsList,
audioOutputsList,
@ -71,7 +69,7 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
}
return () => {
CallManager.removeVideoEventsListener(uniqueId);
removeVideoEventsListener(uniqueId);
};
}, [ongoingCallPubkey, selectedConversationKey, isFullScreen]);

View File

@ -458,8 +458,6 @@ export async function callRecipient(pubkey: string, canCall: boolean) {
}
if (convo && convo.isPrivate() && !convo.isMe()) {
convo.callState = 'offering';
await convo.commit();
await CallManager.USER_callRecipient(convo.id);
}
}

View File

@ -180,8 +180,6 @@ export const fillConvoAttributesWithDefaults = (
});
};
export type CallState = 'offering' | 'incoming' | 'connecting' | 'ongoing' | undefined;
export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public updateLastMessage: () => any;
public throttledBumpTyping: () => void;
@ -189,8 +187,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public markRead: (newestUnreadDate: number, providedOptions?: any) => Promise<void>;
public initialPromise: any;
public callState: CallState;
private typingRefreshTimer?: NodeJS.Timeout | null;
private typingPauseTimer?: NodeJS.Timeout | null;
private typingTimer?: NodeJS.Timeout | null;
@ -446,7 +442,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const left = !!this.get('left');
const expireTimer = this.get('expireTimer');
const currentNotificationSetting = this.get('triggerNotificationsFor');
const callState = this.callState;
// to reduce the redux store size, only set fields which cannot be undefined
// for instance, a boolean can usually be not set if false, etc
@ -554,10 +549,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
text: lastMessageText,
};
}
if (callState) {
toRet.callState = callState;
}
return toRet;
}

View File

@ -15,7 +15,7 @@ import {
export async function removeFromCache(envelope: EnvelopePlus) {
const { id } = envelope;
window?.log?.info(`removing from cache envelope: ${id}`);
// window?.log?.info(`removing from cache envelope: ${id}`);
return removeUnprocessed(id);
}
@ -25,7 +25,7 @@ export async function addToCache(
messageHash: string
) {
const { id } = envelope;
window?.log?.info(`adding to cache envelope: ${id}`);
// window?.log?.info(`adding to cache envelope: ${id}`);
const encodedEnvelope = StringUtils.decode(plaintext, 'base64');
const data: UnprocessedParameter = {

View File

@ -17,6 +17,24 @@ export async function handleCallMessage(
const { type } = callMessage;
// we just allow self send of ANSWER message to remove the incoming call dialog when we accepted it from another device
if (
sender === UserUtils.getOurPubKeyStrFromCache() &&
callMessage.type !== SignalService.CallMessage.Type.ANSWER &&
callMessage.type !== SignalService.CallMessage.Type.END_CALL
) {
window.log.info('Dropping incoming call from ourself');
await removeFromCache(envelope);
return;
}
if (CallManager.isCallRejected(callMessage.uuid)) {
await removeFromCache(envelope);
window.log.info(`Dropping already rejected call from this device ${callMessage.uuid}`);
return;
}
if (type === SignalService.CallMessage.Type.PROVISIONAL_ANSWER) {
await removeFromCache(envelope);
@ -48,7 +66,7 @@ export async function handleCallMessage(
if (type === SignalService.CallMessage.Type.END_CALL) {
await removeFromCache(envelope);
CallManager.handleCallTypeEndCall(sender, callMessage.uuid);
await CallManager.handleCallTypeEndCall(sender, callMessage.uuid);
return;
}

View File

@ -210,7 +210,7 @@ async function decryptUnidentifiedSender(
envelope: EnvelopePlus,
ciphertext: ArrayBuffer
): Promise<ArrayBuffer | null> {
window?.log?.info('received unidentified sender message');
// window?.log?.info('received unidentified sender message');
try {
const userX25519KeyPair = await UserUtils.getIdentityKeyPair();

View File

@ -12,11 +12,7 @@ import { getConversationController } from '../session/conversations';
import { handleClosedGroupControlMessage } from './closedGroups';
import { MessageModel } from '../models/message';
import { MessageModelType } from '../models/messageType';
import {
getMessageBySender,
getMessageBySenderAndServerId,
getMessageBySenderAndServerTimestamp,
} from '../../ts/data/data';
import { getMessageBySender, getMessageBySenderAndServerTimestamp } from '../../ts/data/data';
import { ConversationModel, ConversationTypeEnum } from '../models/conversation';
import { allowOnlyOneAtATime } from '../session/utils/Promise';
import { toHex } from '../session/utils/String';
@ -371,22 +367,14 @@ export async function isMessageDuplicate({
try {
let result;
if (serverId || serverTimestamp) {
// first try to find a duplicate serverId from this sender
if (serverId) {
result = await getMessageBySenderAndServerId({
source,
serverId,
});
}
// if no result, try to find a duplicate with the same serverTimestamp from this sender
// first try to find a duplicate with the same serverTimestamp from this sender
if (!result && serverTimestamp) {
result = await getMessageBySenderAndServerTimestamp({
source,
serverTimestamp,
});
}
// if we have a result, it means a specific user sent two messages either with the same
// serverId or the same serverTimestamp.
// if we have a result, it means a specific user sent two messages either with the same serverTimestamp.
// no need to do anything else, those messages must be the same
// Note: this test is not based on which conversation the user sent the message
// but we consider that a user sending two messages with the same serverTimestamp is unlikely

View File

@ -72,7 +72,7 @@ const envelopeQueue = new EnvelopeQueue();
function queueEnvelope(envelope: EnvelopePlus, messageHash?: string) {
const id = getEnvelopeId(envelope);
window?.log?.info('queueing envelope', id);
// window?.log?.info('queueing envelope', id);
const task = handleEnvelope.bind(null, envelope, messageHash);
const taskWithTimeout = createTaskWithTimeout(task, `queueEnvelope ${id}`);

View File

@ -20,6 +20,7 @@ import { MessageSender } from '.';
import { getMessageById } from '../../../ts/data/data';
import { SNodeAPI } from '../snode_api';
import { getConversationController } from '../conversations';
import { ed25519Str } from '../onions/onionPath';
const DEFAULT_CONNECTIONS = 1;
@ -129,7 +130,7 @@ export async function TEST_sendMessageToSnode(
const data64 = window.dcodeIO.ByteBuffer.wrap(data).toString('base64');
const swarm = await getSwarmFor(pubKey);
window?.log?.debug('Sending envelope with timestamp: ', timestamp, ' to ', pubKey);
window?.log?.debug('Sending envelope with timestamp: ', timestamp, ' to ', ed25519Str(pubKey));
// send parameters
const params = {
pubKey,
@ -190,7 +191,9 @@ export async function TEST_sendMessageToSnode(
}
window?.log?.info(
`loki_message:::sendMessage - Successfully stored message to ${pubKey} via ${snode.ip}:${snode.port}`
`loki_message:::sendMessage - Successfully stored message to ${ed25519Str(pubKey)} via ${
snode.ip
}:${snode.port}`
);
}

View File

@ -1,13 +1,12 @@
import _ from 'lodash';
import { getMessageById } from '../../data/data';
import { MessageModel } from '../../models/message';
import { SignalService } from '../../protobuf';
import { PnServer } from '../../pushnotification';
import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
import { EncryptionType, RawMessage } from '../types';
import { UserUtils } from '../utils';
// tslint:disable-next-line no-unnecessary-class
// tslint:disable-next-line: no-unnecessary-class
export class MessageSentHandler {
public static async handlePublicMessageSentSuccess(
sentMessage: OpenGroupVisibleMessage,
@ -54,10 +53,8 @@ export class MessageSentHandler {
let sentTo = fetchedMessage.get('sent_to') || [];
let isOurDevice = false;
if (sentMessage.device) {
isOurDevice = UserUtils.isUsFromCache(sentMessage.device);
}
const isOurDevice = UserUtils.isUsFromCache(sentMessage.device);
// FIXME this is not correct and will cause issues with syncing
// At this point the only way to check for medium
// group is by comparing the encryption type
@ -113,8 +110,9 @@ export class MessageSentHandler {
window?.log?.warn(
'Got an error while trying to sendSyncMessage(): fetchedMessage is null'
);
return;
}
fetchedMessage = tempFetchMessage as MessageModel;
fetchedMessage = tempFetchMessage;
} catch (e) {
window?.log?.warn('Got an error while trying to sendSyncMessage():', e);
}

View File

@ -318,7 +318,6 @@ export class SwarmPolling {
}
private loadGroupIds() {
// Start polling for medium size groups as well (they might be in different swarms)
const convos = getConversationController().getConversations();
const mediumGroupsOnly = convos.filter(
@ -328,7 +327,6 @@ export class SwarmPolling {
mediumGroupsOnly.forEach((c: any) => {
this.addGroupId(new PubKey(c.id));
// TODO: unsubscribe if the group is deleted
});
}

View File

@ -1,33 +1,42 @@
import _ from 'lodash';
import { MessageUtils, ToastUtils } from '.';
import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings';
import { getConversationById } from '../../data/data';
import { ConversationModel } from '../../models/conversation';
import { MessageModelType } from '../../models/messageType';
import { SignalService } from '../../protobuf';
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';
import {
answerCall,
callConnected,
CallStatusEnum,
endCall,
incomingCall,
openConversationWithMessages,
setFullScreenCall,
startingCallWith,
} from '../../state/ducks/conversations';
import { getConversationController } from '../conversations';
import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
import { ed25519Str } from '../onions/onionPath';
import { getMessageQueue, MessageSender } from '../sending';
import { PubKey } from '../types';
} from '../../../state/ducks/call';
import { getConversationController } from '../../conversations';
import { CallMessage } from '../../messages/outgoing/controlMessage/CallMessage';
import { ed25519Str } from '../../onions/onionPath';
import { PubKey } from '../../types';
import { v4 as uuidv4 } from 'uuid';
import { PnServer } from '../../pushnotification';
import { setIsRinging } from './RingingManager';
import { PnServer } from '../../../pushnotification';
import { setIsRinging } from '../RingingManager';
import { getBlackSilenceMediaStream } from './Silence';
import { getMessageQueue } from '../..';
import { MessageSender } from '../../sending';
// tslint:disable: function-name
export type InputItem = { deviceId: string; label: string };
/**
* This uuid is set only once we accepted a call or started one.
*/
let currentCallUUID: string | undefined;
const rejectedCallUUIDS: Set<string> = new Set();
export type CallManagerOptionsType = {
localStream: MediaStream | null;
remoteStream: MediaStream | null;
@ -47,7 +56,7 @@ function callVideoListeners() {
if (videoEventsListeners.length) {
videoEventsListeners.forEach(item => {
item.listener?.({
localStream: mediaDevices,
localStream,
remoteStream,
camerasList,
audioInputsList,
@ -80,14 +89,14 @@ export function removeVideoEventsListener(uniqueId: string) {
}
/**
* This field stores all the details received about a specific call with the same uuid. It is a per pubkey and per device cache.
* 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<string, Map<string, Array<SignalService.CallMessage>>>();
let peerConnection: RTCPeerConnection | null;
let dataChannel: RTCDataChannel | null;
let remoteStream: MediaStream | null;
let mediaDevices: MediaStream | null;
let localStream: MediaStream | null;
let remoteVideoStreamIsMuted = true;
export const DEVICE_DISABLED_DEVICE_ID = 'off';
@ -103,8 +112,38 @@ const configuration: RTCConfiguration = {
iceServers: [
{
urls: 'turn:freyr.getsession.org',
username: 'session',
credential: 'session',
username: 'session202111',
credential: '053c268164bc7bd7',
},
{
urls: 'turn:fenrir.getsession.org',
username: 'session202111',
credential: '053c268164bc7bd7',
},
{
urls: 'turn:frigg.getsession.org',
username: 'session202111',
credential: '053c268164bc7bd7',
},
{
urls: 'turn:angus.getsession.org',
username: 'session202111',
credential: '053c268164bc7bd7',
},
{
urls: 'turn:hereford.getsession.org',
username: 'session202111',
credential: '053c268164bc7bd7',
},
{
urls: 'turn:holstein.getsession.org',
username: 'session202111',
credential: '053c268164bc7bd7',
},
{
urls: 'turn:brahman.getsession.org',
username: 'session202111',
credential: '053c268164bc7bd7',
},
],
// iceTransportPolicy: 'relay', // for now, this cause the connection to break after 30-40 sec if we enable this
@ -184,6 +223,14 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
if (sender?.track) {
sender.track.enabled = false;
}
// do the same changes locally
localStream?.getVideoTracks().forEach(t => {
t.stop();
localStream?.removeTrack(t);
});
localStream?.addTrack(getBlackSilenceMediaStream().getVideoTracks()[0]);
sendVideoStatusViaDataChannel();
callVideoListeners();
return;
@ -200,28 +247,37 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
try {
const newVideoStream = await navigator.mediaDevices.getUserMedia(devicesConfig);
const videoTrack = newVideoStream.getVideoTracks()[0];
if (!peerConnection) {
throw new Error('cannot selectCameraByDeviceId without a peer connection');
}
const sender = peerConnection.getSenders().find(s => {
return s.track?.kind === videoTrack.kind;
});
if (sender) {
await sender.replaceTrack(videoTrack);
videoTrack.enabled = true;
mediaDevices?.getVideoTracks().forEach(t => {
t.stop();
mediaDevices?.removeTrack(t);
});
mediaDevices?.addTrack(videoTrack);
sendVideoStatusViaDataChannel();
callVideoListeners();
window.log.info('replacing video track');
const videoSender = peerConnection
.getTransceivers()
.find(t => t.sender.track?.kind === 'video')?.sender;
videoTrack.enabled = true;
if (videoSender) {
await videoSender.replaceTrack(videoTrack);
} else {
throw new Error('Failed to get sender for selectCameraByDeviceId ');
throw new Error(
'We should always have a videoSender as we are using a black video when no camera are in use'
);
}
// do the same changes locally
localStream?.getVideoTracks().forEach(t => {
t.stop();
localStream?.removeTrack(t);
});
localStream?.addTrack(videoTrack);
sendVideoStatusViaDataChannel();
callVideoListeners();
} catch (e) {
window.log.warn('selectCameraByDeviceId failed with', e.message);
ToastUtils.pushToastError('selectCamera', e.message);
callVideoListeners();
}
}
@ -237,6 +293,12 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
if (sender?.track) {
sender.track.enabled = false;
}
// do the same changes locally
localStream?.getAudioTracks().forEach(t => {
t.stop();
localStream?.removeTrack(t);
});
localStream?.addTrack(getBlackSilenceMediaStream().getAudioTracks()[0]);
callVideoListeners();
return;
}
@ -251,16 +313,18 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
try {
const newAudioStream = await navigator.mediaDevices.getUserMedia(devicesConfig);
const audioTrack = newAudioStream.getAudioTracks()[0];
if (!peerConnection) {
throw new Error('cannot selectAudioInputByDeviceId without a peer connection');
}
const sender = peerConnection.getSenders().find(s => {
const audioSender = peerConnection.getSenders().find(s => {
return s.track?.kind === audioTrack.kind;
});
window.log.info('replacing audio track');
if (sender) {
await sender.replaceTrack(audioTrack);
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 ');
@ -287,18 +351,15 @@ export async function selectAudioOutputByDeviceId(audioOutputDeviceId: string) {
}
}
async function handleNegotiationNeededEvent(recipient: string) {
async function createOfferAndSendIt(recipient: string) {
try {
makingOffer = true;
window.log.info('got handleNegotiationNeeded event. creating offer');
const offer = await peerConnection?.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
window.log.info('got createOfferAndSendIt event. creating offer');
await (peerConnection as any)?.setLocalDescription();
const offer = peerConnection?.localDescription;
if (!offer) {
throw new Error('Could not create an offer');
}
await peerConnection?.setLocalDescription(offer);
if (!currentCallUUID) {
window.log.warn('cannot send offer without a currentCallUUID');
@ -313,18 +374,18 @@ async function handleNegotiationNeededEvent(recipient: string) {
uuid: currentCallUUID,
});
window.log.info('sending OFFER MESSAGE');
const negotationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably(
window.log.info(`sending '${offer.type}'' with callUUID: ${currentCallUUID}`);
const negotiationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably(
PubKey.cast(recipient),
offerMessage
);
if (typeof negotationOfferSendResult === 'number') {
if (typeof negotiationOfferSendResult === 'number') {
// window.log?.warn('setting last sent timestamp');
lastOutgoingOfferTimestamp = negotationOfferSendResult;
lastOutgoingOfferTimestamp = negotiationOfferSendResult;
}
}
} catch (err) {
window.log?.error(`Error on handling negotiation needed ${err}`);
window.log?.error(`Error createOfferAndSendIt ${err}`);
} finally {
makingOffer = false;
}
@ -340,51 +401,32 @@ function handleIceCandidates(event: RTCPeerConnectionIceEvent, pubkey: string) {
async function openMediaDevicesAndAddTracks() {
try {
await updateConnectedDevices();
if (!camerasList.length) {
ToastUtils.pushNoCameraFound();
return;
}
if (!audioInputsList.length) {
ToastUtils.pushNoAudioInputFound();
return;
}
selectedAudioInputId = audioInputsList[0].deviceId;
selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID; //audioInputsList[0].deviceId;
selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
window.log.info(
`openMediaDevices videoDevice:${selectedCameraId}:${camerasList[0].label} audioDevice:${selectedAudioInputId}`
`openMediaDevices videoDevice:${selectedCameraId} audioDevice:${selectedAudioInputId}`
);
const devicesConfig = {
audio: {
deviceId: selectedAudioInputId,
echoCancellation: true,
},
video: {
deviceId: selectedCameraId,
// width: VIDEO_WIDTH,
// height: Math.floor(VIDEO_WIDTH * VIDEO_RATIO),
},
};
mediaDevices = await navigator.mediaDevices.getUserMedia(devicesConfig);
mediaDevices.getTracks().map(track => {
if (track.kind === 'video') {
track.enabled = false;
}
if (mediaDevices) {
peerConnection?.addTrack(track, mediaDevices);
localStream = getBlackSilenceMediaStream();
localStream.getTracks().map(track => {
if (localStream) {
peerConnection?.addTrack(track, localStream);
}
});
} catch (err) {
window.log.warn('openMediaDevices: ', err);
ToastUtils.pushVideoCallPermissionNeeded();
closeVideoCall();
}
callVideoListeners();
}
// tslint:disable-next-line: function-name
export async function USER_callRecipient(recipient: string) {
if (!getCallMediaPermissionsSettings()) {
ToastUtils.pushVideoCallPermissionNeeded();
@ -412,12 +454,16 @@ export async function USER_callRecipient(recipient: string) {
});
window.log.info('Sending preOffer message to ', ed25519Str(recipient));
// 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);
const { wrappedEnvelope } = await MessageSender.send(rawMessage);
void PnServer.notifyPnServer(wrappedEnvelope, recipient);
await openMediaDevicesAndAddTracks();
setIsRinging(true);
await createOfferAndSendIt(recipient);
}
const iceCandidates: Array<RTCIceCandidate> = new Array();
@ -455,7 +501,9 @@ const iceSenderDebouncer = _.debounce(async (recipient: string) => {
uuid: currentCallUUID,
});
window.log.info('sending ICE CANDIDATES MESSAGE to ', recipient);
window.log.info(
`sending ICE CANDIDATES MESSAGE to ${ed25519Str(recipient)} about call ${currentCallUUID}`
);
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates);
}, 2000);
@ -490,6 +538,15 @@ function handleConnectionStateChanged(pubkey: string) {
closeVideoCall();
} else if (peerConnection?.connectionState === 'connected') {
setIsRinging(false);
const firstAudioInput = audioInputsList?.[0].deviceId || undefined;
if (firstAudioInput) {
void selectAudioInputByDeviceId(firstAudioInput);
}
const firstAudioOutput = audioOutputsList?.[0].deviceId || undefined;
if (firstAudioOutput) {
void selectAudioOutputByDeviceId(firstAudioOutput);
}
window.inboxStore?.dispatch(callConnected({ pubkey }));
}
}
@ -510,14 +567,16 @@ function closeVideoCall() {
dataChannel.close();
dataChannel = null;
}
if (mediaDevices) {
mediaDevices.getTracks().forEach(track => {
if (localStream) {
localStream.getTracks().forEach(track => {
track.stop();
localStream?.removeTrack(track);
});
}
if (remoteStream) {
remoteStream.getTracks().forEach(track => {
track.stop();
remoteStream?.removeTrack(track);
});
}
@ -526,13 +585,15 @@ function closeVideoCall() {
peerConnection = null;
}
mediaDevices = null;
localStream = null;
remoteStream = null;
selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID;
currentCallUUID = undefined;
window.inboxStore?.dispatch(setFullScreenCall(false));
window.inboxStore?.dispatch(endCall());
remoteVideoStreamIsMuted = true;
makingOffer = false;
@ -542,24 +603,26 @@ function closeVideoCall() {
callVideoListeners();
}
function getCallingStateOutsideOfRedux() {
const ongoingCallWith = window.inboxStore?.getState().call.ongoingWith as string | undefined;
const ongoingCallStatus = window.inboxStore?.getState().call.ongoingCallStatus as CallStatusEnum;
return { ongoingCallWith, ongoingCallStatus };
}
function onDataChannelReceivedMessage(ev: MessageEvent<string>) {
try {
const parsed = JSON.parse(ev.data);
if (parsed.hangup !== undefined) {
const foundEntry = getConversationController()
.getConversations()
.find(
(convo: ConversationModel) =>
convo.callState === 'connecting' ||
convo.callState === 'offering' ||
convo.callState === 'ongoing'
);
if (!foundEntry || !foundEntry.id) {
return;
const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux();
if (
(ongoingCallStatus === 'connecting' ||
ongoingCallStatus === 'offering' ||
ongoingCallStatus === 'ongoing') &&
ongoingCallWith
) {
void handleCallTypeEndCall(ongoingCallWith, currentCallUUID);
}
handleCallTypeEndCall(foundEntry.id, currentCallUUID);
return;
}
@ -578,7 +641,7 @@ function onDataChannelOnOpen() {
sendVideoStatusViaDataChannel();
}
function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) {
function createOrGetPeerConnection(withPubkey: string) {
if (peerConnection) {
return peerConnection;
}
@ -592,15 +655,7 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false)
dataChannel.onmessage = onDataChannelReceivedMessage;
dataChannel.onopen = onDataChannelOnOpen;
if (!isAcceptingCall) {
peerConnection.onnegotiationneeded = async () => {
await handleNegotiationNeededEvent(withPubkey);
};
}
peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
peerConnection.ontrack = event => {
event.track.onunmute = () => {
remoteStream?.addTrack(event.track);
@ -640,7 +695,6 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false)
return peerConnection;
}
// tslint:disable-next-line: function-name
export async function USER_acceptIncomingCallRequest(fromSender: string) {
window.log.info('USER_acceptIncomingCallRequest');
setIsRinging(false);
@ -674,7 +728,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
}
currentCallUUID = lastOfferMessage.uuid;
peerConnection = createOrGetPeerConnection(fromSender, true);
peerConnection = createOrGetPeerConnection(fromSender);
await openMediaDevicesAndAddTracks();
@ -711,46 +765,59 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
await buildAnswerAndSendIt(fromSender);
}
// tslint:disable-next-line: function-name
export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUID: string) {
setIsRinging(false);
window.log.info(`rejectCallAlreadyAnotherCall ${ed25519Str(fromSender)}: ${forcedUUID}`);
rejectedCallUUIDS.add(forcedUUID);
const rejectCallMessage = new CallMessage({
type: SignalService.CallMessage.Type.END_CALL,
timestamp: Date.now(),
uuid: forcedUUID,
});
await sendCallMessageAndSync(rejectCallMessage, fromSender);
// delete all msg not from that uuid only but from that sender pubkey
clearCallCacheFromPubkeyAndUUID(fromSender, forcedUUID);
}
export async function USER_rejectIncomingCallRequest(fromSender: string) {
setIsRinging(false);
// close the popup call
window.inboxStore?.dispatch(endCall());
const lastOfferMessage = findLastMessageTypeFromSender(
fromSender,
SignalService.CallMessage.Type.OFFER
);
const lastCallUUID = lastOfferMessage?.uuid;
window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${lastCallUUID}`);
if (lastCallUUID) {
const aboutCallUUID = lastOfferMessage?.uuid;
window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${aboutCallUUID}`);
if (aboutCallUUID) {
rejectedCallUUIDS.add(aboutCallUUID);
const endCallMessage = new CallMessage({
type: SignalService.CallMessage.Type.END_CALL,
timestamp: Date.now(),
uuid: lastCallUUID,
uuid: aboutCallUUID,
});
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage);
// sync the reject event so our other devices remove the popup too
await sendCallMessageAndSync(endCallMessage, fromSender);
// delete all msg not from that uuid only but from that sender pubkey
clearCallCacheFromPubkeyAndUUID(fromSender, lastCallUUID);
clearCallCacheFromPubkeyAndUUID(fromSender, aboutCallUUID);
}
const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux();
window.inboxStore?.dispatch(
endCall({
pubkey: fromSender,
})
);
const convos = getConversationController().getConversations();
const callingConvos = convos.filter(convo => convo.callState !== undefined);
if (callingConvos.length > 0) {
// we just got a new offer from someone we are already in a call with
if (callingConvos.length === 1 && callingConvos[0].id === fromSender) {
closeVideoCall();
}
// clear the ongoing call if needed
if (ongoingCallWith && ongoingCallStatus && ongoingCallWith === fromSender) {
closeVideoCall();
}
}
// tslint:disable-next-line: function-name
async function sendCallMessageAndSync(callmessage: CallMessage, user: string) {
await Promise.all([
getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(user), callmessage),
getMessageQueue().sendToPubKeyNonDurably(UserUtils.getOurPubKeyFromCache(), callmessage),
]);
}
export async function USER_hangup(fromSender: string) {
window.log.info('USER_hangup');
@ -758,6 +825,7 @@ export async function USER_hangup(fromSender: string) {
window.log.warn('should not be able to hangup without a currentCallUUID');
return;
} else {
rejectedCallUUIDS.add(currentCallUUID);
const endCallMessage = new CallMessage({
type: SignalService.CallMessage.Type.END_CALL,
timestamp: Date.now(),
@ -766,7 +834,7 @@ export async function USER_hangup(fromSender: string) {
void getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage);
}
window.inboxStore?.dispatch(endCall({ pubkey: fromSender }));
window.inboxStore?.dispatch(endCall());
window.log.info('sending hangup with an END_CALL MESSAGE');
sendHangupViaDataChannel();
@ -776,16 +844,36 @@ export async function USER_hangup(fromSender: string) {
closeVideoCall();
}
export function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) {
/**
* This can actually be called from either the datachannel or from the receiver END_CALL event
*/
export async function handleCallTypeEndCall(sender: string, aboutCallUUID?: string) {
window.log.info('handling callMessage END_CALL:', aboutCallUUID);
if (aboutCallUUID) {
rejectedCallUUIDS.add(aboutCallUUID);
clearCallCacheFromPubkeyAndUUID(sender, aboutCallUUID);
// this is a end call from ourself. We must remove the popup about the incoming call
// if it matches the owner of this callUUID
if (sender === UserUtils.getOurPubKeyStrFromCache()) {
const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux();
const ownerOfCall = getOwnerOfCallUUID(aboutCallUUID);
if (
(ongoingCallStatus === 'incoming' || ongoingCallStatus === 'connecting') &&
ongoingCallWith === ownerOfCall
) {
closeVideoCall();
window.inboxStore?.dispatch(endCall());
}
return;
}
if (aboutCallUUID === currentCallUUID) {
closeVideoCall();
window.inboxStore?.dispatch(endCall({ pubkey: sender }));
window.inboxStore?.dispatch(endCall());
}
}
}
@ -796,16 +884,12 @@ async function buildAnswerAndSendIt(sender: string) {
window.log.warn('cannot send answer without a currentCallUUID');
return;
}
const answer = await peerConnection.createAnswer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
await (peerConnection as any).setLocalDescription();
const answer = peerConnection.localDescription;
if (!answer?.sdp || answer.sdp.length === 0) {
window.log.warn('failed to create answer');
return;
}
await peerConnection.setLocalDescription(answer);
const answerSdp = answer.sdp;
const callAnswerMessage = new CallMessage({
timestamp: Date.now(),
@ -814,12 +898,15 @@ async function buildAnswerAndSendIt(sender: string) {
uuid: currentCallUUID,
});
window.log.info('sending ANSWER MESSAGE');
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), callAnswerMessage);
window.log.info('sending ANSWER MESSAGE and sync');
await sendCallMessageAndSync(callAnswerMessage, sender);
}
}
export function isCallRejected(uuid: string) {
return rejectedCallUUIDS.has(uuid);
}
export async function handleCallTypeOffer(
sender: string,
callMessage: SignalService.CallMessage,
@ -832,45 +919,53 @@ export async function handleCallTypeOffer(
}
window.log.info('handling callMessage OFFER with uuid: ', remoteCallUUID);
const convos = getConversationController().getConversations();
const callingConvos = convos.filter(convo => convo.callState !== undefined);
if (!getCallMediaPermissionsSettings()) {
await handleMissedCall(sender, incomingOfferTimestamp, true);
return;
}
if (callingConvos.length > 0) {
// we just got a new offer from someone we are NOT already in a call with
if (callingConvos.length !== 1 || callingConvos[0].id !== sender) {
await handleMissedCall(sender, incomingOfferTimestamp, false);
if (currentCallUUID && currentCallUUID !== remoteCallUUID) {
// we just got a new offer with a different callUUID. this is a missed call (from either the same sender or another one)
if (callCache.get(sender)?.has(currentCallUUID)) {
// this is a missed call from the same sender but with a different callID.
// another call from another device maybe? just reject it.
await rejectCallAlreadyAnotherCall(sender, remoteCallUUID);
return;
}
// add a message in the convo with this user about the missed call.
await handleMissedCall(sender, incomingOfferTimestamp, false);
// Here, we are in a call, and we got an offer from someone we are in a call with, and not one of his other devices.
// Just hangup automatically the call on the calling side.
await rejectCallAlreadyAnotherCall(sender, remoteCallUUID);
return;
}
const readyForOffer =
!makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending);
const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
const offerCollision = !readyForOffer;
ignoreOffer = !polite && offerCollision;
if (ignoreOffer) {
window.log?.warn('Received offer when unready for offer; Ignoring offer.');
return;
}
if (callingConvos.length === 1 && callingConvos[0].id === sender) {
if (peerConnection && remoteCallUUID === currentCallUUID && currentCallUUID) {
window.log.info('Got a new offer message from our ongoing call');
isSettingRemoteAnswerPending = false;
const remoteDesc = new RTCSessionDescription({
const remoteOfferDesc = new RTCSessionDescription({
type: 'offer',
sdp: callMessage.sdps[0],
});
isSettingRemoteAnswerPending = false;
if (peerConnection) {
await peerConnection.setRemoteDescription(remoteDesc); // SRD rolls back as needed
await buildAnswerAndSendIt(sender);
}
await peerConnection.setRemoteDescription(remoteOfferDesc); // SRD rolls back as needed
isSettingRemoteAnswerPending = false;
await buildAnswerAndSendIt(sender);
} else {
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
@ -927,32 +1022,94 @@ export async function handleMissedCall(
return;
}
function getOwnerOfCallUUID(callUUID: string) {
for (const deviceKey of callCache.keys()) {
for (const callUUIDEntry of callCache.get(deviceKey) as Map<
string,
Array<SignalService.CallMessage>
>) {
if (callUUIDEntry[0] === callUUID) {
return deviceKey;
}
}
}
return null;
}
export async function handleCallTypeAnswer(sender: string, callMessage: SignalService.CallMessage) {
if (!callMessage.sdps || callMessage.sdps.length === 0) {
window.log.warn('cannot handle answered message without signal description protols');
window.log.warn('cannot handle answered message without signal description proto sdps');
return;
}
const remoteCallUUID = callMessage.uuid;
if (!remoteCallUUID || remoteCallUUID.length === 0) {
const callMessageUUID = callMessage.uuid;
if (!callMessageUUID || callMessageUUID.length === 0) {
window.log.warn('handleCallTypeAnswer has no valid uuid');
return;
}
window.log.info('handling callMessage ANSWER');
// this is an answer we sent to ourself, this must be about another of our device accepting an incoming call.
// if we accepted that call already from the current device, currentCallUUID would be set
if (sender === UserUtils.getOurPubKeyStrFromCache()) {
// when we answer a call, we get this message on all our devices, including the one we just accepted the call with.
pushCallMessageToCallCache(sender, remoteCallUUID, callMessage);
const isDeviceWhichJustAcceptedCall = currentCallUUID === callMessageUUID;
if (isDeviceWhichJustAcceptedCall) {
window.log.info(
`isDeviceWhichJustAcceptedCall: skipping message back ANSWER from ourself about call ${callMessageUUID}`
);
return;
}
window.log.info(`handling callMessage ANSWER from ourself about call ${callMessageUUID}`);
const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux();
const foundOwnerOfCallUUID = getOwnerOfCallUUID(callMessageUUID);
if (callMessageUUID !== currentCallUUID) {
// this is an answer we sent from another of our devices
// automatically close that call
if (foundOwnerOfCallUUID) {
rejectedCallUUIDS.add(callMessageUUID);
// if this call is about the one being currently displayed, force close it
if (ongoingCallStatus && ongoingCallWith === foundOwnerOfCallUUID) {
closeVideoCall();
}
window.inboxStore?.dispatch(endCall());
}
}
return;
} else {
window.log.info(`handling callMessage ANSWER from ${callMessageUUID}`);
}
pushCallMessageToCallCache(sender, callMessageUUID, callMessage);
if (!peerConnection) {
window.log.info('handleCallTypeAnswer without peer connection. Dropping');
return;
}
window.inboxStore?.dispatch(answerCall({ pubkey: sender }));
const remoteDesc = new RTCSessionDescription({ type: 'answer', sdp: callMessage.sdps[0] });
window.inboxStore?.dispatch(
answerCall({
pubkey: sender,
})
);
// window.log?.info('Setting remote answer pending');
isSettingRemoteAnswerPending = true;
await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed
isSettingRemoteAnswerPending = false;
try {
isSettingRemoteAnswerPending = true;
const remoteDesc = new RTCSessionDescription({
type: 'answer',
sdp: callMessage.sdps[0],
});
await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed
} catch (e) {
window.log.warn('setRemoteDescriptio failed:', e);
} finally {
isSettingRemoteAnswerPending = false;
}
}
export async function handleCallTypeIceCandidates(

View File

@ -0,0 +1,24 @@
const maxWidth = 1920;
const maxHeight = 1080;
const silence = () => {
const ctx = new AudioContext();
const oscillator = ctx.createOscillator();
const dst = oscillator.connect(ctx.createMediaStreamDestination());
oscillator.start();
return Object.assign((dst as any).stream.getAudioTracks()[0], { enabled: false });
};
const black = () => {
const canvas = Object.assign(document.createElement('canvas'), {
width: maxWidth,
height: maxHeight,
});
canvas.getContext('2d')?.fillRect(0, 0, maxWidth, maxHeight);
const stream = (canvas as any).captureStream();
return Object.assign(stream.getVideoTracks()[0], { enabled: false });
};
export const getBlackSilenceMediaStream = () => {
return new MediaStream([black(), silence()]);
};

View File

@ -8,7 +8,7 @@ import * as UserUtils from './User';
import * as SyncUtils from './syncUtils';
import * as AttachmentsV2Utils from './AttachmentsV2';
import * as AttachmentDownloads from './AttachmentsDownload';
import * as CallManager from './CallManager';
import * as CallManager from './calling/CallManager';
export * from './Attachments';
export * from './TypedEmitter';

View File

@ -149,7 +149,7 @@ const lightColorTextSubtle = `${black}99`;
const lightColorTextAccent = '#00c769';
const lightColorSessionShadow = `0 0 4px 0 ${black}5E`;
const lightColorComposeViewBg = '#efefef';
const lightColorSentMessageBg = accentLightTheme;
const lightColorSentMessageBg = 'hsl(152, 100%, 40%)';
const lightColorSentMessageText = white;
const lightColorClickableHovered = '#dfdfdf';
const lightColorSessionBorderColor = borderLightThemeColor;

112
ts/state/ducks/call.tsx Normal file
View File

@ -0,0 +1,112 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export type CallStatusEnum = 'offering' | 'incoming' | 'connecting' | 'ongoing' | undefined;
export type CallStateType = {
ongoingWith?: string;
ongoingCallStatus?: CallStatusEnum;
callIsInFullScreen: boolean;
};
export const initialCallState: CallStateType = {
ongoingWith: undefined,
ongoingCallStatus: undefined,
callIsInFullScreen: false,
};
/**
* This slice is the one holding the default joinable rooms fetched once in a while from the default opengroup v2 server.
*/
const callSlice = createSlice({
name: 'call',
initialState: initialCallState,
reducers: {
incomingCall(state: CallStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
if (state.ongoingWith && state.ongoingWith !== callerPubkey) {
window.log.warn(
`Got an incoming call action for ${callerPubkey} but we are already in a call.`
);
return state;
}
state.ongoingWith = callerPubkey;
state.ongoingCallStatus = 'incoming';
return state;
},
endCall(state: CallStateType) {
state.ongoingCallStatus = undefined;
state.ongoingWith = undefined;
return state;
},
answerCall(state: CallStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
// to answer a call we need an incoming call form that specific pubkey
if (state.ongoingWith !== callerPubkey || state.ongoingCallStatus !== 'incoming') {
window.log.info('cannot answer a call we are not displaying a dialog with');
return state;
}
state.ongoingCallStatus = 'connecting';
state.callIsInFullScreen = false;
return state;
},
callConnected(state: CallStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
if (callerPubkey !== state.ongoingWith) {
window.log.info('cannot answer a call we did not start or receive first');
return state;
}
const existingCallState = state.ongoingCallStatus;
if (existingCallState !== 'connecting' && existingCallState !== 'offering') {
window.log.info(
'cannot answer a call we are not connecting (and so answered) to or offering a call'
);
return state;
}
state.ongoingCallStatus = 'ongoing';
state.callIsInFullScreen = false;
return state;
},
startingCallWith(state: CallStateType, action: PayloadAction<{ pubkey: string }>) {
if (state.ongoingWith) {
window.log.warn('cannot start a call with an ongoing call already: ongoingWith');
return state;
}
if (state.ongoingCallStatus) {
window.log.warn('cannot start a call with an ongoing call already: ongoingCallStatus');
return state;
}
const callerPubkey = action.payload.pubkey;
state.ongoingWith = callerPubkey;
state.ongoingCallStatus = 'offering';
state.callIsInFullScreen = false;
return state;
},
setFullScreenCall(state: CallStateType, action: PayloadAction<boolean>) {
// only set in full screen if we have an ongoing call
if (state.ongoingWith && state.ongoingCallStatus === 'ongoing' && action.payload) {
state.callIsInFullScreen = true;
return state;
}
state.callIsInFullScreen = false;
return state;
},
},
});
const { actions, reducer } = callSlice;
export const {
incomingCall,
endCall,
answerCall,
callConnected,
startingCallWith,
setFullScreenCall,
} = actions;
export const callReducer = reducer;

View File

@ -3,7 +3,6 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { getConversationController } from '../../session/conversations';
import { getFirstUnreadMessageIdInConversation, getMessagesByConversation } from '../../data/data';
import {
CallState,
ConversationNotificationSettingType,
ConversationTypeEnum,
} from '../../models/conversation';
@ -254,7 +253,6 @@ export interface ReduxConversationType {
isPinned?: boolean;
isApproved?: boolean;
callState?: CallState;
}
export interface NotificationForConvoOption {
@ -278,7 +276,6 @@ export type ConversationsStateType = {
quotedMessage?: ReplyingToMessageProps;
areMoreMessagesBeingFetched: boolean;
haveDoneFirstScroll: boolean;
callIsInFullScreen: boolean;
showScrollButton: boolean;
animateQuotedMessageId?: string;
@ -373,7 +370,6 @@ export function getEmptyConversationState(): ConversationsStateType {
mentionMembers: [],
firstUnreadMessageId: undefined,
haveDoneFirstScroll: false,
callIsInFullScreen: false,
};
}
@ -699,7 +695,6 @@ const conversationsSlice = createSlice({
return {
conversationLookup: state.conversationLookup,
callIsInFullScreen: state.callIsInFullScreen,
selectedConversation: action.payload.id,
areMoreMessagesBeingFetched: false,
@ -763,102 +758,6 @@ const conversationsSlice = createSlice({
state.mentionMembers = action.payload;
return state;
},
incomingCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
const existingCallState = state.conversationLookup[callerPubkey].callState;
if (existingCallState !== undefined) {
return state;
}
const foundConvo = getConversationController().get(callerPubkey);
if (!foundConvo) {
return state;
}
// we have to update the model itself.
// not the db (as we dont want to store that field in it)
// and not the redux store directly as it gets overriden by the commit() of the conversationModel
foundConvo.callState = 'incoming';
void foundConvo.commit();
return state;
},
endCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
const existingCallState = state.conversationLookup[callerPubkey].callState;
if (!existingCallState) {
return state;
}
const foundConvo = getConversationController().get(callerPubkey);
if (!foundConvo) {
return state;
}
// we have to update the model itself.
// not the db (as we dont want to store that field in it)
// and not the redux store directly as it gets overriden by the commit() of the conversationModel
foundConvo.callState = undefined;
void foundConvo.commit();
return state;
},
answerCall(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
const existingCallState = state.conversationLookup[callerPubkey].callState;
if (!existingCallState || existingCallState !== 'incoming') {
return state;
}
const foundConvo = getConversationController().get(callerPubkey);
if (!foundConvo) {
return state;
}
// we have to update the model itself.
// not the db (as we dont want to store that field in it)
// and not the redux store directly as it gets overriden by the commit() of the conversationModel
foundConvo.callState = 'connecting';
void foundConvo.commit();
return state;
},
callConnected(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
const existingCallState = state.conversationLookup[callerPubkey].callState;
if (!existingCallState || existingCallState === 'ongoing') {
return state;
}
const foundConvo = getConversationController().get(callerPubkey);
if (!foundConvo) {
return state;
}
// we have to update the model itself.
// not the db (as we dont want to store that field in it)
// and not the redux store directly as it gets overriden by the commit() of the conversationModel
foundConvo.callState = 'ongoing';
void foundConvo.commit();
return state;
},
startingCallWith(state: ConversationsStateType, action: PayloadAction<{ pubkey: string }>) {
const callerPubkey = action.payload.pubkey;
const existingCallState = state.conversationLookup[callerPubkey].callState;
if (existingCallState) {
return state;
}
const foundConvo = getConversationController().get(callerPubkey);
if (!foundConvo) {
return state;
}
// we have to update the model itself.
// not the db (as we dont want to store that field in it)
// and not the redux store directly as it gets overriden by the commit() of the conversationModel
foundConvo.callState = 'offering';
void foundConvo.commit();
return state;
},
setFullScreenCall(state: ConversationsStateType, action: PayloadAction<boolean>) {
state.callIsInFullScreen = action.payload;
return state;
},
},
extraReducers: (builder: any) => {
// Add reducers for additional action types here, and handle loading state as needed
@ -918,13 +817,6 @@ export const {
quotedMessageToAnimate,
setNextMessageToPlayId,
updateMentionsMembers,
// calls
incomingCall,
endCall,
answerCall,
callConnected,
startingCallWith,
setFullScreenCall,
} = actions;
export async function openConversationWithMessages(args: {

View File

@ -6,6 +6,7 @@ import { reducer as user, UserStateType } from './ducks/user';
import { reducer as theme, ThemeStateType } from './ducks/theme';
import { reducer as section, SectionStateType } from './ducks/section';
import { defaultRoomReducer as defaultRooms, DefaultRoomsState } from './ducks/defaultRooms';
import { callReducer as call, CallStateType } from './ducks/call';
import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion';
import { modalReducer as modals, ModalState } from './ducks/modalDialog';
@ -28,6 +29,7 @@ export type StateType = {
userConfig: UserConfigState;
timerOptions: TimerOptionsState;
stagedAttachments: StagedAttachmentsStateType;
call: CallStateType;
};
export const reducers = {
@ -42,6 +44,7 @@ export const reducers = {
userConfig,
timerOptions,
stagedAttachments,
call,
};
// Making this work would require that our reducer signature supported AnyAction, not

103
ts/state/selectors/call.ts Normal file
View File

@ -0,0 +1,103 @@
import { createSelector } from 'reselect';
import { CallStateType } from '../ducks/call';
import { ConversationsStateType, ReduxConversationType } from '../ducks/conversations';
import { StateType } from '../reducer';
import { getConversations, getSelectedConversationKey } from './conversations';
export const getCallState = (state: StateType): CallStateType => state.call;
// --- INCOMING CALLS
export const getHasIncomingCallFrom = createSelector(getCallState, (state: CallStateType):
| string
| undefined => {
return state.ongoingWith && state.ongoingCallStatus === 'incoming'
? state.ongoingWith
: undefined;
});
export const getHasIncomingCall = createSelector(
getHasIncomingCallFrom,
(withConvo: string | undefined): boolean => !!withConvo
);
// --- ONGOING CALLS
export const getHasOngoingCallWith = createSelector(
getConversations,
getCallState,
(convos: ConversationsStateType, callState: CallStateType): ReduxConversationType | undefined => {
if (
callState.ongoingWith &&
(callState.ongoingCallStatus === 'connecting' ||
callState.ongoingCallStatus === 'offering' ||
callState.ongoingCallStatus === 'ongoing')
) {
return convos.conversationLookup[callState.ongoingWith] || undefined;
}
return undefined;
}
);
export const getHasOngoingCall = createSelector(
getHasOngoingCallWith,
(withConvo: ReduxConversationType | undefined): boolean => !!withConvo
);
export const getHasOngoingCallWithPubkey = createSelector(
getHasOngoingCallWith,
(withConvo: ReduxConversationType | undefined): string | undefined => withConvo?.id
);
export const getHasOngoingCallWithFocusedConvo = createSelector(
getHasOngoingCallWithPubkey,
getSelectedConversationKey,
(withPubkey, selectedPubkey) => {
return withPubkey && withPubkey === selectedPubkey;
}
);
export const getHasOngoingCallWithFocusedConvoIsOffering = createSelector(
getCallState,
getSelectedConversationKey,
(callState: CallStateType, selectedConvoPubkey?: string): boolean => {
if (
!selectedConvoPubkey ||
!callState.ongoingWith ||
callState.ongoingCallStatus !== 'offering' ||
selectedConvoPubkey !== callState.ongoingWith
) {
return false;
}
return true;
}
);
export const getHasOngoingCallWithFocusedConvosIsConnecting = createSelector(
getCallState,
getSelectedConversationKey,
(callState: CallStateType, selectedConvoPubkey?: string): boolean => {
if (
!selectedConvoPubkey ||
!callState.ongoingWith ||
callState.ongoingCallStatus !== 'connecting' ||
selectedConvoPubkey !== callState.ongoingWith
) {
return false;
}
return true;
}
);
export const getHasOngoingCallWithNonFocusedConvo = createSelector(
getHasOngoingCallWithPubkey,
getSelectedConversationKey,
(withPubkey, selectedPubkey) => {
return withPubkey && withPubkey !== selectedPubkey;
}
);
export const getCallIsInFullScreen = createSelector(
getCallState,
(callState): boolean => callState.callIsInFullScreen
);

View File

@ -96,99 +96,6 @@ export const getConversationById = createSelector(
}
);
export const getHasIncomingCallFrom = createSelector(
getConversations,
(state: ConversationsStateType): string | undefined => {
const foundEntry = Object.entries(state.conversationLookup).find(
([_convoKey, convo]) => convo.callState === 'incoming'
);
if (!foundEntry) {
return undefined;
}
return foundEntry[1].id;
}
);
export const getHasOngoingCallWith = createSelector(
getConversations,
(state: ConversationsStateType): ReduxConversationType | undefined => {
const foundEntry = Object.entries(state.conversationLookup).find(
([_convoKey, convo]) =>
convo.callState === 'connecting' ||
convo.callState === 'offering' ||
convo.callState === 'ongoing'
);
if (!foundEntry) {
return undefined;
}
return foundEntry[1];
}
);
export const getHasIncomingCall = createSelector(
getHasIncomingCallFrom,
(withConvo: string | undefined): boolean => !!withConvo
);
export const getHasOngoingCall = createSelector(
getHasOngoingCallWith,
(withConvo: ReduxConversationType | undefined): boolean => !!withConvo
);
export const getHasOngoingCallWithPubkey = createSelector(
getHasOngoingCallWith,
(withConvo: ReduxConversationType | undefined): string | undefined => withConvo?.id
);
export const getHasOngoingCallWithFocusedConvo = createSelector(
getHasOngoingCallWithPubkey,
getSelectedConversationKey,
(withPubkey, selectedPubkey) => {
return withPubkey && withPubkey === selectedPubkey;
}
);
export const getHasOngoingCallWithFocusedConvoIsOffering = createSelector(
getConversations,
getSelectedConversationKey,
(state: ConversationsStateType, selectedConvoPubkey?: string): boolean => {
if (!selectedConvoPubkey) {
return false;
}
const isOffering = state.conversationLookup[selectedConvoPubkey]?.callState === 'offering';
return Boolean(isOffering);
}
);
export const getHasOngoingCallWithFocusedConvosIsConnecting = createSelector(
getConversations,
getSelectedConversationKey,
(state: ConversationsStateType, selectedConvoPubkey?: string): boolean => {
if (!selectedConvoPubkey) {
return false;
}
const isOffering = state.conversationLookup[selectedConvoPubkey]?.callState === 'connecting';
return Boolean(isOffering);
}
);
export const getHasOngoingCallWithNonFocusedConvo = createSelector(
getHasOngoingCallWithPubkey,
getSelectedConversationKey,
(withPubkey, selectedPubkey) => {
return withPubkey && withPubkey !== selectedPubkey;
}
);
export const getCallIsInFullScreen = createSelector(
getConversations,
(state: ConversationsStateType): boolean => state.callIsInFullScreen
);
export const getIsTypingEnabled = createSelector(
getConversations,
getSelectedConversationKey,

View File

@ -4,7 +4,6 @@ import { SessionConversation } from '../../components/session/conversation/Sessi
import { StateType } from '../reducer';
import { getTheme } from '../selectors/theme';
import {
getHasOngoingCallWithFocusedConvo,
getLightBoxOptions,
getSelectedConversation,
getSelectedConversationKey,
@ -15,6 +14,7 @@ import {
} from '../selectors/conversations';
import { getOurNumber } from '../selectors/user';
import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments';
import { getHasOngoingCallWithFocusedConvo } from '../selectors/call';
const mapStateToProps = (state: StateType) => {
return {