Merge branch 'clearnet' into message-requests
This commit is contained in:
commit
4576f3cb32
16
app/sql.js
16
app/sql.js
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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' });
|
||||
}}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -133,7 +133,7 @@ const HeaderItem = () => {
|
|||
<div className="group-settings-header">
|
||||
<SessionIconButton
|
||||
iconType="chevron"
|
||||
iconSize={'medium'}
|
||||
iconSize="medium"
|
||||
iconRotation={270}
|
||||
onClick={() => {
|
||||
dispatch(closeRightPanel());
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
|
@ -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()]);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue