cleanup SessionCompositionBox
This commit is contained in:
parent
3741e96c61
commit
f91ed7729b
|
@ -128,7 +128,7 @@ const SelectionOverlay = () => {
|
|||
const TripleDotsMenu = (props: { triggerId: string; showBackButton: boolean }) => {
|
||||
const { showBackButton } = props;
|
||||
if (showBackButton) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -157,7 +157,7 @@ export const QuoteGenericFile = (
|
|||
const { attachment, isIncoming } = props;
|
||||
|
||||
if (!attachment) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
const { fileName, contentType } = attachment;
|
||||
|
@ -167,7 +167,7 @@ export const QuoteGenericFile = (
|
|||
!MIME.isAudio(contentType);
|
||||
|
||||
if (!isGenericFile) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -22,7 +22,7 @@ export const StagedLinkPreview = (props: Props) => {
|
|||
|
||||
const isImage = image && isImageAttachment(image);
|
||||
if (isLoaded && !(title && domain)) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
const isLoading = !isLoaded;
|
||||
|
|
|
@ -26,7 +26,7 @@ const TypingBubbleContainer = styled.div<TypingBubbleProps>`
|
|||
|
||||
export const TypingBubble = (props: TypingBubbleProps) => {
|
||||
if (props.conversationType === ConversationTypeEnum.GROUP) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!props.isTyping) {
|
||||
|
|
|
@ -193,7 +193,7 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
|
|||
const { zombies } = this.state;
|
||||
|
||||
if (!zombies.length) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
const zombieElements = zombies.map((member: ContactType, index: number) => {
|
||||
|
|
|
@ -274,7 +274,7 @@ export const ActionsPanel = () => {
|
|||
|
||||
if (!ourPrimaryConversation) {
|
||||
window?.log?.warn('ActionsPanel: ourPrimaryConversation is not set');
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
useInterval(() => {
|
||||
|
|
|
@ -50,7 +50,7 @@ export class SessionInboxView extends React.Component<any, State> {
|
|||
|
||||
public render() {
|
||||
if (!this.state.isInitialLoadComplete) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
const persistor = persistStore(this.store);
|
||||
|
|
|
@ -162,7 +162,7 @@ export const SessionJoinableRooms = (props: { onRoomClicked: () => void }) => {
|
|||
|
||||
if (!joinableRooms.inProgress && !joinableRooms.rooms?.length) {
|
||||
window?.log?.info('no default joinable rooms yet and not in progress');
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
const componentToRender = joinableRooms.inProgress ? (
|
||||
|
|
|
@ -4,9 +4,9 @@ import classNames from 'classnames';
|
|||
|
||||
import {
|
||||
SendMessageType,
|
||||
SessionCompositionBox,
|
||||
CompositionBox,
|
||||
StagedAttachmentType,
|
||||
} from './SessionCompositionBox';
|
||||
} from './composition/CompositionBox';
|
||||
|
||||
import { Constants } from '../../../session';
|
||||
import _ from 'lodash';
|
||||
|
@ -41,7 +41,6 @@ import { SplitViewContainer } from '../SplitViewContainer';
|
|||
// tslint:disable: jsx-curly-spacing
|
||||
|
||||
interface State {
|
||||
showRecordingView: boolean;
|
||||
isDraggingFile: boolean;
|
||||
}
|
||||
export interface LightBoxOptions {
|
||||
|
@ -75,7 +74,6 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
showRecordingView: false,
|
||||
isDraggingFile: false,
|
||||
};
|
||||
this.messageContainerRef = React.createRef();
|
||||
|
@ -135,7 +133,6 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
}
|
||||
if (newConversationKey !== oldConversationKey) {
|
||||
this.setState({
|
||||
showRecordingView: false,
|
||||
isDraggingFile: false,
|
||||
});
|
||||
}
|
||||
|
@ -247,11 +244,9 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
{isDraggingFile && <SessionFileDropzone />}
|
||||
</div>
|
||||
|
||||
<SessionCompositionBox
|
||||
<CompositionBox
|
||||
sendMessage={this.sendMessageFn}
|
||||
stagedAttachments={this.props.stagedAttachments}
|
||||
onLoadVoiceNoteView={this.onLoadVoiceNoteView}
|
||||
onExitVoiceNoteView={this.onExitVoiceNoteView}
|
||||
onChoseAttachments={this.onChoseAttachments}
|
||||
/>
|
||||
</div>
|
||||
|
@ -264,35 +259,12 @@ export class SessionConversation extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// ~~~~~~~~~~~~ MICROPHONE METHODS ~~~~~~~~~~~~
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
private onLoadVoiceNoteView() {
|
||||
this.setState({
|
||||
showRecordingView: true,
|
||||
});
|
||||
window.inboxStore?.dispatch(resetSelectedMessageIds());
|
||||
}
|
||||
|
||||
private onExitVoiceNoteView() {
|
||||
this.setState({
|
||||
showRecordingView: false,
|
||||
});
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// ~~~~~~~~~~~ KEYBOARD NAVIGATION ~~~~~~~~~~~~
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
private onKeyDown(event: any) {
|
||||
const selectionMode = !!this.props.selectedMessages.length;
|
||||
const recordingMode = this.state.showRecordingView;
|
||||
if (event.key === 'Escape') {
|
||||
// EXIT MEDIA VIEW
|
||||
if (recordingMode) {
|
||||
// EXIT RECORDING VIEW
|
||||
}
|
||||
// EXIT WHAT ELSE?
|
||||
}
|
||||
|
||||
if (event.target.classList.contains('conversation-content')) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
// keep this draft state local to not have to do a redux state update (a bit slow with our large state for some computers)
|
||||
const draftsForConversations: Record<string, string> = {};
|
||||
|
||||
export function getDraftForConversation(conversationKey?: string) {
|
||||
if (!conversationKey || !draftsForConversations[conversationKey]) {
|
||||
return '';
|
||||
}
|
||||
return draftsForConversations[conversationKey] || '';
|
||||
}
|
||||
|
||||
export function updateDraftForConversation({
|
||||
conversationKey,
|
||||
draft,
|
||||
}: {
|
||||
conversationKey: string;
|
||||
draft: string;
|
||||
}) {
|
||||
draftsForConversations[conversationKey] = draft;
|
||||
}
|
|
@ -10,9 +10,9 @@ import MicRecorder from 'mic-recorder-to-mp3';
|
|||
import styled from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
onExitVoiceNoteView: any;
|
||||
onLoadVoiceNoteView: any;
|
||||
sendVoiceMessage: any;
|
||||
onExitVoiceNoteView: () => void;
|
||||
onLoadVoiceNoteView: () => void;
|
||||
sendVoiceMessage: (audioBlob: Blob) => Promise<void>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -24,13 +24,10 @@ interface State {
|
|||
actionHover: boolean;
|
||||
startTimestamp: number;
|
||||
nowTimestamp: number;
|
||||
|
||||
updateTimerInterval: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
function getTimestamp(asInt = false) {
|
||||
const timestamp = Date.now() / 1000;
|
||||
return asInt ? Math.floor(timestamp) : timestamp;
|
||||
function getTimestamp() {
|
||||
return Date.now() / 1000;
|
||||
}
|
||||
|
||||
interface StyledFlexWrapperProps {
|
||||
|
@ -50,20 +47,16 @@ const StyledFlexWrapper = styled.div<StyledFlexWrapperProps>`
|
|||
}
|
||||
`;
|
||||
|
||||
class SessionRecordingInner extends React.Component<Props, State> {
|
||||
private recorder: any;
|
||||
export class SessionRecording extends React.Component<Props, State> {
|
||||
private recorder?: any;
|
||||
private audioBlobMp3?: Blob;
|
||||
private audioElement?: HTMLAudioElement | null;
|
||||
private updateTimerInterval?: NodeJS.Timeout;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
autoBind(this);
|
||||
|
||||
// Refs
|
||||
|
||||
const now = getTimestamp();
|
||||
const updateTimerInterval = global.setInterval(this.timerUpdate, 500);
|
||||
|
||||
this.state = {
|
||||
recordDuration: 0,
|
||||
|
@ -73,7 +66,6 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
actionHover: false,
|
||||
startTimestamp: now,
|
||||
nowTimestamp: now,
|
||||
updateTimerInterval,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -86,10 +78,13 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
if (this.props.onLoadVoiceNoteView) {
|
||||
this.props.onLoadVoiceNoteView();
|
||||
}
|
||||
this.updateTimerInterval = global.setInterval(this.timerUpdate, 500);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
clearInterval(this.state.updateTimerInterval);
|
||||
if (this.updateTimerInterval) {
|
||||
clearInterval(this.updateTimerInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
|
@ -276,7 +271,7 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.sendVoiceMessage(this.audioBlobMp3);
|
||||
void this.props.sendVoiceMessage(this.audioBlobMp3);
|
||||
}
|
||||
|
||||
private async initiateRecordingStream() {
|
||||
|
@ -348,5 +343,3 @@ class SessionRecordingInner extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const SessionRecording = SessionRecordingInner;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { arrayBufferFromFile } from '../../../types/Attachment';
|
||||
import { AttachmentUtil, LinkPreviewUtil } from '../../../util';
|
||||
import { StagedLinkPreviewData } from './SessionCompositionBox';
|
||||
import { StagedLinkPreviewData } from './composition/CompositionBox';
|
||||
import { default as insecureNodeFetch } from 'node-fetch';
|
||||
import { fetchLinkPreviewImage } from '../../../util/linkPreviewFetch';
|
||||
import { AbortSignal } from 'abort-controller';
|
||||
|
@ -107,7 +107,7 @@ export const getPreview = async (
|
|||
|
||||
export const SessionStagedLinkPreview = (props: StagedLinkPreviewProps) => {
|
||||
if (!props.url) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,56 +1,52 @@
|
|||
import React from 'react';
|
||||
import _, { debounce } from 'lodash';
|
||||
|
||||
import { AttachmentType } from '../../../types/Attachment';
|
||||
import * as MIME from '../../../types/MIME';
|
||||
import { AttachmentType } from '../../../../types/Attachment';
|
||||
import * as MIME from '../../../../types/MIME';
|
||||
|
||||
import { SessionIconButton } from '../icon';
|
||||
import { SessionEmojiPanel } from './SessionEmojiPanel';
|
||||
import { SessionRecording } from './SessionRecording';
|
||||
import { SessionEmojiPanel } from '../SessionEmojiPanel';
|
||||
import { SessionRecording } from '../SessionRecording';
|
||||
|
||||
import { Constants } from '../../../session';
|
||||
import { Constants } from '../../../../session';
|
||||
|
||||
import { toArray } from 'react-emoji-render';
|
||||
import { Flex } from '../../basic/Flex';
|
||||
import { StagedAttachmentList } from '../../conversation/StagedAttachmentList';
|
||||
import { ToastUtils } from '../../../session/utils';
|
||||
import { AttachmentUtil } from '../../../util';
|
||||
import { Flex } from '../../../basic/Flex';
|
||||
import { StagedAttachmentList } from '../../../conversation/StagedAttachmentList';
|
||||
import { ToastUtils } from '../../../../session/utils';
|
||||
import { AttachmentUtil } from '../../../../util';
|
||||
import {
|
||||
getPreview,
|
||||
LINK_PREVIEW_TIMEOUT,
|
||||
SessionStagedLinkPreview,
|
||||
} from './SessionStagedLinkPreview';
|
||||
} from '../SessionStagedLinkPreview';
|
||||
import { AbortController } from 'abort-controller';
|
||||
import { SessionQuotedMessageComposition } from './SessionQuotedMessageComposition';
|
||||
import { SessionQuotedMessageComposition } from '../SessionQuotedMessageComposition';
|
||||
import { Mention, MentionsInput } from 'react-mentions';
|
||||
import { CaptionEditor } from '../../CaptionEditor';
|
||||
import { getConversationController } from '../../../session/conversations';
|
||||
import { ReduxConversationType } from '../../../state/ducks/conversations';
|
||||
import { SessionMemberListItem } from '../SessionMemberListItem';
|
||||
import { CaptionEditor } from '../../../CaptionEditor';
|
||||
import { getConversationController } from '../../../../session/conversations';
|
||||
import { ReduxConversationType } from '../../../../state/ducks/conversations';
|
||||
import { SessionMemberListItem } from '../../SessionMemberListItem';
|
||||
import autoBind from 'auto-bind';
|
||||
import { getMediaPermissionsSettings, SessionSettingCategory } from '../settings/SessionSettings';
|
||||
import { updateConfirmModal } from '../../../state/ducks/modalDialog';
|
||||
import {
|
||||
SectionType,
|
||||
showLeftPaneSection,
|
||||
showSettingsSection,
|
||||
} from '../../../state/ducks/section';
|
||||
import { SessionButtonColor } from '../SessionButton';
|
||||
import {
|
||||
createOrUpdateItem,
|
||||
getItemById,
|
||||
hasLinkPreviewPopupBeenDisplayed,
|
||||
} from '../../../data/data';
|
||||
import { getMediaPermissionsSettings } from '../../settings/SessionSettings';
|
||||
import {
|
||||
getIsTypingEnabled,
|
||||
getMentionsInput,
|
||||
getQuotedMessage,
|
||||
getSelectedConversation,
|
||||
getSelectedConversationKey,
|
||||
} from '../../../state/selectors/conversations';
|
||||
} from '../../../../state/selectors/conversations';
|
||||
import { connect } from 'react-redux';
|
||||
import { StateType } from '../../../state/reducer';
|
||||
import { getTheme } from '../../../state/selectors/theme';
|
||||
import { removeAllStagedAttachmentsInConversation } from '../../../state/ducks/stagedAttachments';
|
||||
import { StateType } from '../../../../state/reducer';
|
||||
import { getTheme } from '../../../../state/selectors/theme';
|
||||
import { removeAllStagedAttachmentsInConversation } from '../../../../state/ducks/stagedAttachments';
|
||||
import { getDraftForConversation, updateDraftForConversation } from '../SessionConversationDrafts';
|
||||
import { showLinkSharingConfirmationModalDialog } from '../../../../interactions/conversationInteractions';
|
||||
import {
|
||||
AddStagedAttachmentButton,
|
||||
StartRecordingButton,
|
||||
ToggleEmojiButton,
|
||||
SendMessageButton,
|
||||
} from './CompositionButtons';
|
||||
|
||||
export interface ReplyingToMessageProps {
|
||||
convoId: string;
|
||||
|
@ -83,79 +79,11 @@ export type SendMessageType = {
|
|||
groupInvitation: { url: string | undefined; name: string } | undefined;
|
||||
};
|
||||
|
||||
const AddStagedAttachmentButton = (props: { onClick: () => void }) => {
|
||||
return (
|
||||
<SessionIconButton
|
||||
iconType="plusThin"
|
||||
backgroundColor={'var(--color-compose-view-button-background)'}
|
||||
iconSize={'huge2'}
|
||||
borderRadius="300px"
|
||||
iconPadding="8px"
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const StartRecordingButton = (props: { onClick: () => void }) => {
|
||||
return (
|
||||
<SessionIconButton
|
||||
iconType="microphone"
|
||||
iconSize={'huge2'}
|
||||
backgroundColor={'var(--color-compose-view-button-background)'}
|
||||
borderRadius="300px"
|
||||
iconPadding="6px"
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ToggleEmojiButton = React.forwardRef<HTMLDivElement, { onClick: () => void }>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<SessionIconButton
|
||||
iconType="emoji"
|
||||
ref={ref}
|
||||
backgroundColor="var(--color-compose-view-button-background)"
|
||||
iconSize={'huge2'}
|
||||
borderRadius="300px"
|
||||
iconPadding="6px"
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const SendMessageButton = (props: { onClick: () => void }) => {
|
||||
return (
|
||||
<div className="send-message-button">
|
||||
<SessionIconButton
|
||||
iconType="send"
|
||||
backgroundColor={'var(--color-compose-view-button-background)'}
|
||||
iconSize={'huge2'}
|
||||
iconRotation={90}
|
||||
borderRadius="300px"
|
||||
iconPadding="6px"
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// keep this draft state local to not have to do a redux state update (a bit slow with our large state for soem computers)
|
||||
const draftsForConversations: Array<{ conversationKey: string; draft: string }> = new Array();
|
||||
function updateDraftForConversation(action: { conversationKey: string; draft: string }) {
|
||||
const { conversationKey, draft } = action;
|
||||
const foundAtIndex = draftsForConversations.findIndex(c => c.conversationKey === conversationKey);
|
||||
foundAtIndex === -1
|
||||
? draftsForConversations.push({ conversationKey, draft })
|
||||
: (draftsForConversations[foundAtIndex] = action);
|
||||
}
|
||||
interface Props {
|
||||
sendMessage: (msg: SendMessageType) => void;
|
||||
onLoadVoiceNoteView: any;
|
||||
onExitVoiceNoteView: any;
|
||||
selectedConversationKey: string;
|
||||
selectedConversation: ReduxConversationType | undefined;
|
||||
typingEnabled: boolean;
|
||||
quotedMessageProps?: ReplyingToMessageProps;
|
||||
stagedAttachments: Array<StagedAttachmentType>;
|
||||
onChoseAttachments: (newAttachments: Array<File>) => void;
|
||||
|
@ -165,8 +93,7 @@ interface State {
|
|||
showRecordingView: boolean;
|
||||
draft: string;
|
||||
showEmojiPanel: boolean;
|
||||
voiceRecording?: Blob;
|
||||
ignoredLink?: string; // set the the ignored url when users closed the link preview
|
||||
ignoredLink?: string; // set the ignored url when users closed the link preview
|
||||
stagedLinkPreview?: StagedLinkPreviewData;
|
||||
showCaptionEditor?: AttachmentType;
|
||||
}
|
||||
|
@ -191,12 +118,10 @@ const sendMessageStyle = {
|
|||
minHeight: '24px',
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
const getDefaultState = (newConvoId?: string) => {
|
||||
return {
|
||||
draft:
|
||||
(newConvoId && draftsForConversations.find(c => c.conversationKey === newConvoId)?.draft) ||
|
||||
'',
|
||||
voiceRecording: undefined,
|
||||
draft: getDraftForConversation(newConvoId),
|
||||
showRecordingView: false,
|
||||
showEmojiPanel: false,
|
||||
ignoredLink: undefined,
|
||||
|
@ -205,14 +130,84 @@ const getDefaultState = (newConvoId?: string) => {
|
|||
};
|
||||
};
|
||||
|
||||
class SessionCompositionBoxInner extends React.Component<Props, State> {
|
||||
function parseEmojis(value: string) {
|
||||
const emojisArray = toArray(value);
|
||||
|
||||
// toArray outputs React elements for emojis and strings for other
|
||||
return emojisArray.reduce((previous: string, current: any) => {
|
||||
if (typeof current === 'string') {
|
||||
return previous + current;
|
||||
}
|
||||
return previous + (current.props.children as string);
|
||||
}, '');
|
||||
}
|
||||
|
||||
const mentionsRegex = /@\uFFD205[0-9a-f]{64}\uFFD7[^\uFFD2]+\uFFD2/gu;
|
||||
|
||||
const getSelectionBasedOnMentions = (draft: string, index: number) => {
|
||||
// we have to get the real selectionStart/end of an index in the mentions box.
|
||||
// this is kind of a pain as the mentions box has two inputs, one with the real text, and one with the extracted mentions
|
||||
|
||||
// the index shown to the user is actually just the visible part of the mentions (so the part between ᅲ...ᅭ
|
||||
const matches = draft.match(mentionsRegex);
|
||||
|
||||
let lastMatchStartIndex = 0;
|
||||
let lastMatchEndIndex = 0;
|
||||
let lastRealMatchEndIndex = 0;
|
||||
|
||||
if (!matches) {
|
||||
return index;
|
||||
}
|
||||
const mapStartToLengthOfMatches = matches.map(match => {
|
||||
const displayNameStart = match.indexOf('\uFFD7') + 1;
|
||||
const displayNameEnd = match.lastIndexOf('\uFFD2');
|
||||
const displayName = match.substring(displayNameStart, displayNameEnd);
|
||||
|
||||
const currentMatchStartIndex = draft.indexOf(match) + lastMatchStartIndex;
|
||||
lastMatchStartIndex = currentMatchStartIndex;
|
||||
lastMatchEndIndex = currentMatchStartIndex + match.length;
|
||||
|
||||
const realLength = displayName.length + 1;
|
||||
lastRealMatchEndIndex = lastRealMatchEndIndex + realLength;
|
||||
|
||||
// the +1 is for the @
|
||||
return {
|
||||
length: displayName.length + 1,
|
||||
lastRealMatchEndIndex,
|
||||
start: lastMatchStartIndex,
|
||||
end: lastMatchEndIndex,
|
||||
};
|
||||
});
|
||||
|
||||
const beforeFirstMatch = index < mapStartToLengthOfMatches[0].start;
|
||||
if (beforeFirstMatch) {
|
||||
// those first char are always just char, so the mentions logic does not come into account
|
||||
return index;
|
||||
}
|
||||
const lastMatchMap = _.last(mapStartToLengthOfMatches);
|
||||
|
||||
if (!lastMatchMap) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
const indexIsAfterEndOfLastMatch = lastMatchMap.lastRealMatchEndIndex <= index;
|
||||
if (indexIsAfterEndOfLastMatch) {
|
||||
const lastEnd = lastMatchMap.end;
|
||||
const diffBetweenEndAndLastRealEnd = index - lastMatchMap.lastRealMatchEndIndex;
|
||||
return lastEnd + diffBetweenEndAndLastRealEnd - 1;
|
||||
}
|
||||
// now this is the hard part, the cursor is currently between the end of the first match and the start of the last match
|
||||
// for now, just append it to the end
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
};
|
||||
|
||||
class CompositionBoxInner extends React.Component<Props, State> {
|
||||
private readonly textarea: React.RefObject<any>;
|
||||
private readonly fileInput: React.RefObject<HTMLInputElement>;
|
||||
private readonly emojiPanel: any;
|
||||
private readonly emojiPanel: React.RefObject<HTMLDivElement>;
|
||||
private readonly emojiPanelButton: any;
|
||||
private linkPreviewAbortController?: AbortController;
|
||||
private container: any;
|
||||
private readonly mentionsRegex = /@\uFFD205[0-9a-f]{64}\uFFD7[^\uFFD2]+\uFFD2/gu;
|
||||
private container: HTMLDivElement | null;
|
||||
private lastBumpTypingMessageLength: number = 0;
|
||||
|
||||
constructor(props: any) {
|
||||
|
@ -222,6 +217,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
this.textarea = React.createRef();
|
||||
this.fileInput = React.createRef();
|
||||
|
||||
this.container = null;
|
||||
// Emojis
|
||||
this.emojiPanel = React.createRef();
|
||||
this.emojiPanelButton = React.createRef();
|
||||
|
@ -286,10 +282,13 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
this.hideEmojiPanel();
|
||||
}
|
||||
|
||||
private handlePaste(e: any) {
|
||||
private handlePaste(e: ClipboardEvent) {
|
||||
if (!e.clipboardData) {
|
||||
return;
|
||||
}
|
||||
const { items } = e.clipboardData;
|
||||
let imgBlob = null;
|
||||
for (const item of items) {
|
||||
for (const item of items as any) {
|
||||
const pasteType = item.type.split('/')[0];
|
||||
if (pasteType === 'image') {
|
||||
imgBlob = item.getAsFile();
|
||||
|
@ -300,7 +299,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
imgBlob = item.getAsFile();
|
||||
break;
|
||||
case 'text':
|
||||
void this.showLinkSharingConfirmationModalDialog(e);
|
||||
void showLinkSharingConfirmationModalDialog(e);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
@ -315,47 +314,6 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if what is pasted is a URL and prompt confirmation for a setting change
|
||||
* @param e paste event
|
||||
*/
|
||||
private async showLinkSharingConfirmationModalDialog(e: any) {
|
||||
const pastedText = e.clipboardData.getData('text');
|
||||
if (this.isURL(pastedText) && !window.getSettingValue('link-preview-setting', false)) {
|
||||
const alreadyDisplayedPopup =
|
||||
(await getItemById(hasLinkPreviewPopupBeenDisplayed))?.value || false;
|
||||
if (!alreadyDisplayedPopup) {
|
||||
window.inboxStore?.dispatch(
|
||||
updateConfirmModal({
|
||||
shouldShowConfirm:
|
||||
!window.getSettingValue('link-preview-setting') && !alreadyDisplayedPopup,
|
||||
title: window.i18n('linkPreviewsTitle'),
|
||||
message: window.i18n('linkPreviewsConfirmMessage'),
|
||||
okTheme: SessionButtonColor.Danger,
|
||||
onClickOk: () => {
|
||||
window.setSettingValue('link-preview-setting', true);
|
||||
},
|
||||
onClickClose: async () => {
|
||||
await createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: true });
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param str String to evaluate
|
||||
* @returns boolean if the string is true or false
|
||||
*/
|
||||
private isURL(str: string) {
|
||||
const urlRegex =
|
||||
'^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$';
|
||||
const url = new RegExp(urlRegex, 'i');
|
||||
return str.length < 2083 && url.test(str);
|
||||
}
|
||||
|
||||
private showEmojiPanel() {
|
||||
document.addEventListener('mousedown', this.handleClick, false);
|
||||
|
||||
|
@ -390,18 +348,9 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
private isTypingEnabled(): boolean {
|
||||
if (!this.props.selectedConversation) {
|
||||
return false;
|
||||
}
|
||||
const { isBlocked, isKickedFromGroup, left } = this.props.selectedConversation;
|
||||
|
||||
return !(isBlocked || isKickedFromGroup || left);
|
||||
}
|
||||
|
||||
private renderCompositionView() {
|
||||
const { showEmojiPanel } = this.state;
|
||||
const typingEnabled = this.isTypingEnabled();
|
||||
const { typingEnabled } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -463,7 +412,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
: isBlocked && !isPrivate
|
||||
? i18n('unblockGroupToSend')
|
||||
: i18n('sendMessage');
|
||||
const typingEnabled = this.isTypingEnabled();
|
||||
const { typingEnabled } = this.props;
|
||||
let index = 0;
|
||||
|
||||
return (
|
||||
|
@ -478,7 +427,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
disabled={!typingEnabled}
|
||||
rows={1}
|
||||
style={sendMessageStyle}
|
||||
suggestionsPortalHost={this.container}
|
||||
suggestionsPortalHost={this.container as any}
|
||||
forceSuggestionsAboveCursor={true} // force mentions to be rendered on top of the cursor, this is working with a fork of react-mentions for now
|
||||
>
|
||||
<Mention
|
||||
|
@ -585,10 +534,10 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
callback(mentionsData);
|
||||
}
|
||||
|
||||
private renderStagedLinkPreview(): JSX.Element {
|
||||
private renderStagedLinkPreview(): JSX.Element | null {
|
||||
// Don't generate link previews if user has turned them off
|
||||
if (!(window.getSettingValue('link-preview-setting') || false)) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
const { stagedAttachments, quotedMessageProps } = this.props;
|
||||
|
@ -596,7 +545,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
|
||||
// Don't render link previews if quoted message or attachments are already added
|
||||
if (stagedAttachments.length !== 0 || quotedMessageProps?.id) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
// we try to match the first link found in the current message
|
||||
const links = window.Signal.LinkPreviews.findLinks(this.state.draft, undefined);
|
||||
|
@ -606,7 +555,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
stagedLinkPreview: undefined,
|
||||
});
|
||||
}
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
const firstLink = links[0];
|
||||
// if the first link changed, reset the ignored link so that the preview is generated
|
||||
|
@ -620,7 +569,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
|
||||
// if the fetch did not start yet, just don't show anything
|
||||
if (!this.state.stagedLinkPreview) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isLoaded, title, description, domain, image } = this.state.stagedLinkPreview;
|
||||
|
@ -767,7 +716,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
/>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
private renderAttachmentsStaged() {
|
||||
|
@ -785,7 +734,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
</>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
private onChooseAttachment() {
|
||||
|
@ -838,25 +787,13 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
private parseEmojis(value: string) {
|
||||
const emojisArray = toArray(value);
|
||||
|
||||
// toArray outputs React elements for emojis and strings for other
|
||||
return emojisArray.reduce((previous: string, current: any) => {
|
||||
if (typeof current === 'string') {
|
||||
return previous + current;
|
||||
}
|
||||
return previous + (current.props.children as string);
|
||||
}, '');
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: cyclomatic-complexity
|
||||
private async onSendMessage() {
|
||||
this.abortLinkPreviewFetch();
|
||||
|
||||
// this is dirty but we have to replace all @(xxx) by @xxx manually here
|
||||
const cleanMentions = (text: string): string => {
|
||||
const matches = text.match(this.mentionsRegex);
|
||||
const matches = text.match(mentionsRegex);
|
||||
let replacedMentions = text;
|
||||
(matches || []).forEach(match => {
|
||||
const replacedMention = match.substring(2, match.indexOf('\uFFD7'));
|
||||
|
@ -866,7 +803,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
return replacedMentions;
|
||||
};
|
||||
|
||||
const messagePlaintext = cleanMentions(this.parseEmojis(this.state.draft));
|
||||
const messagePlaintext = cleanMentions(parseEmojis(this.state.draft));
|
||||
|
||||
const { selectedConversation } = this.props;
|
||||
|
||||
|
@ -1008,29 +945,18 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
private async onLoadVoiceNoteView() {
|
||||
// Do stuff for component, then run callback to SessionConversation
|
||||
const mediaSetting = getMediaPermissionsSettings();
|
||||
|
||||
if (mediaSetting) {
|
||||
this.setState({
|
||||
showRecordingView: true,
|
||||
showEmojiPanel: false,
|
||||
});
|
||||
this.props.onLoadVoiceNoteView();
|
||||
|
||||
if (!getMediaPermissionsSettings()) {
|
||||
ToastUtils.pushAudioPermissionNeeded();
|
||||
return;
|
||||
}
|
||||
|
||||
ToastUtils.pushAudioPermissionNeeded(() => {
|
||||
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
|
||||
window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
|
||||
this.setState({
|
||||
showRecordingView: true,
|
||||
showEmojiPanel: false,
|
||||
});
|
||||
}
|
||||
|
||||
private onExitVoiceNoteView() {
|
||||
// Do stuff for component, then run callback to SessionConversation
|
||||
this.setState({ showRecordingView: false });
|
||||
this.props.onExitVoiceNoteView();
|
||||
}
|
||||
|
||||
private onChange(event: any) {
|
||||
|
@ -1039,63 +965,6 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
updateDraftForConversation({ conversationKey: this.props.selectedConversationKey, draft });
|
||||
}
|
||||
|
||||
private getSelectionBasedOnMentions(index: number) {
|
||||
// we have to get the real selectionStart/end of an index in the mentions box.
|
||||
// this is kind of a pain as the mentions box has two inputs, one with the real text, and one with the extracted mentions
|
||||
|
||||
// the index shown to the user is actually just the visible part of the mentions (so the part between ᅲ...ᅭ
|
||||
const matches = this.state.draft.match(this.mentionsRegex);
|
||||
|
||||
let lastMatchStartIndex = 0;
|
||||
let lastMatchEndIndex = 0;
|
||||
let lastRealMatchEndIndex = 0;
|
||||
|
||||
if (!matches) {
|
||||
return index;
|
||||
}
|
||||
const mapStartToLengthOfMatches = matches.map(match => {
|
||||
const displayNameStart = match.indexOf('\uFFD7') + 1;
|
||||
const displayNameEnd = match.lastIndexOf('\uFFD2');
|
||||
const displayName = match.substring(displayNameStart, displayNameEnd);
|
||||
|
||||
const currentMatchStartIndex = this.state.draft.indexOf(match) + lastMatchStartIndex;
|
||||
lastMatchStartIndex = currentMatchStartIndex;
|
||||
lastMatchEndIndex = currentMatchStartIndex + match.length;
|
||||
|
||||
const realLength = displayName.length + 1;
|
||||
lastRealMatchEndIndex = lastRealMatchEndIndex + realLength;
|
||||
|
||||
// the +1 is for the @
|
||||
return {
|
||||
length: displayName.length + 1,
|
||||
lastRealMatchEndIndex,
|
||||
start: lastMatchStartIndex,
|
||||
end: lastMatchEndIndex,
|
||||
};
|
||||
});
|
||||
|
||||
const beforeFirstMatch = index < mapStartToLengthOfMatches[0].start;
|
||||
if (beforeFirstMatch) {
|
||||
// those first char are always just char, so the mentions logic does not come into account
|
||||
return index;
|
||||
}
|
||||
const lastMatchMap = _.last(mapStartToLengthOfMatches);
|
||||
|
||||
if (!lastMatchMap) {
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
const indexIsAfterEndOfLastMatch = lastMatchMap.lastRealMatchEndIndex <= index;
|
||||
if (indexIsAfterEndOfLastMatch) {
|
||||
const lastEnd = lastMatchMap.end;
|
||||
const diffBetweenEndAndLastRealEnd = index - lastMatchMap.lastRealMatchEndIndex;
|
||||
return lastEnd + diffBetweenEndAndLastRealEnd - 1;
|
||||
}
|
||||
// now this is the hard part, the cursor is currently between the end of the first match and the start of the last match
|
||||
// for now, just append it to the end
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
private onEmojiClick({ colons }: { colons: string }) {
|
||||
const messageBox = this.textarea.current;
|
||||
if (!messageBox) {
|
||||
|
@ -1106,7 +975,7 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
|
|||
|
||||
const currentSelectionStart = Number(messageBox.selectionStart);
|
||||
|
||||
const realSelectionStart = this.getSelectionBasedOnMentions(currentSelectionStart);
|
||||
const realSelectionStart = getSelectionBasedOnMentions(draft, currentSelectionStart);
|
||||
|
||||
const before = draft.slice(0, realSelectionStart);
|
||||
const end = draft.slice(realSelectionStart);
|
||||
|
@ -1146,10 +1015,11 @@ const mapStateToProps = (state: StateType) => {
|
|||
quotedMessageProps: getQuotedMessage(state),
|
||||
selectedConversation: getSelectedConversation(state),
|
||||
selectedConversationKey: getSelectedConversationKey(state),
|
||||
typingEnabled: getIsTypingEnabled(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps);
|
||||
|
||||
export const SessionCompositionBox = smart(SessionCompositionBoxInner);
|
||||
export const CompositionBox = smart(CompositionBoxInner);
|
|
@ -0,0 +1,60 @@
|
|||
import React from 'react';
|
||||
import { SessionIconButton } from '../../icon';
|
||||
|
||||
export const AddStagedAttachmentButton = (props: { onClick: () => void }) => {
|
||||
return (
|
||||
<SessionIconButton
|
||||
iconType="plusThin"
|
||||
backgroundColor={'var(--color-compose-view-button-background)'}
|
||||
iconSize={'huge2'}
|
||||
borderRadius="300px"
|
||||
iconPadding="8px"
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const StartRecordingButton = (props: { onClick: () => void }) => {
|
||||
return (
|
||||
<SessionIconButton
|
||||
iconType="microphone"
|
||||
iconSize={'huge2'}
|
||||
backgroundColor={'var(--color-compose-view-button-background)'}
|
||||
borderRadius="300px"
|
||||
iconPadding="6px"
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToggleEmojiButton = React.forwardRef<HTMLDivElement, { onClick: () => void }>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<SessionIconButton
|
||||
iconType="emoji"
|
||||
ref={ref}
|
||||
backgroundColor="var(--color-compose-view-button-background)"
|
||||
iconSize={'huge2'}
|
||||
borderRadius="300px"
|
||||
iconPadding="6px"
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const SendMessageButton = (props: { onClick: () => void }) => {
|
||||
return (
|
||||
<div className="send-message-button">
|
||||
<SessionIconButton
|
||||
iconType="send"
|
||||
backgroundColor={'var(--color-compose-view-button-background)'}
|
||||
iconSize={'huge2'}
|
||||
iconRotation={90}
|
||||
borderRadius="300px"
|
||||
iconPadding="6px"
|
||||
onClick={props.onClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -64,7 +64,7 @@ const SignInContinueButton = (props: {
|
|||
handleContinueYourSessionClick: () => any;
|
||||
}) => {
|
||||
if (props.signInMode === SignInMode.Default) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ContinueYourSessionButton
|
||||
|
@ -80,7 +80,7 @@ const SignInButtons = (props: {
|
|||
onLinkDeviceButtonClicked: () => any;
|
||||
}) => {
|
||||
if (props.signInMode !== SignInMode.Default) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
|
|
|
@ -22,7 +22,9 @@ import {
|
|||
} from '../state/ducks/modalDialog';
|
||||
import {
|
||||
createOrUpdateItem,
|
||||
getItemById,
|
||||
getMessageById,
|
||||
hasLinkPreviewPopupBeenDisplayed,
|
||||
lastAvatarUploadTimestamp,
|
||||
removeAllMessagesInConversation,
|
||||
} from '../data/data';
|
||||
|
@ -388,3 +390,44 @@ export async function replyToMessage(messageId: string) {
|
|||
window.inboxStore?.dispatch(quoteMessage(undefined));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if what is pasted is a URL and prompt confirmation for a setting change
|
||||
* @param e paste event
|
||||
*/
|
||||
export async function showLinkSharingConfirmationModalDialog(e: any) {
|
||||
const pastedText = e.clipboardData.getData('text');
|
||||
if (isURL(pastedText) && !window.getSettingValue('link-preview-setting', false)) {
|
||||
const alreadyDisplayedPopup =
|
||||
(await getItemById(hasLinkPreviewPopupBeenDisplayed))?.value || false;
|
||||
if (!alreadyDisplayedPopup) {
|
||||
window.inboxStore?.dispatch(
|
||||
updateConfirmModal({
|
||||
shouldShowConfirm:
|
||||
!window.getSettingValue('link-preview-setting') && !alreadyDisplayedPopup,
|
||||
title: window.i18n('linkPreviewsTitle'),
|
||||
message: window.i18n('linkPreviewsConfirmMessage'),
|
||||
okTheme: SessionButtonColor.Danger,
|
||||
onClickOk: () => {
|
||||
window.setSettingValue('link-preview-setting', true);
|
||||
},
|
||||
onClickClose: async () => {
|
||||
await createOrUpdateItem({ id: hasLinkPreviewPopupBeenDisplayed, value: true });
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param str String to evaluate
|
||||
* @returns boolean if the string is true or false
|
||||
*/
|
||||
function isURL(str: string) {
|
||||
const urlRegex =
|
||||
'^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$';
|
||||
const url = new RegExp(urlRegex, 'i');
|
||||
return str.length < 2083 && url.test(str);
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ import { perfEnd, perfStart } from '../session/utils/Performance';
|
|||
import {
|
||||
ReplyingToMessageProps,
|
||||
SendMessageType,
|
||||
} from '../components/session/conversation/SessionCompositionBox';
|
||||
} from '../components/session/conversation/composition/CompositionBox';
|
||||
import { ed25519Str } from '../session/onions/onionPath';
|
||||
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
|
||||
import { IMAGE_JPEG } from '../types/MIME';
|
||||
|
@ -180,8 +180,8 @@ export type CallState = 'offering' | 'incoming' | 'connecting' | 'ongoing' | 'no
|
|||
|
||||
export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
||||
public updateLastMessage: () => any;
|
||||
public throttledBumpTyping: any;
|
||||
public throttledNotify: any;
|
||||
public throttledBumpTyping: () => void;
|
||||
public throttledNotify: (message: MessageModel) => void;
|
||||
public markRead: (newestUnreadDate: number, providedOptions?: any) => Promise<void>;
|
||||
public initialPromise: any;
|
||||
|
||||
|
@ -192,7 +192,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
|
|||
private typingTimer?: NodeJS.Timeout | null;
|
||||
private lastReadTimestamp: number;
|
||||
|
||||
private pending: any;
|
||||
private pending?: Promise<any>;
|
||||
|
||||
constructor(attributes: ConversationAttributesOptionals) {
|
||||
super(fillConvoAttributesWithDefaults(attributes));
|
||||
|
|
|
@ -175,12 +175,15 @@ export function pushVideoCallPermissionNeeded() {
|
|||
);
|
||||
}
|
||||
|
||||
export function pushAudioPermissionNeeded(onClicked: () => void) {
|
||||
export function pushAudioPermissionNeeded() {
|
||||
pushToastInfo(
|
||||
'audioPermissionNeeded',
|
||||
window.i18n('audioPermissionNeededTitle'),
|
||||
window.i18n('audioPermissionNeeded'),
|
||||
onClicked
|
||||
() => {
|
||||
window.inboxStore?.dispatch(showLeftPaneSection(SectionType.Settings));
|
||||
window.inboxStore?.dispatch(showSettingsSection(SessionSettingCategory.Privacy));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
PropsForDataExtractionNotification,
|
||||
} from '../../models/messageType';
|
||||
import { LightBoxOptions } from '../../components/session/conversation/SessionConversation';
|
||||
import { ReplyingToMessageProps } from '../../components/session/conversation/SessionCompositionBox';
|
||||
import { ReplyingToMessageProps } from '../../components/session/conversation/composition/CompositionBox';
|
||||
import { QuotedAttachmentType } from '../../components/conversation/Quote';
|
||||
import { perfEnd, perfStart } from '../../session/utils/Performance';
|
||||
import { omit } from 'lodash';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { StagedAttachmentType } from '../../components/session/conversation/SessionCompositionBox';
|
||||
import { StagedAttachmentType } from '../../components/session/conversation/composition/CompositionBox';
|
||||
|
||||
export type StagedAttachmentsStateType = {
|
||||
stagedAttachments: { [conversationKey: string]: Array<StagedAttachmentType> };
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
ConversationHeaderTitleProps,
|
||||
} from '../../components/conversation/ConversationHeader';
|
||||
import { LightBoxOptions } from '../../components/session/conversation/SessionConversation';
|
||||
import { ReplyingToMessageProps } from '../../components/session/conversation/SessionCompositionBox';
|
||||
import { ReplyingToMessageProps } from '../../components/session/conversation/composition/CompositionBox';
|
||||
import { getConversationController } from '../../session/conversations';
|
||||
import { UserUtils } from '../../session/utils';
|
||||
import { MessageAvatarSelectorProps } from '../../components/conversation/message/MessageAvatar';
|
||||
|
@ -188,6 +188,22 @@ export const getCallIsInFullScreen = createSelector(
|
|||
(state: ConversationsStateType): boolean => state.callIsInFullScreen
|
||||
);
|
||||
|
||||
export const getIsTypingEnabled = createSelector(
|
||||
getConversations,
|
||||
getSelectedConversationKey,
|
||||
(state: ConversationsStateType, selectedConvoPubkey?: string): boolean => {
|
||||
if (!selectedConvoPubkey) {
|
||||
return false;
|
||||
}
|
||||
const selectedConvo = state.conversationLookup[selectedConvoPubkey];
|
||||
if (!selectedConvo) {
|
||||
return false;
|
||||
}
|
||||
const { isBlocked, isKickedFromGroup, left } = selectedConvo;
|
||||
|
||||
return !(isBlocked || isKickedFromGroup || left);
|
||||
}
|
||||
);
|
||||
/**
|
||||
* Returns true if the current conversation selected is a group conversation.
|
||||
* Returns false if the current conversation selected is not a group conversation, or none are selected
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { StagedAttachmentType } from '../../components/session/conversation/SessionCompositionBox';
|
||||
import { StagedAttachmentType } from '../../components/session/conversation/composition/CompositionBox';
|
||||
import { StagedAttachmentsStateType } from '../ducks/stagedAttachments';
|
||||
import { StateType } from '../reducer';
|
||||
import { getSelectedConversationKey } from './conversations';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { StagedAttachmentType } from '../components/session/conversation/SessionCompositionBox';
|
||||
import { StagedAttachmentType } from '../components/session/conversation/composition/CompositionBox';
|
||||
import { SignalService } from '../protobuf';
|
||||
import { Constants } from '../session';
|
||||
import loadImage from 'blueimp-load-image';
|
||||
|
|
Loading…
Reference in New Issue