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,
|
getUnreadByConversation,
|
||||||
getUnreadCountByConversation,
|
getUnreadCountByConversation,
|
||||||
getMessageBySender,
|
getMessageBySender,
|
||||||
getMessageBySenderAndServerId,
|
|
||||||
getMessageBySenderAndServerTimestamp,
|
getMessageBySenderAndServerTimestamp,
|
||||||
getMessageBySenderAndTimestamp,
|
getMessageBySenderAndTimestamp,
|
||||||
getMessageIdsFromServerIds,
|
getMessageIdsFromServerIds,
|
||||||
|
@ -2058,21 +2057,6 @@ function getMessageBySender({ source, sourceDevice, sentAt }) {
|
||||||
return map(rows, row => jsonToObject(row.json));
|
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 }) {
|
function getMessageBySenderAndTimestamp({ source, timestamp }) {
|
||||||
const rows = globalInstance
|
const rows = globalInstance
|
||||||
.prepare(
|
.prepare(
|
||||||
|
|
|
@ -26,13 +26,6 @@ message WebSocketRequestMessage {
|
||||||
optional uint64 id = 4;
|
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 {
|
message WebSocketMessage {
|
||||||
enum Type {
|
enum Type {
|
||||||
|
@ -43,5 +36,4 @@ message WebSocketMessage {
|
||||||
|
|
||||||
optional Type type = 1;
|
optional Type type = 1;
|
||||||
optional WebSocketRequestMessage request = 2;
|
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
|
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
|
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
|
||||||
onAvatarClick?: () => void;
|
onAvatarClick?: () => void;
|
||||||
|
dataTestId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Identicon = (props: Props) => {
|
const Identicon = (props: Props) => {
|
||||||
|
@ -92,7 +93,7 @@ const AvatarImage = (props: {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvatarInner = (props: 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);
|
const [imageBroken, setImageBroken] = useState(false);
|
||||||
// contentType is not important
|
// contentType is not important
|
||||||
const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', '');
|
const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', '');
|
||||||
|
@ -122,6 +123,7 @@ const AvatarInner = (props: Props) => {
|
||||||
props.onAvatarClick?.();
|
props.onAvatarClick?.();
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
|
data-testid={dataTestId}
|
||||||
>
|
>
|
||||||
{hasImage ? (
|
{hasImage ? (
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
|
|
|
@ -14,8 +14,6 @@ import {
|
||||||
getConversationHeaderProps,
|
getConversationHeaderProps,
|
||||||
getConversationHeaderTitleProps,
|
getConversationHeaderTitleProps,
|
||||||
getCurrentNotificationSettingText,
|
getCurrentNotificationSettingText,
|
||||||
getHasIncomingCall,
|
|
||||||
getHasOngoingCall,
|
|
||||||
getIsSelectedNoteToSelf,
|
getIsSelectedNoteToSelf,
|
||||||
getIsSelectedPrivate,
|
getIsSelectedPrivate,
|
||||||
getSelectedConversation,
|
getSelectedConversation,
|
||||||
|
@ -40,6 +38,7 @@ import {
|
||||||
resetSelectedMessageIds,
|
resetSelectedMessageIds,
|
||||||
} from '../../state/ducks/conversations';
|
} from '../../state/ducks/conversations';
|
||||||
import { callRecipient } from '../../interactions/conversationInteractions';
|
import { callRecipient } from '../../interactions/conversationInteractions';
|
||||||
|
import { getHasIncomingCall, getHasOngoingCall } from '../../state/selectors/call';
|
||||||
|
|
||||||
export interface TimerOption {
|
export interface TimerOption {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -107,7 +106,7 @@ const SelectionOverlay = () => {
|
||||||
return (
|
return (
|
||||||
<div className="message-selection-overlay">
|
<div className="message-selection-overlay">
|
||||||
<div className="close-button">
|
<div className="close-button">
|
||||||
<SessionIconButton iconType="exit" iconSize={'medium'} onClick={onCloseOverlay} />
|
<SessionIconButton iconType="exit" iconSize="medium" onClick={onCloseOverlay} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="button-group">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -45,7 +45,7 @@ const Sections = (props: Props & { selectedTab: TabType }) => {
|
||||||
const label =
|
const label =
|
||||||
type === 'media' ? window.i18n('mediaEmptyState') : window.i18n('documentsEmptyState');
|
type === 'media' ? window.i18n('mediaEmptyState') : window.i18n('documentsEmptyState');
|
||||||
|
|
||||||
return <EmptyState data-test="EmptyState" label={label} />;
|
return <EmptyState data-testid="EmptyState" label={label} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -82,7 +82,7 @@ export class EditProfileDialog extends React.Component<{}, State> {
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="edit-profile-dialog">
|
<div className="edit-profile-dialog" data-testid="edit-profile-dialog">
|
||||||
<SessionWrapperModal
|
<SessionWrapperModal
|
||||||
title={i18n('editProfileModalTitle')}
|
title={i18n('editProfileModalTitle')}
|
||||||
onClose={this.closeDialog}
|
onClose={this.closeDialog}
|
||||||
|
@ -97,7 +97,10 @@ export class EditProfileDialog extends React.Component<{}, State> {
|
||||||
|
|
||||||
<div className="session-id-section">
|
<div className="session-id-section">
|
||||||
<PillDivider text={window.i18n('yourSessionID')} />
|
<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}
|
{sessionID}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -182,10 +185,10 @@ export class EditProfileDialog extends React.Component<{}, State> {
|
||||||
{this.renderProfileHeader()}
|
{this.renderProfileHeader()}
|
||||||
|
|
||||||
<div className="profile-name-uneditable">
|
<div className="profile-name-uneditable">
|
||||||
<p>{name}</p>
|
<p data-testid="your-profile-name">{name}</p>
|
||||||
<SessionIconButton
|
<SessionIconButton
|
||||||
iconType="pencil"
|
iconType="pencil"
|
||||||
iconSize={'medium'}
|
iconSize="medium"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.setState({ mode: 'edit' });
|
this.setState({ mode: 'edit' });
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -150,8 +150,9 @@ export const ModalStatusLight = (props: StatusLightType) => {
|
||||||
export const ActionPanelOnionStatusLight = (props: {
|
export const ActionPanelOnionStatusLight = (props: {
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
handleClick: () => void;
|
handleClick: () => void;
|
||||||
|
dataTestId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { isSelected, handleClick } = props;
|
const { isSelected, handleClick, dataTestId } = props;
|
||||||
|
|
||||||
const onionPathsCount = useSelector(getOnionPathsCount);
|
const onionPathsCount = useSelector(getOnionPathsCount);
|
||||||
const firstPathLength = useSelector(getFirstOnionPathLength);
|
const firstPathLength = useSelector(getFirstOnionPathLength);
|
||||||
|
@ -179,6 +180,7 @@ export const ActionPanelOnionStatusLight = (props: {
|
||||||
glowStartDelay={0}
|
glowStartDelay={0}
|
||||||
noScale={true}
|
noScale={true}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
|
dataTestId={dataTestId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { SessionIconButton, SessionIconType } from './icon';
|
import { SessionIconButton } from './icon';
|
||||||
import { Avatar, AvatarSize } from '../Avatar';
|
import { Avatar, AvatarSize } from '../Avatar';
|
||||||
import { SessionToastContainer } from './SessionToastContainer';
|
import { SessionToastContainer } from './SessionToastContainer';
|
||||||
import { getConversationController } from '../../session/conversations';
|
import { getConversationController } from '../../session/conversations';
|
||||||
|
@ -96,47 +96,71 @@ const Section = (props: { type: SectionType; avatarPath?: string | null }) => {
|
||||||
onAvatarClick={handleClick}
|
onAvatarClick={handleClick}
|
||||||
name={userName}
|
name={userName}
|
||||||
pubkey={ourNumber}
|
pubkey={ourNumber}
|
||||||
|
dataTestId="leftpane-primary-avatar"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unreadToShow = type === SectionType.Message ? unreadMessageCount : undefined;
|
const unreadToShow = type === SectionType.Message ? unreadMessageCount : undefined;
|
||||||
|
|
||||||
let iconType: SessionIconType;
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SectionType.Message:
|
case SectionType.Message:
|
||||||
iconType = 'chatBubble';
|
return (
|
||||||
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} />
|
|
||||||
) : (
|
|
||||||
<SessionIconButton
|
<SessionIconButton
|
||||||
iconSize={'medium'}
|
iconSize="medium"
|
||||||
iconType={iconType}
|
dataTestId="message-section"
|
||||||
iconColor={iconColor}
|
iconType={'chatBubble'}
|
||||||
|
iconColor={undefined}
|
||||||
notificationCount={unreadToShow}
|
notificationCount={unreadToShow}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
isSelected={isSelected}
|
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;
|
const cleanUpMediasInterval = DURATION.MINUTES * 30;
|
||||||
|
@ -300,7 +324,10 @@ export const ActionsPanel = () => {
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
|
|
||||||
<CallContainer />
|
<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.Profile} avatarPath={ourPrimaryConversation.avatarPath} />
|
||||||
<Section type={SectionType.Message} />
|
<Section type={SectionType.Message} />
|
||||||
<Section type={SectionType.Contact} />
|
<Section type={SectionType.Contact} />
|
||||||
|
|
|
@ -58,7 +58,7 @@ const LeftPaneSettingsCategoryRow = (props: { item: any }) => {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{item.id === focusedSettingsSection && (
|
{item.id === focusedSettingsSection && (
|
||||||
<SessionIcon iconSize={'medium'} iconType="chevron" iconRotation={270} />
|
<SessionIcon iconSize="medium" iconType="chevron" iconRotation={270} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { PersistGate } from 'redux-persist/integration/react';
|
||||||
import { persistStore } from 'redux-persist';
|
import { persistStore } from 'redux-persist';
|
||||||
import { TimerOptionsArray } from '../../state/ducks/timerOptions';
|
import { TimerOptionsArray } from '../../state/ducks/timerOptions';
|
||||||
import { getEmptyStagedAttachmentsState } from '../../state/ducks/stagedAttachments';
|
import { getEmptyStagedAttachmentsState } from '../../state/ducks/stagedAttachments';
|
||||||
|
import { initialCallState } from '../../state/ducks/call';
|
||||||
|
|
||||||
// Workaround: A react component's required properties are filtering up through connect()
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
|
@ -105,6 +106,7 @@ export class SessionInboxView extends React.Component<any, State> {
|
||||||
timerOptions,
|
timerOptions,
|
||||||
},
|
},
|
||||||
stagedAttachments: getEmptyStagedAttachmentsState(),
|
stagedAttachments: getEmptyStagedAttachmentsState(),
|
||||||
|
call: initialCallState,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.store = createStore(initialState);
|
this.store = createStore(initialState);
|
||||||
|
|
|
@ -108,7 +108,7 @@ export class SessionInput extends React.PureComponent<Props, State> {
|
||||||
return (
|
return (
|
||||||
<SessionIconButton
|
<SessionIconButton
|
||||||
iconType="eye"
|
iconType="eye"
|
||||||
iconSize={'medium'}
|
iconSize="medium"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
forceShow: !this.state.forceShow,
|
forceShow: !this.state.forceShow,
|
||||||
|
|
|
@ -72,7 +72,7 @@ export const SessionMemberListItem = (props: Props) => {
|
||||||
<span className="session-member-item__name">{name}</span>
|
<span className="session-member-item__name">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className={classNames('session-member-item__checkmark', isSelected && 'selected')}>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const SessionSearchInput = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="session-search-input">
|
<div className="session-search-input">
|
||||||
<SessionIconButton iconSize={'medium'} iconType="search" />
|
<SessionIconButton iconSize="medium" iconType="search" />
|
||||||
<input
|
<input
|
||||||
value={searchString}
|
value={searchString}
|
||||||
onChange={e => onChange(e.target.value)}
|
onChange={e => onChange(e.target.value)}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { SessionIconButton } from '../icon';
|
import { SessionIconButton } from '../icon';
|
||||||
import { animation, contextMenu, Item, Menu } from 'react-contexify';
|
import { animation, contextMenu, Item, Menu } from 'react-contexify';
|
||||||
import { InputItem } from '../../../session/utils/CallManager';
|
import { InputItem } from '../../../session/utils/calling/CallManager';
|
||||||
import { setFullScreenCall } from '../../../state/ducks/conversations';
|
import { setFullScreenCall } from '../../../state/ducks/call';
|
||||||
import { CallManager, ToastUtils } from '../../../session/utils';
|
import { CallManager, ToastUtils } from '../../../session/utils';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { getHasOngoingCallWithPubkey } from '../../../state/selectors/conversations';
|
import { getHasOngoingCallWithPubkey } from '../../../state/selectors/call';
|
||||||
import { DropDownAndToggleButton } from '../icon/DropDownAndToggleButton';
|
import { DropDownAndToggleButton } from '../icon/DropDownAndToggleButton';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
@ -71,16 +71,16 @@ export const AudioInputButton = ({
|
||||||
|
|
||||||
export const AudioOutputButton = ({
|
export const AudioOutputButton = ({
|
||||||
currentConnectedAudioOutputs,
|
currentConnectedAudioOutputs,
|
||||||
}: // isAudioOutputMuted,
|
isAudioOutputMuted,
|
||||||
// hideArrowIcon = false,
|
hideArrowIcon = false,
|
||||||
{
|
}: {
|
||||||
currentConnectedAudioOutputs: Array<InputItem>;
|
currentConnectedAudioOutputs: Array<InputItem>;
|
||||||
isAudioOutputMuted: boolean;
|
isAudioOutputMuted: boolean;
|
||||||
hideArrowIcon?: boolean;
|
hideArrowIcon?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <DropDownAndToggleButton
|
<DropDownAndToggleButton
|
||||||
iconType="volume"
|
iconType="volume"
|
||||||
isMuted={isAudioOutputMuted}
|
isMuted={isAudioOutputMuted}
|
||||||
onMainButtonClick={() => {
|
onMainButtonClick={() => {
|
||||||
|
@ -90,7 +90,7 @@ export const AudioOutputButton = ({
|
||||||
showAudioOutputMenu(currentConnectedAudioOutputs, e);
|
showAudioOutputMenu(currentConnectedAudioOutputs, e);
|
||||||
}}
|
}}
|
||||||
hidePopoverArrow={hideArrowIcon}
|
hidePopoverArrow={hideArrowIcon}
|
||||||
/> */}
|
/>
|
||||||
|
|
||||||
<AudioOutputMenu
|
<AudioOutputMenu
|
||||||
triggerId={audioOutputTriggerId}
|
triggerId={audioOutputTriggerId}
|
||||||
|
@ -238,19 +238,19 @@ const showAudioInputMenu = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// const showAudioOutputMenu = (
|
const showAudioOutputMenu = (
|
||||||
// currentConnectedAudioOutputs: Array<any>,
|
currentConnectedAudioOutputs: Array<any>,
|
||||||
// e: React.MouseEvent<HTMLDivElement>
|
e: React.MouseEvent<HTMLDivElement>
|
||||||
// ) => {
|
) => {
|
||||||
// if (currentConnectedAudioOutputs.length === 0) {
|
if (currentConnectedAudioOutputs.length === 0) {
|
||||||
// ToastUtils.pushNoAudioOutputFound();
|
ToastUtils.pushNoAudioOutputFound();
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
// contextMenu.show({
|
contextMenu.show({
|
||||||
// id: audioOutputTriggerId,
|
id: audioOutputTriggerId,
|
||||||
// event: e,
|
event: e,
|
||||||
// });
|
});
|
||||||
// };
|
};
|
||||||
|
|
||||||
const showVideoInputMenu = (
|
const showVideoInputMenu = (
|
||||||
currentConnectedCameras: Array<InputItem>,
|
currentConnectedCameras: Array<InputItem>,
|
||||||
|
@ -300,22 +300,22 @@ const handleMicrophoneToggle = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleSpeakerToggle = async (
|
const handleSpeakerToggle = async (
|
||||||
// currentConnectedAudioOutputs: Array<InputItem>,
|
currentConnectedAudioOutputs: Array<InputItem>,
|
||||||
// isAudioOutputMuted: boolean
|
isAudioOutputMuted: boolean
|
||||||
// ) => {
|
) => {
|
||||||
// if (!currentConnectedAudioOutputs.length) {
|
if (!currentConnectedAudioOutputs.length) {
|
||||||
// ToastUtils.pushNoAudioInputFound();
|
ToastUtils.pushNoAudioInputFound();
|
||||||
|
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
// if (isAudioOutputMuted) {
|
if (isAudioOutputMuted) {
|
||||||
// // selects the first one
|
// selects the first one
|
||||||
// await CallManager.selectAudioOutputByDeviceId(currentConnectedAudioOutputs[0].deviceId);
|
await CallManager.selectAudioOutputByDeviceId(currentConnectedAudioOutputs[0].deviceId);
|
||||||
// } else {
|
} else {
|
||||||
// await CallManager.selectAudioOutputByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID);
|
await CallManager.selectAudioOutputByDeviceId(CallManager.DEVICE_DISABLED_DEVICE_ID);
|
||||||
// }
|
}
|
||||||
// };
|
};
|
||||||
|
|
||||||
const StyledCallWindowControls = styled.div`
|
const StyledCallWindowControls = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -4,11 +4,11 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||||
import useKey from 'react-use/lib/useKey';
|
import useKey from 'react-use/lib/useKey';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener';
|
import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener';
|
||||||
import { setFullScreenCall } from '../../../state/ducks/conversations';
|
import { setFullScreenCall } from '../../../state/ducks/call';
|
||||||
import {
|
import {
|
||||||
getCallIsInFullScreen,
|
getCallIsInFullScreen,
|
||||||
getHasOngoingCallWithFocusedConvo,
|
getHasOngoingCallWithFocusedConvo,
|
||||||
} from '../../../state/selectors/conversations';
|
} from '../../../state/selectors/call';
|
||||||
import { CallWindowControls } from './CallButtons';
|
import { CallWindowControls } from './CallButtons';
|
||||||
import { StyledVideoElement } from './DraggableCallContainer';
|
import { StyledVideoElement } from './DraggableCallContainer';
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,8 @@ import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
|
||||||
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
import {
|
import { getSelectedConversationKey } from '../../../state/selectors/conversations';
|
||||||
getHasOngoingCall,
|
import { getHasOngoingCall, getHasOngoingCallWith } from '../../../state/selectors/call';
|
||||||
getHasOngoingCallWith,
|
|
||||||
getSelectedConversationKey,
|
|
||||||
} from '../../../state/selectors/conversations';
|
|
||||||
import { openConversationWithMessages } from '../../../state/ducks/conversations';
|
import { openConversationWithMessages } from '../../../state/ducks/conversations';
|
||||||
import { Avatar, AvatarSize } from '../../Avatar';
|
import { Avatar, AvatarSize } from '../../Avatar';
|
||||||
import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener';
|
import { useVideoCallEventsListener } from '../../../hooks/useVideoEventListener';
|
||||||
|
|
|
@ -5,12 +5,13 @@ import styled from 'styled-components';
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
import { UserUtils } from '../../../session/utils';
|
import { UserUtils } from '../../../session/utils';
|
||||||
import {
|
import {
|
||||||
|
getCallIsInFullScreen,
|
||||||
getHasOngoingCallWith,
|
getHasOngoingCallWith,
|
||||||
getHasOngoingCallWithFocusedConvo,
|
getHasOngoingCallWithFocusedConvo,
|
||||||
getHasOngoingCallWithFocusedConvoIsOffering,
|
getHasOngoingCallWithFocusedConvoIsOffering,
|
||||||
getHasOngoingCallWithFocusedConvosIsConnecting,
|
getHasOngoingCallWithFocusedConvosIsConnecting,
|
||||||
getHasOngoingCallWithPubkey,
|
getHasOngoingCallWithPubkey,
|
||||||
} from '../../../state/selectors/conversations';
|
} from '../../../state/selectors/call';
|
||||||
import { StyledVideoElement } from './DraggableCallContainer';
|
import { StyledVideoElement } from './DraggableCallContainer';
|
||||||
import { Avatar, AvatarSize } from '../../Avatar';
|
import { Avatar, AvatarSize } from '../../Avatar';
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ import {
|
||||||
import { useModuloWithTripleDots } from '../../../hooks/useModuloWithTripleDots';
|
import { useModuloWithTripleDots } from '../../../hooks/useModuloWithTripleDots';
|
||||||
import { CallWindowControls } from './CallButtons';
|
import { CallWindowControls } from './CallButtons';
|
||||||
import { SessionSpinner } from '../SessionSpinner';
|
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`
|
const VideoContainer = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -119,6 +120,8 @@ export const VideoLoadingSpinner = (props: { fullWidth: boolean }) => {
|
||||||
export const InConversationCallContainer = () => {
|
export const InConversationCallContainer = () => {
|
||||||
const ongoingCallProps = useSelector(getHasOngoingCallWith);
|
const ongoingCallProps = useSelector(getHasOngoingCallWith);
|
||||||
|
|
||||||
|
const isInFullScreen = useSelector(getCallIsInFullScreen);
|
||||||
|
|
||||||
const ongoingCallPubkey = useSelector(getHasOngoingCallWithPubkey);
|
const ongoingCallPubkey = useSelector(getHasOngoingCallWithPubkey);
|
||||||
const ongoingCallWithFocused = useSelector(getHasOngoingCallWithFocusedConvo);
|
const ongoingCallWithFocused = useSelector(getHasOngoingCallWithFocusedConvo);
|
||||||
const ongoingCallUsername = ongoingCallProps?.profileName || ongoingCallProps?.name;
|
const ongoingCallUsername = ongoingCallProps?.profileName || ongoingCallProps?.name;
|
||||||
|
@ -156,14 +159,19 @@ export const InConversationCallContainer = () => {
|
||||||
|
|
||||||
if (videoRefRemote.current) {
|
if (videoRefRemote.current) {
|
||||||
if (currentSelectedAudioOutput === DEVICE_DISABLED_DEVICE_ID) {
|
if (currentSelectedAudioOutput === DEVICE_DISABLED_DEVICE_ID) {
|
||||||
videoRefLocal.current.muted = true;
|
videoRefRemote.current.muted = true;
|
||||||
} else {
|
} else {
|
||||||
// void videoRefLocal.current.setSinkId(currentSelectedAudioOutput);
|
void (videoRefRemote.current as any)?.setSinkId(currentSelectedAudioOutput);
|
||||||
videoRefLocal.current.muted = false;
|
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) {
|
if (!ongoingCallWithFocused) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import _ from 'underscore';
|
||||||
import { useAvatarPath, useConversationUsername } from '../../../hooks/useParamSelector';
|
import { useAvatarPath, useConversationUsername } from '../../../hooks/useParamSelector';
|
||||||
import { ed25519Str } from '../../../session/onions/onionPath';
|
import { ed25519Str } from '../../../session/onions/onionPath';
|
||||||
import { CallManager } from '../../../session/utils';
|
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 { Avatar, AvatarSize } from '../../Avatar';
|
||||||
import { SessionButton, SessionButtonColor } from '../SessionButton';
|
import { SessionButton, SessionButtonColor } from '../SessionButton';
|
||||||
import { SessionWrapperModal } from '../SessionWrapperModal';
|
import { SessionWrapperModal } from '../SessionWrapperModal';
|
||||||
|
|
|
@ -125,16 +125,16 @@ export class SessionRecording extends React.Component<Props, State> {
|
||||||
{isRecording && (
|
{isRecording && (
|
||||||
<SessionIconButton
|
<SessionIconButton
|
||||||
iconType="pause"
|
iconType="pause"
|
||||||
iconSize={'medium'}
|
iconSize="medium"
|
||||||
iconColor={Constants.UI.COLORS.DANGER_ALT}
|
iconColor={Constants.UI.COLORS.DANGER_ALT}
|
||||||
onClick={actionPauseFn}
|
onClick={actionPauseFn}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{actionPauseAudio && (
|
{actionPauseAudio && (
|
||||||
<SessionIconButton iconType="pause" iconSize={'medium'} onClick={actionPauseFn} />
|
<SessionIconButton iconType="pause" iconSize="medium" onClick={actionPauseFn} />
|
||||||
)}
|
)}
|
||||||
{hasRecordingAndPaused && (
|
{hasRecordingAndPaused && (
|
||||||
<SessionIconButton iconType="play" iconSize={'medium'} onClick={this.playAudio} />
|
<SessionIconButton iconType="play" iconSize="medium" onClick={this.playAudio} />
|
||||||
)}
|
)}
|
||||||
{hasRecording && (
|
{hasRecording && (
|
||||||
<SessionIconButton
|
<SessionIconButton
|
||||||
|
|
|
@ -133,7 +133,7 @@ const HeaderItem = () => {
|
||||||
<div className="group-settings-header">
|
<div className="group-settings-header">
|
||||||
<SessionIconButton
|
<SessionIconButton
|
||||||
iconType="chevron"
|
iconType="chevron"
|
||||||
iconSize={'medium'}
|
iconSize="medium"
|
||||||
iconRotation={270}
|
iconRotation={270}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch(closeRightPanel());
|
dispatch(closeRightPanel());
|
||||||
|
|
|
@ -10,6 +10,7 @@ interface SProps extends SessionIconProps {
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
isHidden?: boolean;
|
isHidden?: boolean;
|
||||||
margin?: string;
|
margin?: string;
|
||||||
|
dataTestId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props, ref) => {
|
const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props, ref) => {
|
||||||
|
@ -43,6 +44,7 @@ const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props,
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={clickHandler}
|
onClick={clickHandler}
|
||||||
style={{ display: isHidden ? 'none' : 'flex', margin: margin ? margin : '' }}
|
style={{ display: isHidden ? 'none' : 'flex', margin: margin ? margin : '' }}
|
||||||
|
data-testid={props.dataTestId}
|
||||||
>
|
>
|
||||||
<SessionIcon
|
<SessionIcon
|
||||||
iconType={iconType}
|
iconType={iconType}
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import { getHasIncomingCall, getHasOngoingCall } from '../../../state/selectors/call';
|
||||||
getHasIncomingCall,
|
import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations';
|
||||||
getHasOngoingCall,
|
|
||||||
getNumberOfPinnedConversations,
|
|
||||||
} from '../../../state/selectors/conversations';
|
|
||||||
import { getFocusedSection } from '../../../state/selectors/section';
|
import { getFocusedSection } from '../../../state/selectors/section';
|
||||||
import { Item, Submenu } from 'react-contexify';
|
import { Item, Submenu } from 'react-contexify';
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -48,7 +48,7 @@ const SessionInfo = () => {
|
||||||
<div className="session-settings__version-info">
|
<div className="session-settings__version-info">
|
||||||
<span className="text-selectable">v{window.versionInfo.version}</span>
|
<span className="text-selectable">v{window.versionInfo.version}</span>
|
||||||
<span>
|
<span>
|
||||||
<SessionIconButton iconSize={'medium'} iconType="oxen" onClick={openOxenWebsite} />
|
<SessionIconButton iconSize="medium" iconType="oxen" onClick={openOxenWebsite} />
|
||||||
</span>
|
</span>
|
||||||
<span className="text-selectable">{window.versionInfo.commitHash}</span>
|
<span className="text-selectable">{window.versionInfo.commitHash}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -110,7 +110,6 @@ const channelsToMake = {
|
||||||
removeAllMessagesInConversation,
|
removeAllMessagesInConversation,
|
||||||
|
|
||||||
getMessageBySender,
|
getMessageBySender,
|
||||||
getMessageBySenderAndServerId,
|
|
||||||
getMessageBySenderAndServerTimestamp,
|
getMessageBySenderAndServerTimestamp,
|
||||||
getMessageBySenderAndTimestamp,
|
getMessageBySenderAndTimestamp,
|
||||||
getMessageIdsFromServerIds,
|
getMessageIdsFromServerIds,
|
||||||
|
@ -690,24 +689,6 @@ export async function getMessageBySender({
|
||||||
return new MessageModel(messages[0]);
|
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({
|
export async function getMessageBySenderAndServerTimestamp({
|
||||||
source,
|
source,
|
||||||
serverTimestamp,
|
serverTimestamp,
|
||||||
|
|
|
@ -2,17 +2,15 @@ import { useEffect, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
// tslint:disable-next-line: no-submodule-imports
|
// tslint:disable-next-line: no-submodule-imports
|
||||||
import useMountedState from 'react-use/lib/useMountedState';
|
import useMountedState from 'react-use/lib/useMountedState';
|
||||||
import { CallManager } from '../session/utils';
|
|
||||||
import {
|
import {
|
||||||
|
addVideoEventsListener,
|
||||||
CallManagerOptionsType,
|
CallManagerOptionsType,
|
||||||
DEVICE_DISABLED_DEVICE_ID,
|
DEVICE_DISABLED_DEVICE_ID,
|
||||||
InputItem,
|
InputItem,
|
||||||
} from '../session/utils/CallManager';
|
removeVideoEventsListener,
|
||||||
import {
|
} from '../session/utils/calling/CallManager';
|
||||||
getCallIsInFullScreen,
|
import { getSelectedConversationKey } from '../state/selectors/conversations';
|
||||||
getHasOngoingCallWithPubkey,
|
import { getCallIsInFullScreen, getHasOngoingCallWithPubkey } from '../state/selectors/call';
|
||||||
getSelectedConversationKey,
|
|
||||||
} from '../state/selectors/conversations';
|
|
||||||
|
|
||||||
export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
|
export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
|
||||||
const selectedConversationKey = useSelector(getSelectedConversationKey);
|
const selectedConversationKey = useSelector(getSelectedConversationKey);
|
||||||
|
@ -43,7 +41,7 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
|
||||||
(onSame && ongoingCallPubkey === selectedConversationKey) ||
|
(onSame && ongoingCallPubkey === selectedConversationKey) ||
|
||||||
(!onSame && ongoingCallPubkey !== selectedConversationKey)
|
(!onSame && ongoingCallPubkey !== selectedConversationKey)
|
||||||
) {
|
) {
|
||||||
CallManager.addVideoEventsListener(uniqueId, (options: CallManagerOptionsType) => {
|
addVideoEventsListener(uniqueId, (options: CallManagerOptionsType) => {
|
||||||
const {
|
const {
|
||||||
audioInputsList,
|
audioInputsList,
|
||||||
audioOutputsList,
|
audioOutputsList,
|
||||||
|
@ -71,7 +69,7 @@ export function useVideoCallEventsListener(uniqueId: string, onSame: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
CallManager.removeVideoEventsListener(uniqueId);
|
removeVideoEventsListener(uniqueId);
|
||||||
};
|
};
|
||||||
}, [ongoingCallPubkey, selectedConversationKey, isFullScreen]);
|
}, [ongoingCallPubkey, selectedConversationKey, isFullScreen]);
|
||||||
|
|
||||||
|
|
|
@ -458,8 +458,6 @@ export async function callRecipient(pubkey: string, canCall: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (convo && convo.isPrivate() && !convo.isMe()) {
|
if (convo && convo.isPrivate() && !convo.isMe()) {
|
||||||
convo.callState = 'offering';
|
|
||||||
await convo.commit();
|
|
||||||
await CallManager.USER_callRecipient(convo.id);
|
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> {
|
export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
||||||
public updateLastMessage: () => any;
|
public updateLastMessage: () => any;
|
||||||
public throttledBumpTyping: () => void;
|
public throttledBumpTyping: () => void;
|
||||||
|
@ -189,8 +187,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
||||||
public markRead: (newestUnreadDate: number, providedOptions?: any) => Promise<void>;
|
public markRead: (newestUnreadDate: number, providedOptions?: any) => Promise<void>;
|
||||||
public initialPromise: any;
|
public initialPromise: any;
|
||||||
|
|
||||||
public callState: CallState;
|
|
||||||
|
|
||||||
private typingRefreshTimer?: NodeJS.Timeout | null;
|
private typingRefreshTimer?: NodeJS.Timeout | null;
|
||||||
private typingPauseTimer?: NodeJS.Timeout | null;
|
private typingPauseTimer?: NodeJS.Timeout | null;
|
||||||
private typingTimer?: 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 left = !!this.get('left');
|
||||||
const expireTimer = this.get('expireTimer');
|
const expireTimer = this.get('expireTimer');
|
||||||
const currentNotificationSetting = this.get('triggerNotificationsFor');
|
const currentNotificationSetting = this.get('triggerNotificationsFor');
|
||||||
const callState = this.callState;
|
|
||||||
|
|
||||||
// to reduce the redux store size, only set fields which cannot be undefined
|
// 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
|
// 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,
|
text: lastMessageText,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callState) {
|
|
||||||
toRet.callState = callState;
|
|
||||||
}
|
|
||||||
return toRet;
|
return toRet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
|
|
||||||
export async function removeFromCache(envelope: EnvelopePlus) {
|
export async function removeFromCache(envelope: EnvelopePlus) {
|
||||||
const { id } = envelope;
|
const { id } = envelope;
|
||||||
window?.log?.info(`removing from cache envelope: ${id}`);
|
// window?.log?.info(`removing from cache envelope: ${id}`);
|
||||||
return removeUnprocessed(id);
|
return removeUnprocessed(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ export async function addToCache(
|
||||||
messageHash: string
|
messageHash: string
|
||||||
) {
|
) {
|
||||||
const { id } = envelope;
|
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 encodedEnvelope = StringUtils.decode(plaintext, 'base64');
|
||||||
const data: UnprocessedParameter = {
|
const data: UnprocessedParameter = {
|
||||||
|
|
|
@ -17,6 +17,24 @@ export async function handleCallMessage(
|
||||||
|
|
||||||
const { type } = callMessage;
|
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) {
|
if (type === SignalService.CallMessage.Type.PROVISIONAL_ANSWER) {
|
||||||
await removeFromCache(envelope);
|
await removeFromCache(envelope);
|
||||||
|
|
||||||
|
@ -48,7 +66,7 @@ export async function handleCallMessage(
|
||||||
if (type === SignalService.CallMessage.Type.END_CALL) {
|
if (type === SignalService.CallMessage.Type.END_CALL) {
|
||||||
await removeFromCache(envelope);
|
await removeFromCache(envelope);
|
||||||
|
|
||||||
CallManager.handleCallTypeEndCall(sender, callMessage.uuid);
|
await CallManager.handleCallTypeEndCall(sender, callMessage.uuid);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -210,7 +210,7 @@ async function decryptUnidentifiedSender(
|
||||||
envelope: EnvelopePlus,
|
envelope: EnvelopePlus,
|
||||||
ciphertext: ArrayBuffer
|
ciphertext: ArrayBuffer
|
||||||
): Promise<ArrayBuffer | null> {
|
): Promise<ArrayBuffer | null> {
|
||||||
window?.log?.info('received unidentified sender message');
|
// window?.log?.info('received unidentified sender message');
|
||||||
try {
|
try {
|
||||||
const userX25519KeyPair = await UserUtils.getIdentityKeyPair();
|
const userX25519KeyPair = await UserUtils.getIdentityKeyPair();
|
||||||
|
|
||||||
|
|
|
@ -12,11 +12,7 @@ import { getConversationController } from '../session/conversations';
|
||||||
import { handleClosedGroupControlMessage } from './closedGroups';
|
import { handleClosedGroupControlMessage } from './closedGroups';
|
||||||
import { MessageModel } from '../models/message';
|
import { MessageModel } from '../models/message';
|
||||||
import { MessageModelType } from '../models/messageType';
|
import { MessageModelType } from '../models/messageType';
|
||||||
import {
|
import { getMessageBySender, getMessageBySenderAndServerTimestamp } from '../../ts/data/data';
|
||||||
getMessageBySender,
|
|
||||||
getMessageBySenderAndServerId,
|
|
||||||
getMessageBySenderAndServerTimestamp,
|
|
||||||
} from '../../ts/data/data';
|
|
||||||
import { ConversationModel, ConversationTypeEnum } from '../models/conversation';
|
import { ConversationModel, ConversationTypeEnum } from '../models/conversation';
|
||||||
import { allowOnlyOneAtATime } from '../session/utils/Promise';
|
import { allowOnlyOneAtATime } from '../session/utils/Promise';
|
||||||
import { toHex } from '../session/utils/String';
|
import { toHex } from '../session/utils/String';
|
||||||
|
@ -371,22 +367,14 @@ export async function isMessageDuplicate({
|
||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
if (serverId || serverTimestamp) {
|
if (serverId || serverTimestamp) {
|
||||||
// first try to find a duplicate serverId from this sender
|
// first try to find a duplicate with the same serverTimestamp 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
|
|
||||||
if (!result && serverTimestamp) {
|
if (!result && serverTimestamp) {
|
||||||
result = await getMessageBySenderAndServerTimestamp({
|
result = await getMessageBySenderAndServerTimestamp({
|
||||||
source,
|
source,
|
||||||
serverTimestamp,
|
serverTimestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// if we have a result, it means a specific user sent two messages either with the same
|
// if we have a result, it means a specific user sent two messages either with the same serverTimestamp.
|
||||||
// serverId or the same serverTimestamp.
|
|
||||||
// no need to do anything else, those messages must be the same
|
// 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
|
// 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
|
// 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) {
|
function queueEnvelope(envelope: EnvelopePlus, messageHash?: string) {
|
||||||
const id = getEnvelopeId(envelope);
|
const id = getEnvelopeId(envelope);
|
||||||
window?.log?.info('queueing envelope', id);
|
// window?.log?.info('queueing envelope', id);
|
||||||
|
|
||||||
const task = handleEnvelope.bind(null, envelope, messageHash);
|
const task = handleEnvelope.bind(null, envelope, messageHash);
|
||||||
const taskWithTimeout = createTaskWithTimeout(task, `queueEnvelope ${id}`);
|
const taskWithTimeout = createTaskWithTimeout(task, `queueEnvelope ${id}`);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { MessageSender } from '.';
|
||||||
import { getMessageById } from '../../../ts/data/data';
|
import { getMessageById } from '../../../ts/data/data';
|
||||||
import { SNodeAPI } from '../snode_api';
|
import { SNodeAPI } from '../snode_api';
|
||||||
import { getConversationController } from '../conversations';
|
import { getConversationController } from '../conversations';
|
||||||
|
import { ed25519Str } from '../onions/onionPath';
|
||||||
|
|
||||||
const DEFAULT_CONNECTIONS = 1;
|
const DEFAULT_CONNECTIONS = 1;
|
||||||
|
|
||||||
|
@ -129,7 +130,7 @@ export async function TEST_sendMessageToSnode(
|
||||||
const data64 = window.dcodeIO.ByteBuffer.wrap(data).toString('base64');
|
const data64 = window.dcodeIO.ByteBuffer.wrap(data).toString('base64');
|
||||||
const swarm = await getSwarmFor(pubKey);
|
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
|
// send parameters
|
||||||
const params = {
|
const params = {
|
||||||
pubKey,
|
pubKey,
|
||||||
|
@ -190,7 +191,9 @@ export async function TEST_sendMessageToSnode(
|
||||||
}
|
}
|
||||||
|
|
||||||
window?.log?.info(
|
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 _ from 'lodash';
|
||||||
import { getMessageById } from '../../data/data';
|
import { getMessageById } from '../../data/data';
|
||||||
import { MessageModel } from '../../models/message';
|
|
||||||
import { SignalService } from '../../protobuf';
|
import { SignalService } from '../../protobuf';
|
||||||
import { PnServer } from '../../pushnotification';
|
import { PnServer } from '../../pushnotification';
|
||||||
import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
|
import { OpenGroupVisibleMessage } from '../messages/outgoing/visibleMessage/OpenGroupVisibleMessage';
|
||||||
import { EncryptionType, RawMessage } from '../types';
|
import { EncryptionType, RawMessage } from '../types';
|
||||||
import { UserUtils } from '../utils';
|
import { UserUtils } from '../utils';
|
||||||
|
|
||||||
// tslint:disable-next-line no-unnecessary-class
|
// tslint:disable-next-line: no-unnecessary-class
|
||||||
export class MessageSentHandler {
|
export class MessageSentHandler {
|
||||||
public static async handlePublicMessageSentSuccess(
|
public static async handlePublicMessageSentSuccess(
|
||||||
sentMessage: OpenGroupVisibleMessage,
|
sentMessage: OpenGroupVisibleMessage,
|
||||||
|
@ -54,10 +53,8 @@ export class MessageSentHandler {
|
||||||
|
|
||||||
let sentTo = fetchedMessage.get('sent_to') || [];
|
let sentTo = fetchedMessage.get('sent_to') || [];
|
||||||
|
|
||||||
let isOurDevice = false;
|
const isOurDevice = UserUtils.isUsFromCache(sentMessage.device);
|
||||||
if (sentMessage.device) {
|
|
||||||
isOurDevice = UserUtils.isUsFromCache(sentMessage.device);
|
|
||||||
}
|
|
||||||
// FIXME this is not correct and will cause issues with syncing
|
// FIXME this is not correct and will cause issues with syncing
|
||||||
// At this point the only way to check for medium
|
// At this point the only way to check for medium
|
||||||
// group is by comparing the encryption type
|
// group is by comparing the encryption type
|
||||||
|
@ -113,8 +110,9 @@ export class MessageSentHandler {
|
||||||
window?.log?.warn(
|
window?.log?.warn(
|
||||||
'Got an error while trying to sendSyncMessage(): fetchedMessage is null'
|
'Got an error while trying to sendSyncMessage(): fetchedMessage is null'
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
fetchedMessage = tempFetchMessage as MessageModel;
|
fetchedMessage = tempFetchMessage;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window?.log?.warn('Got an error while trying to sendSyncMessage():', e);
|
window?.log?.warn('Got an error while trying to sendSyncMessage():', e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -318,7 +318,6 @@ export class SwarmPolling {
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadGroupIds() {
|
private loadGroupIds() {
|
||||||
// Start polling for medium size groups as well (they might be in different swarms)
|
|
||||||
const convos = getConversationController().getConversations();
|
const convos = getConversationController().getConversations();
|
||||||
|
|
||||||
const mediumGroupsOnly = convos.filter(
|
const mediumGroupsOnly = convos.filter(
|
||||||
|
@ -328,7 +327,6 @@ export class SwarmPolling {
|
||||||
|
|
||||||
mediumGroupsOnly.forEach((c: any) => {
|
mediumGroupsOnly.forEach((c: any) => {
|
||||||
this.addGroupId(new PubKey(c.id));
|
this.addGroupId(new PubKey(c.id));
|
||||||
// TODO: unsubscribe if the group is deleted
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,42 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { MessageUtils, ToastUtils } from '.';
|
import { MessageUtils, ToastUtils, UserUtils } from '../';
|
||||||
import { getCallMediaPermissionsSettings } from '../../components/session/settings/SessionSettings';
|
import { getCallMediaPermissionsSettings } from '../../../components/session/settings/SessionSettings';
|
||||||
import { getConversationById } from '../../data/data';
|
import { getConversationById } from '../../../data/data';
|
||||||
import { ConversationModel } from '../../models/conversation';
|
import { MessageModelType } from '../../../models/messageType';
|
||||||
import { MessageModelType } from '../../models/messageType';
|
import { SignalService } from '../../../protobuf';
|
||||||
import { SignalService } from '../../protobuf';
|
import { openConversationWithMessages } from '../../../state/ducks/conversations';
|
||||||
import {
|
import {
|
||||||
answerCall,
|
answerCall,
|
||||||
callConnected,
|
callConnected,
|
||||||
|
CallStatusEnum,
|
||||||
endCall,
|
endCall,
|
||||||
incomingCall,
|
incomingCall,
|
||||||
openConversationWithMessages,
|
|
||||||
setFullScreenCall,
|
setFullScreenCall,
|
||||||
startingCallWith,
|
startingCallWith,
|
||||||
} from '../../state/ducks/conversations';
|
} from '../../../state/ducks/call';
|
||||||
import { getConversationController } from '../conversations';
|
import { getConversationController } from '../../conversations';
|
||||||
import { CallMessage } from '../messages/outgoing/controlMessage/CallMessage';
|
import { CallMessage } from '../../messages/outgoing/controlMessage/CallMessage';
|
||||||
import { ed25519Str } from '../onions/onionPath';
|
import { ed25519Str } from '../../onions/onionPath';
|
||||||
import { getMessageQueue, MessageSender } from '../sending';
|
import { PubKey } from '../../types';
|
||||||
import { PubKey } from '../types';
|
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { PnServer } from '../../pushnotification';
|
import { PnServer } from '../../../pushnotification';
|
||||||
import { setIsRinging } from './RingingManager';
|
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 };
|
export type InputItem = { deviceId: string; label: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This uuid is set only once we accepted a call or started one.
|
||||||
|
*/
|
||||||
let currentCallUUID: string | undefined;
|
let currentCallUUID: string | undefined;
|
||||||
|
|
||||||
|
const rejectedCallUUIDS: Set<string> = new Set();
|
||||||
|
|
||||||
export type CallManagerOptionsType = {
|
export type CallManagerOptionsType = {
|
||||||
localStream: MediaStream | null;
|
localStream: MediaStream | null;
|
||||||
remoteStream: MediaStream | null;
|
remoteStream: MediaStream | null;
|
||||||
|
@ -47,7 +56,7 @@ function callVideoListeners() {
|
||||||
if (videoEventsListeners.length) {
|
if (videoEventsListeners.length) {
|
||||||
videoEventsListeners.forEach(item => {
|
videoEventsListeners.forEach(item => {
|
||||||
item.listener?.({
|
item.listener?.({
|
||||||
localStream: mediaDevices,
|
localStream,
|
||||||
remoteStream,
|
remoteStream,
|
||||||
camerasList,
|
camerasList,
|
||||||
audioInputsList,
|
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>>>();
|
const callCache = new Map<string, Map<string, Array<SignalService.CallMessage>>>();
|
||||||
|
|
||||||
let peerConnection: RTCPeerConnection | null;
|
let peerConnection: RTCPeerConnection | null;
|
||||||
let dataChannel: RTCDataChannel | null;
|
let dataChannel: RTCDataChannel | null;
|
||||||
let remoteStream: MediaStream | null;
|
let remoteStream: MediaStream | null;
|
||||||
let mediaDevices: MediaStream | null;
|
let localStream: MediaStream | null;
|
||||||
let remoteVideoStreamIsMuted = true;
|
let remoteVideoStreamIsMuted = true;
|
||||||
|
|
||||||
export const DEVICE_DISABLED_DEVICE_ID = 'off';
|
export const DEVICE_DISABLED_DEVICE_ID = 'off';
|
||||||
|
@ -103,8 +112,38 @@ const configuration: RTCConfiguration = {
|
||||||
iceServers: [
|
iceServers: [
|
||||||
{
|
{
|
||||||
urls: 'turn:freyr.getsession.org',
|
urls: 'turn:freyr.getsession.org',
|
||||||
username: 'session',
|
username: 'session202111',
|
||||||
credential: 'session',
|
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
|
// 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) {
|
if (sender?.track) {
|
||||||
sender.track.enabled = false;
|
sender.track.enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// do the same changes locally
|
||||||
|
localStream?.getVideoTracks().forEach(t => {
|
||||||
|
t.stop();
|
||||||
|
localStream?.removeTrack(t);
|
||||||
|
});
|
||||||
|
localStream?.addTrack(getBlackSilenceMediaStream().getVideoTracks()[0]);
|
||||||
|
|
||||||
sendVideoStatusViaDataChannel();
|
sendVideoStatusViaDataChannel();
|
||||||
callVideoListeners();
|
callVideoListeners();
|
||||||
return;
|
return;
|
||||||
|
@ -200,28 +247,37 @@ export async function selectCameraByDeviceId(cameraDeviceId: string) {
|
||||||
try {
|
try {
|
||||||
const newVideoStream = await navigator.mediaDevices.getUserMedia(devicesConfig);
|
const newVideoStream = await navigator.mediaDevices.getUserMedia(devicesConfig);
|
||||||
const videoTrack = newVideoStream.getVideoTracks()[0];
|
const videoTrack = newVideoStream.getVideoTracks()[0];
|
||||||
|
|
||||||
if (!peerConnection) {
|
if (!peerConnection) {
|
||||||
throw new Error('cannot selectCameraByDeviceId without a peer connection');
|
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();
|
window.log.info('replacing video track');
|
||||||
callVideoListeners();
|
const videoSender = peerConnection
|
||||||
|
.getTransceivers()
|
||||||
|
.find(t => t.sender.track?.kind === 'video')?.sender;
|
||||||
|
|
||||||
|
videoTrack.enabled = true;
|
||||||
|
if (videoSender) {
|
||||||
|
await videoSender.replaceTrack(videoTrack);
|
||||||
} else {
|
} 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) {
|
} catch (e) {
|
||||||
window.log.warn('selectCameraByDeviceId failed with', e.message);
|
window.log.warn('selectCameraByDeviceId failed with', e.message);
|
||||||
|
ToastUtils.pushToastError('selectCamera', e.message);
|
||||||
callVideoListeners();
|
callVideoListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,6 +293,12 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
|
||||||
if (sender?.track) {
|
if (sender?.track) {
|
||||||
sender.track.enabled = false;
|
sender.track.enabled = false;
|
||||||
}
|
}
|
||||||
|
// do the same changes locally
|
||||||
|
localStream?.getAudioTracks().forEach(t => {
|
||||||
|
t.stop();
|
||||||
|
localStream?.removeTrack(t);
|
||||||
|
});
|
||||||
|
localStream?.addTrack(getBlackSilenceMediaStream().getAudioTracks()[0]);
|
||||||
callVideoListeners();
|
callVideoListeners();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -251,16 +313,18 @@ export async function selectAudioInputByDeviceId(audioInputDeviceId: string) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newAudioStream = await navigator.mediaDevices.getUserMedia(devicesConfig);
|
const newAudioStream = await navigator.mediaDevices.getUserMedia(devicesConfig);
|
||||||
|
|
||||||
const audioTrack = newAudioStream.getAudioTracks()[0];
|
const audioTrack = newAudioStream.getAudioTracks()[0];
|
||||||
if (!peerConnection) {
|
if (!peerConnection) {
|
||||||
throw new Error('cannot selectAudioInputByDeviceId without a peer connection');
|
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;
|
return s.track?.kind === audioTrack.kind;
|
||||||
});
|
});
|
||||||
|
window.log.info('replacing audio track');
|
||||||
|
|
||||||
if (sender) {
|
if (audioSender) {
|
||||||
await sender.replaceTrack(audioTrack);
|
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)
|
// we actually do not need to toggle the track here, as toggling it here unmuted here locally (so we start to hear ourselves)
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to get sender for selectAudioInputByDeviceId ');
|
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 {
|
try {
|
||||||
makingOffer = true;
|
makingOffer = true;
|
||||||
window.log.info('got handleNegotiationNeeded event. creating offer');
|
window.log.info('got createOfferAndSendIt event. creating offer');
|
||||||
const offer = await peerConnection?.createOffer({
|
await (peerConnection as any)?.setLocalDescription();
|
||||||
offerToReceiveAudio: true,
|
const offer = peerConnection?.localDescription;
|
||||||
offerToReceiveVideo: true,
|
|
||||||
});
|
|
||||||
if (!offer) {
|
if (!offer) {
|
||||||
throw new Error('Could not create an offer');
|
throw new Error('Could not create an offer');
|
||||||
}
|
}
|
||||||
await peerConnection?.setLocalDescription(offer);
|
|
||||||
|
|
||||||
if (!currentCallUUID) {
|
if (!currentCallUUID) {
|
||||||
window.log.warn('cannot send offer without a currentCallUUID');
|
window.log.warn('cannot send offer without a currentCallUUID');
|
||||||
|
@ -313,18 +374,18 @@ async function handleNegotiationNeededEvent(recipient: string) {
|
||||||
uuid: currentCallUUID,
|
uuid: currentCallUUID,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.log.info('sending OFFER MESSAGE');
|
window.log.info(`sending '${offer.type}'' with callUUID: ${currentCallUUID}`);
|
||||||
const negotationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably(
|
const negotiationOfferSendResult = await getMessageQueue().sendToPubKeyNonDurably(
|
||||||
PubKey.cast(recipient),
|
PubKey.cast(recipient),
|
||||||
offerMessage
|
offerMessage
|
||||||
);
|
);
|
||||||
if (typeof negotationOfferSendResult === 'number') {
|
if (typeof negotiationOfferSendResult === 'number') {
|
||||||
// window.log?.warn('setting last sent timestamp');
|
// window.log?.warn('setting last sent timestamp');
|
||||||
lastOutgoingOfferTimestamp = negotationOfferSendResult;
|
lastOutgoingOfferTimestamp = negotiationOfferSendResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.log?.error(`Error on handling negotiation needed ${err}`);
|
window.log?.error(`Error createOfferAndSendIt ${err}`);
|
||||||
} finally {
|
} finally {
|
||||||
makingOffer = false;
|
makingOffer = false;
|
||||||
}
|
}
|
||||||
|
@ -340,51 +401,32 @@ function handleIceCandidates(event: RTCPeerConnectionIceEvent, pubkey: string) {
|
||||||
async function openMediaDevicesAndAddTracks() {
|
async function openMediaDevicesAndAddTracks() {
|
||||||
try {
|
try {
|
||||||
await updateConnectedDevices();
|
await updateConnectedDevices();
|
||||||
if (!camerasList.length) {
|
|
||||||
ToastUtils.pushNoCameraFound();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!audioInputsList.length) {
|
if (!audioInputsList.length) {
|
||||||
ToastUtils.pushNoAudioInputFound();
|
ToastUtils.pushNoAudioInputFound();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedAudioInputId = audioInputsList[0].deviceId;
|
selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID; //audioInputsList[0].deviceId;
|
||||||
selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
|
selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`openMediaDevices videoDevice:${selectedCameraId}:${camerasList[0].label} audioDevice:${selectedAudioInputId}`
|
`openMediaDevices videoDevice:${selectedCameraId} audioDevice:${selectedAudioInputId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const devicesConfig = {
|
localStream = getBlackSilenceMediaStream();
|
||||||
audio: {
|
localStream.getTracks().map(track => {
|
||||||
deviceId: selectedAudioInputId,
|
if (localStream) {
|
||||||
|
peerConnection?.addTrack(track, localStream);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
window.log.warn('openMediaDevices: ', err);
|
||||||
ToastUtils.pushVideoCallPermissionNeeded();
|
ToastUtils.pushVideoCallPermissionNeeded();
|
||||||
closeVideoCall();
|
closeVideoCall();
|
||||||
}
|
}
|
||||||
callVideoListeners();
|
callVideoListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line: function-name
|
|
||||||
export async function USER_callRecipient(recipient: string) {
|
export async function USER_callRecipient(recipient: string) {
|
||||||
if (!getCallMediaPermissionsSettings()) {
|
if (!getCallMediaPermissionsSettings()) {
|
||||||
ToastUtils.pushVideoCallPermissionNeeded();
|
ToastUtils.pushVideoCallPermissionNeeded();
|
||||||
|
@ -412,12 +454,16 @@ export async function USER_callRecipient(recipient: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
window.log.info('Sending preOffer message to ', ed25519Str(recipient));
|
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 rawMessage = await MessageUtils.toRawMessage(PubKey.cast(recipient), preOfferMsg);
|
||||||
const { wrappedEnvelope } = await MessageSender.send(rawMessage);
|
const { wrappedEnvelope } = await MessageSender.send(rawMessage);
|
||||||
void PnServer.notifyPnServer(wrappedEnvelope, recipient);
|
void PnServer.notifyPnServer(wrappedEnvelope, recipient);
|
||||||
|
|
||||||
await openMediaDevicesAndAddTracks();
|
await openMediaDevicesAndAddTracks();
|
||||||
setIsRinging(true);
|
setIsRinging(true);
|
||||||
|
await createOfferAndSendIt(recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
const iceCandidates: Array<RTCIceCandidate> = new Array();
|
const iceCandidates: Array<RTCIceCandidate> = new Array();
|
||||||
|
@ -455,7 +501,9 @@ const iceSenderDebouncer = _.debounce(async (recipient: string) => {
|
||||||
uuid: currentCallUUID,
|
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);
|
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
@ -490,6 +538,15 @@ function handleConnectionStateChanged(pubkey: string) {
|
||||||
closeVideoCall();
|
closeVideoCall();
|
||||||
} else if (peerConnection?.connectionState === 'connected') {
|
} else if (peerConnection?.connectionState === 'connected') {
|
||||||
setIsRinging(false);
|
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 }));
|
window.inboxStore?.dispatch(callConnected({ pubkey }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -510,14 +567,16 @@ function closeVideoCall() {
|
||||||
dataChannel.close();
|
dataChannel.close();
|
||||||
dataChannel = null;
|
dataChannel = null;
|
||||||
}
|
}
|
||||||
if (mediaDevices) {
|
if (localStream) {
|
||||||
mediaDevices.getTracks().forEach(track => {
|
localStream.getTracks().forEach(track => {
|
||||||
track.stop();
|
track.stop();
|
||||||
|
localStream?.removeTrack(track);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteStream) {
|
if (remoteStream) {
|
||||||
remoteStream.getTracks().forEach(track => {
|
remoteStream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
remoteStream?.removeTrack(track);
|
remoteStream?.removeTrack(track);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -526,13 +585,15 @@ function closeVideoCall() {
|
||||||
peerConnection = null;
|
peerConnection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaDevices = null;
|
localStream = null;
|
||||||
remoteStream = null;
|
remoteStream = null;
|
||||||
selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
|
selectedCameraId = DEVICE_DISABLED_DEVICE_ID;
|
||||||
selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID;
|
selectedAudioInputId = DEVICE_DISABLED_DEVICE_ID;
|
||||||
currentCallUUID = undefined;
|
currentCallUUID = undefined;
|
||||||
|
|
||||||
window.inboxStore?.dispatch(setFullScreenCall(false));
|
window.inboxStore?.dispatch(setFullScreenCall(false));
|
||||||
|
window.inboxStore?.dispatch(endCall());
|
||||||
|
|
||||||
remoteVideoStreamIsMuted = true;
|
remoteVideoStreamIsMuted = true;
|
||||||
|
|
||||||
makingOffer = false;
|
makingOffer = false;
|
||||||
|
@ -542,24 +603,26 @@ function closeVideoCall() {
|
||||||
callVideoListeners();
|
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>) {
|
function onDataChannelReceivedMessage(ev: MessageEvent<string>) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(ev.data);
|
const parsed = JSON.parse(ev.data);
|
||||||
|
|
||||||
if (parsed.hangup !== undefined) {
|
if (parsed.hangup !== undefined) {
|
||||||
const foundEntry = getConversationController()
|
const { ongoingCallStatus, ongoingCallWith } = getCallingStateOutsideOfRedux();
|
||||||
.getConversations()
|
if (
|
||||||
.find(
|
(ongoingCallStatus === 'connecting' ||
|
||||||
(convo: ConversationModel) =>
|
ongoingCallStatus === 'offering' ||
|
||||||
convo.callState === 'connecting' ||
|
ongoingCallStatus === 'ongoing') &&
|
||||||
convo.callState === 'offering' ||
|
ongoingCallWith
|
||||||
convo.callState === 'ongoing'
|
) {
|
||||||
);
|
void handleCallTypeEndCall(ongoingCallWith, currentCallUUID);
|
||||||
|
|
||||||
if (!foundEntry || !foundEntry.id) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
handleCallTypeEndCall(foundEntry.id, currentCallUUID);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -578,7 +641,7 @@ function onDataChannelOnOpen() {
|
||||||
sendVideoStatusViaDataChannel();
|
sendVideoStatusViaDataChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false) {
|
function createOrGetPeerConnection(withPubkey: string) {
|
||||||
if (peerConnection) {
|
if (peerConnection) {
|
||||||
return peerConnection;
|
return peerConnection;
|
||||||
}
|
}
|
||||||
|
@ -592,15 +655,7 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false)
|
||||||
|
|
||||||
dataChannel.onmessage = onDataChannelReceivedMessage;
|
dataChannel.onmessage = onDataChannelReceivedMessage;
|
||||||
dataChannel.onopen = onDataChannelOnOpen;
|
dataChannel.onopen = onDataChannelOnOpen;
|
||||||
|
|
||||||
if (!isAcceptingCall) {
|
|
||||||
peerConnection.onnegotiationneeded = async () => {
|
|
||||||
await handleNegotiationNeededEvent(withPubkey);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
|
peerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
|
||||||
|
|
||||||
peerConnection.ontrack = event => {
|
peerConnection.ontrack = event => {
|
||||||
event.track.onunmute = () => {
|
event.track.onunmute = () => {
|
||||||
remoteStream?.addTrack(event.track);
|
remoteStream?.addTrack(event.track);
|
||||||
|
@ -640,7 +695,6 @@ function createOrGetPeerConnection(withPubkey: string, isAcceptingCall = false)
|
||||||
return peerConnection;
|
return peerConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line: function-name
|
|
||||||
export async function USER_acceptIncomingCallRequest(fromSender: string) {
|
export async function USER_acceptIncomingCallRequest(fromSender: string) {
|
||||||
window.log.info('USER_acceptIncomingCallRequest');
|
window.log.info('USER_acceptIncomingCallRequest');
|
||||||
setIsRinging(false);
|
setIsRinging(false);
|
||||||
|
@ -674,7 +728,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
|
||||||
}
|
}
|
||||||
currentCallUUID = lastOfferMessage.uuid;
|
currentCallUUID = lastOfferMessage.uuid;
|
||||||
|
|
||||||
peerConnection = createOrGetPeerConnection(fromSender, true);
|
peerConnection = createOrGetPeerConnection(fromSender);
|
||||||
|
|
||||||
await openMediaDevicesAndAddTracks();
|
await openMediaDevicesAndAddTracks();
|
||||||
|
|
||||||
|
@ -711,46 +765,59 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) {
|
||||||
await buildAnswerAndSendIt(fromSender);
|
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) {
|
export async function USER_rejectIncomingCallRequest(fromSender: string) {
|
||||||
setIsRinging(false);
|
setIsRinging(false);
|
||||||
|
// close the popup call
|
||||||
|
window.inboxStore?.dispatch(endCall());
|
||||||
const lastOfferMessage = findLastMessageTypeFromSender(
|
const lastOfferMessage = findLastMessageTypeFromSender(
|
||||||
fromSender,
|
fromSender,
|
||||||
SignalService.CallMessage.Type.OFFER
|
SignalService.CallMessage.Type.OFFER
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastCallUUID = lastOfferMessage?.uuid;
|
const aboutCallUUID = lastOfferMessage?.uuid;
|
||||||
window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${lastCallUUID}`);
|
window.log.info(`USER_rejectIncomingCallRequest ${ed25519Str(fromSender)}: ${aboutCallUUID}`);
|
||||||
if (lastCallUUID) {
|
if (aboutCallUUID) {
|
||||||
|
rejectedCallUUIDS.add(aboutCallUUID);
|
||||||
const endCallMessage = new CallMessage({
|
const endCallMessage = new CallMessage({
|
||||||
type: SignalService.CallMessage.Type.END_CALL,
|
type: SignalService.CallMessage.Type.END_CALL,
|
||||||
timestamp: Date.now(),
|
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
|
// 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(
|
// clear the ongoing call if needed
|
||||||
endCall({
|
if (ongoingCallWith && ongoingCallStatus && ongoingCallWith === fromSender) {
|
||||||
pubkey: fromSender,
|
closeVideoCall();
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
export async function USER_hangup(fromSender: string) {
|
||||||
window.log.info('USER_hangup');
|
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');
|
window.log.warn('should not be able to hangup without a currentCallUUID');
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
rejectedCallUUIDS.add(currentCallUUID);
|
||||||
const endCallMessage = new CallMessage({
|
const endCallMessage = new CallMessage({
|
||||||
type: SignalService.CallMessage.Type.END_CALL,
|
type: SignalService.CallMessage.Type.END_CALL,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
@ -766,7 +834,7 @@ export async function USER_hangup(fromSender: string) {
|
||||||
void getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(fromSender), endCallMessage);
|
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');
|
window.log.info('sending hangup with an END_CALL MESSAGE');
|
||||||
|
|
||||||
sendHangupViaDataChannel();
|
sendHangupViaDataChannel();
|
||||||
|
@ -776,16 +844,36 @@ export async function USER_hangup(fromSender: string) {
|
||||||
closeVideoCall();
|
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);
|
window.log.info('handling callMessage END_CALL:', aboutCallUUID);
|
||||||
|
|
||||||
if (aboutCallUUID) {
|
if (aboutCallUUID) {
|
||||||
|
rejectedCallUUIDS.add(aboutCallUUID);
|
||||||
|
|
||||||
clearCallCacheFromPubkeyAndUUID(sender, 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) {
|
if (aboutCallUUID === currentCallUUID) {
|
||||||
closeVideoCall();
|
closeVideoCall();
|
||||||
|
window.inboxStore?.dispatch(endCall());
|
||||||
window.inboxStore?.dispatch(endCall({ pubkey: sender }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -796,16 +884,12 @@ async function buildAnswerAndSendIt(sender: string) {
|
||||||
window.log.warn('cannot send answer without a currentCallUUID');
|
window.log.warn('cannot send answer without a currentCallUUID');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await (peerConnection as any).setLocalDescription();
|
||||||
const answer = await peerConnection.createAnswer({
|
const answer = peerConnection.localDescription;
|
||||||
offerToReceiveAudio: true,
|
|
||||||
offerToReceiveVideo: true,
|
|
||||||
});
|
|
||||||
if (!answer?.sdp || answer.sdp.length === 0) {
|
if (!answer?.sdp || answer.sdp.length === 0) {
|
||||||
window.log.warn('failed to create answer');
|
window.log.warn('failed to create answer');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await peerConnection.setLocalDescription(answer);
|
|
||||||
const answerSdp = answer.sdp;
|
const answerSdp = answer.sdp;
|
||||||
const callAnswerMessage = new CallMessage({
|
const callAnswerMessage = new CallMessage({
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
@ -814,12 +898,15 @@ async function buildAnswerAndSendIt(sender: string) {
|
||||||
uuid: currentCallUUID,
|
uuid: currentCallUUID,
|
||||||
});
|
});
|
||||||
|
|
||||||
window.log.info('sending ANSWER MESSAGE');
|
window.log.info('sending ANSWER MESSAGE and sync');
|
||||||
|
await sendCallMessageAndSync(callAnswerMessage, sender);
|
||||||
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(sender), callAnswerMessage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCallRejected(uuid: string) {
|
||||||
|
return rejectedCallUUIDS.has(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleCallTypeOffer(
|
export async function handleCallTypeOffer(
|
||||||
sender: string,
|
sender: string,
|
||||||
callMessage: SignalService.CallMessage,
|
callMessage: SignalService.CallMessage,
|
||||||
|
@ -832,45 +919,53 @@ export async function handleCallTypeOffer(
|
||||||
}
|
}
|
||||||
window.log.info('handling callMessage OFFER with uuid: ', remoteCallUUID);
|
window.log.info('handling callMessage OFFER with uuid: ', remoteCallUUID);
|
||||||
|
|
||||||
const convos = getConversationController().getConversations();
|
|
||||||
const callingConvos = convos.filter(convo => convo.callState !== undefined);
|
|
||||||
|
|
||||||
if (!getCallMediaPermissionsSettings()) {
|
if (!getCallMediaPermissionsSettings()) {
|
||||||
await handleMissedCall(sender, incomingOfferTimestamp, true);
|
await handleMissedCall(sender, incomingOfferTimestamp, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (callingConvos.length > 0) {
|
if (currentCallUUID && currentCallUUID !== remoteCallUUID) {
|
||||||
// we just got a new offer from someone we are NOT already in a call with
|
// we just got a new offer with a different callUUID. this is a missed call (from either the same sender or another one)
|
||||||
if (callingConvos.length !== 1 || callingConvos[0].id !== sender) {
|
if (callCache.get(sender)?.has(currentCallUUID)) {
|
||||||
await handleMissedCall(sender, incomingOfferTimestamp, false);
|
// 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;
|
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 =
|
const readyForOffer =
|
||||||
!makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending);
|
!makingOffer && (peerConnection?.signalingState === 'stable' || isSettingRemoteAnswerPending);
|
||||||
const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
|
const polite = lastOutgoingOfferTimestamp < incomingOfferTimestamp;
|
||||||
const offerCollision = !readyForOffer;
|
const offerCollision = !readyForOffer;
|
||||||
|
|
||||||
ignoreOffer = !polite && offerCollision;
|
ignoreOffer = !polite && offerCollision;
|
||||||
|
|
||||||
if (ignoreOffer) {
|
if (ignoreOffer) {
|
||||||
window.log?.warn('Received offer when unready for offer; Ignoring offer.');
|
window.log?.warn('Received offer when unready for offer; Ignoring offer.');
|
||||||
return;
|
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');
|
window.log.info('Got a new offer message from our ongoing call');
|
||||||
isSettingRemoteAnswerPending = false;
|
|
||||||
const remoteDesc = new RTCSessionDescription({
|
const remoteOfferDesc = new RTCSessionDescription({
|
||||||
type: 'offer',
|
type: 'offer',
|
||||||
sdp: callMessage.sdps[0],
|
sdp: callMessage.sdps[0],
|
||||||
});
|
});
|
||||||
isSettingRemoteAnswerPending = false;
|
isSettingRemoteAnswerPending = false;
|
||||||
if (peerConnection) {
|
|
||||||
await peerConnection.setRemoteDescription(remoteDesc); // SRD rolls back as needed
|
await peerConnection.setRemoteDescription(remoteOfferDesc); // SRD rolls back as needed
|
||||||
await buildAnswerAndSendIt(sender);
|
isSettingRemoteAnswerPending = false;
|
||||||
}
|
|
||||||
|
await buildAnswerAndSendIt(sender);
|
||||||
} else {
|
} else {
|
||||||
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
|
window.inboxStore?.dispatch(incomingCall({ pubkey: sender }));
|
||||||
|
|
||||||
|
@ -927,32 +1022,94 @@ export async function handleMissedCall(
|
||||||
return;
|
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) {
|
export async function handleCallTypeAnswer(sender: string, callMessage: SignalService.CallMessage) {
|
||||||
if (!callMessage.sdps || callMessage.sdps.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const remoteCallUUID = callMessage.uuid;
|
const callMessageUUID = callMessage.uuid;
|
||||||
if (!remoteCallUUID || remoteCallUUID.length === 0) {
|
if (!callMessageUUID || callMessageUUID.length === 0) {
|
||||||
window.log.warn('handleCallTypeAnswer has no valid uuid');
|
window.log.warn('handleCallTypeAnswer has no valid uuid');
|
||||||
return;
|
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) {
|
if (!peerConnection) {
|
||||||
window.log.info('handleCallTypeAnswer without peer connection. Dropping');
|
window.log.info('handleCallTypeAnswer without peer connection. Dropping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.inboxStore?.dispatch(answerCall({ pubkey: sender }));
|
window.inboxStore?.dispatch(
|
||||||
const remoteDesc = new RTCSessionDescription({ type: 'answer', sdp: callMessage.sdps[0] });
|
answerCall({
|
||||||
|
pubkey: sender,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// window.log?.info('Setting remote answer pending');
|
try {
|
||||||
isSettingRemoteAnswerPending = true;
|
isSettingRemoteAnswerPending = true;
|
||||||
await peerConnection?.setRemoteDescription(remoteDesc); // SRD rolls back as needed
|
|
||||||
isSettingRemoteAnswerPending = false;
|
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(
|
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 SyncUtils from './syncUtils';
|
||||||
import * as AttachmentsV2Utils from './AttachmentsV2';
|
import * as AttachmentsV2Utils from './AttachmentsV2';
|
||||||
import * as AttachmentDownloads from './AttachmentsDownload';
|
import * as AttachmentDownloads from './AttachmentsDownload';
|
||||||
import * as CallManager from './CallManager';
|
import * as CallManager from './calling/CallManager';
|
||||||
|
|
||||||
export * from './Attachments';
|
export * from './Attachments';
|
||||||
export * from './TypedEmitter';
|
export * from './TypedEmitter';
|
||||||
|
|
|
@ -149,7 +149,7 @@ const lightColorTextSubtle = `${black}99`;
|
||||||
const lightColorTextAccent = '#00c769';
|
const lightColorTextAccent = '#00c769';
|
||||||
const lightColorSessionShadow = `0 0 4px 0 ${black}5E`;
|
const lightColorSessionShadow = `0 0 4px 0 ${black}5E`;
|
||||||
const lightColorComposeViewBg = '#efefef';
|
const lightColorComposeViewBg = '#efefef';
|
||||||
const lightColorSentMessageBg = accentLightTheme;
|
const lightColorSentMessageBg = 'hsl(152, 100%, 40%)';
|
||||||
const lightColorSentMessageText = white;
|
const lightColorSentMessageText = white;
|
||||||
const lightColorClickableHovered = '#dfdfdf';
|
const lightColorClickableHovered = '#dfdfdf';
|
||||||
const lightColorSessionBorderColor = borderLightThemeColor;
|
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 { getConversationController } from '../../session/conversations';
|
||||||
import { getFirstUnreadMessageIdInConversation, getMessagesByConversation } from '../../data/data';
|
import { getFirstUnreadMessageIdInConversation, getMessagesByConversation } from '../../data/data';
|
||||||
import {
|
import {
|
||||||
CallState,
|
|
||||||
ConversationNotificationSettingType,
|
ConversationNotificationSettingType,
|
||||||
ConversationTypeEnum,
|
ConversationTypeEnum,
|
||||||
} from '../../models/conversation';
|
} from '../../models/conversation';
|
||||||
|
@ -254,7 +253,6 @@ export interface ReduxConversationType {
|
||||||
|
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
isApproved?: boolean;
|
isApproved?: boolean;
|
||||||
callState?: CallState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NotificationForConvoOption {
|
export interface NotificationForConvoOption {
|
||||||
|
@ -278,7 +276,6 @@ export type ConversationsStateType = {
|
||||||
quotedMessage?: ReplyingToMessageProps;
|
quotedMessage?: ReplyingToMessageProps;
|
||||||
areMoreMessagesBeingFetched: boolean;
|
areMoreMessagesBeingFetched: boolean;
|
||||||
haveDoneFirstScroll: boolean;
|
haveDoneFirstScroll: boolean;
|
||||||
callIsInFullScreen: boolean;
|
|
||||||
|
|
||||||
showScrollButton: boolean;
|
showScrollButton: boolean;
|
||||||
animateQuotedMessageId?: string;
|
animateQuotedMessageId?: string;
|
||||||
|
@ -373,7 +370,6 @@ export function getEmptyConversationState(): ConversationsStateType {
|
||||||
mentionMembers: [],
|
mentionMembers: [],
|
||||||
firstUnreadMessageId: undefined,
|
firstUnreadMessageId: undefined,
|
||||||
haveDoneFirstScroll: false,
|
haveDoneFirstScroll: false,
|
||||||
callIsInFullScreen: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -699,7 +695,6 @@ const conversationsSlice = createSlice({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
conversationLookup: state.conversationLookup,
|
conversationLookup: state.conversationLookup,
|
||||||
callIsInFullScreen: state.callIsInFullScreen,
|
|
||||||
|
|
||||||
selectedConversation: action.payload.id,
|
selectedConversation: action.payload.id,
|
||||||
areMoreMessagesBeingFetched: false,
|
areMoreMessagesBeingFetched: false,
|
||||||
|
@ -763,102 +758,6 @@ const conversationsSlice = createSlice({
|
||||||
state.mentionMembers = action.payload;
|
state.mentionMembers = action.payload;
|
||||||
return state;
|
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) => {
|
extraReducers: (builder: any) => {
|
||||||
// Add reducers for additional action types here, and handle loading state as needed
|
// Add reducers for additional action types here, and handle loading state as needed
|
||||||
|
@ -918,13 +817,6 @@ export const {
|
||||||
quotedMessageToAnimate,
|
quotedMessageToAnimate,
|
||||||
setNextMessageToPlayId,
|
setNextMessageToPlayId,
|
||||||
updateMentionsMembers,
|
updateMentionsMembers,
|
||||||
// calls
|
|
||||||
incomingCall,
|
|
||||||
endCall,
|
|
||||||
answerCall,
|
|
||||||
callConnected,
|
|
||||||
startingCallWith,
|
|
||||||
setFullScreenCall,
|
|
||||||
} = actions;
|
} = actions;
|
||||||
|
|
||||||
export async function openConversationWithMessages(args: {
|
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 theme, ThemeStateType } from './ducks/theme';
|
||||||
import { reducer as section, SectionStateType } from './ducks/section';
|
import { reducer as section, SectionStateType } from './ducks/section';
|
||||||
import { defaultRoomReducer as defaultRooms, DefaultRoomsState } from './ducks/defaultRooms';
|
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 { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion';
|
||||||
import { modalReducer as modals, ModalState } from './ducks/modalDialog';
|
import { modalReducer as modals, ModalState } from './ducks/modalDialog';
|
||||||
|
@ -28,6 +29,7 @@ export type StateType = {
|
||||||
userConfig: UserConfigState;
|
userConfig: UserConfigState;
|
||||||
timerOptions: TimerOptionsState;
|
timerOptions: TimerOptionsState;
|
||||||
stagedAttachments: StagedAttachmentsStateType;
|
stagedAttachments: StagedAttachmentsStateType;
|
||||||
|
call: CallStateType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reducers = {
|
export const reducers = {
|
||||||
|
@ -42,6 +44,7 @@ export const reducers = {
|
||||||
userConfig,
|
userConfig,
|
||||||
timerOptions,
|
timerOptions,
|
||||||
stagedAttachments,
|
stagedAttachments,
|
||||||
|
call,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Making this work would require that our reducer signature supported AnyAction, not
|
// 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(
|
export const getIsTypingEnabled = createSelector(
|
||||||
getConversations,
|
getConversations,
|
||||||
getSelectedConversationKey,
|
getSelectedConversationKey,
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { SessionConversation } from '../../components/session/conversation/Sessi
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
import { getTheme } from '../selectors/theme';
|
import { getTheme } from '../selectors/theme';
|
||||||
import {
|
import {
|
||||||
getHasOngoingCallWithFocusedConvo,
|
|
||||||
getLightBoxOptions,
|
getLightBoxOptions,
|
||||||
getSelectedConversation,
|
getSelectedConversation,
|
||||||
getSelectedConversationKey,
|
getSelectedConversationKey,
|
||||||
|
@ -15,6 +14,7 @@ import {
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
import { getOurNumber } from '../selectors/user';
|
import { getOurNumber } from '../selectors/user';
|
||||||
import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments';
|
import { getStagedAttachmentsForCurrentConversation } from '../selectors/stagedAttachments';
|
||||||
|
import { getHasOngoingCallWithFocusedConvo } from '../selectors/call';
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType) => {
|
const mapStateToProps = (state: StateType) => {
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in New Issue