diff --git a/js/ConversationController.d.ts b/js/ConversationController.d.ts index 4e5032a52..5025197e9 100644 --- a/js/ConversationController.d.ts +++ b/js/ConversationController.d.ts @@ -8,4 +8,6 @@ export type ConversationControllerType = { getOrCreateAndWait: (id: string, type: string) => Promise; getOrCreate: (id: string, type: string) => Promise; dangerouslyCreateAndAdd: (any) => any; + getContactProfileNameOrShortenedPubKey: (id: string) => string; + getContactProfileNameOrFullPubKey: (id: string) => string; }; diff --git a/js/models/conversations.d.ts b/js/models/conversations.d.ts index 108bfeaf6..bd0096731 100644 --- a/js/models/conversations.d.ts +++ b/js/models/conversations.d.ts @@ -62,6 +62,7 @@ export interface ConversationModel isClosable: () => boolean; isOnline: () => boolean; isModerator: (id?: string) => boolean; + throttledBumpTyping: () => void; lastMessage: string; messageCollection: Backbone.Collection; diff --git a/js/models/conversations.js b/js/models/conversations.js index dd27600ea..8f674b0eb 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -346,7 +346,7 @@ ); // send the message to a single recipient if this is a session chat - if (this.isPrivate) { + if (this.isPrivate()) { const device = new libsession.Types.PubKey(recipientId); libsession .getMessageQueue() diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 67ad81234..280df4b8f 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -850,19 +850,6 @@ } }, - onKeyUp() { - this.maybeBumpTyping(); - }, - - // Called whenever the user changes the message composition field. But only - // fires if there's content in the message field after the change. - maybeBumpTyping() { - const messageText = this.$messageField.val(); - if (messageText.length) { - this.model.throttledBumpTyping(); - } - }, - handleDeleteOrBackspace(event, isDelete) { const $input = this.$messageField[0]; const text = this.$messageField.val(); diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 3c655fff7..0aa1ec714 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -46,14 +46,6 @@ padding-inline-end: 10px; } -.session-message { - display: flow-root; - padding-bottom: 4px; - padding-top: 4px; - padding-inline-start: 16px; - padding-inline-end: 16px; -} - .group-invitation-container { display: flex; flex-direction: column; diff --git a/ts/components/conversation/TypingBubble.tsx b/ts/components/conversation/TypingBubble.tsx index 5f5f9efb0..036a47e54 100644 --- a/ts/components/conversation/TypingBubble.tsx +++ b/ts/components/conversation/TypingBubble.tsx @@ -3,67 +3,65 @@ import classNames from 'classnames'; import { TypingAnimation } from './TypingAnimation'; import { Avatar } from '../Avatar'; +import styled from 'styled-components'; -import { LocalizerType } from '../../types/Util'; - -interface Props { +interface TypingBubbleProps { avatarPath?: string; - color: string; - name: string; phoneNumber: string; - profileName: string; + displayedName: string | null; conversationType: string; - i18n: LocalizerType; + isTyping: boolean; } -export class TypingBubble extends React.Component { - public renderAvatar() { - const { - avatarPath, - name, - phoneNumber, - profileName, - conversationType, - } = this.props; +const TypingBubbleContainer = styled.div` + height: ${props => (props.isTyping ? 'auto' : '0px')}; + display: flow-root; + padding-bottom: ${props => (props.isTyping ? '4px' : '0px')}; + padding-top: ${props => (props.isTyping ? '4px' : '0px')}; + transition: ${props => props.theme.common.animations.defaultDuration}; + padding-inline-end: 16px; + overflow: hidden; +`; + +export const TypingBubble = (props: TypingBubbleProps) => { + const renderAvatar = () => { + const { avatarPath, displayedName, conversationType, phoneNumber } = props; if (conversationType !== 'group') { return; } - const userName = name || profileName || phoneNumber; return (
); - } + }; - public render() { - const { i18n, color } = this.props; + if (props.conversationType === 'group') { + return <>; + } - return ( -
+ return ( + +
-
-
- -
- {this.renderAvatar()} +
+
+ {renderAvatar()}
- ); - } -} + + ); +}; diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 5cd4dc64c..397e75970 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -128,6 +128,7 @@ export class SessionCompositionBox extends React.Component { private linkPreviewAbortController?: AbortController; private container: any; private readonly mentionsRegex = /@\u{FFD2}05[0-9a-f]{64}:[^\u{FFD2}]+\u{FFD2}/gu; + private lastBumpTypingMessageLength: number = 0; constructor(props: any) { super(props); @@ -167,6 +168,7 @@ export class SessionCompositionBox extends React.Component { // Events this.onKeyDown = this.onKeyDown.bind(this); + this.onKeyUp = this.onKeyUp.bind(this); this.onChange = this.onChange.bind(this); this.focusCompositionBox = this.focusCompositionBox.bind(this); @@ -190,6 +192,7 @@ export class SessionCompositionBox extends React.Component { // reset the state on new conversation key if (prevProps.conversationKey !== this.props.conversationKey) { this.setState(getDefaultState(), this.focusCompositionBox); + this.lastBumpTypingMessageLength = 0; } else if ( this.props.stagedAttachments?.length !== prevProps.stagedAttachments?.length @@ -360,6 +363,7 @@ export class SessionCompositionBox extends React.Component { value={message} onChange={this.onChange} onKeyDown={this.onKeyDown} + onKeyUp={this.onKeyUp} placeholder={messagePlaceHolder} spellCheck={false} inputRef={this.textarea} @@ -723,6 +727,24 @@ export class SessionCompositionBox extends React.Component { } } + private async onKeyUp(event: any) { + const { message } = this.state; + // Called whenever the user changes the message composition field. But only + // fires if there's content in the message field after the change. + // Also, check for a message length change before firing it up, to avoid + // catching ESC, tab, or whatever which is not typing + if (message.length && message.length !== this.lastBumpTypingMessageLength) { + const conversationModel = window.ConversationController.get( + this.props.conversationKey + ); + if (!conversationModel) { + return; + } + conversationModel.throttledBumpTyping(); + this.lastBumpTypingMessageLength = message.length; + } + } + private parseEmojis(value: string) { const emojisArray = toArray(value); diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 9aacf4042..8a0a4fc86 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -16,6 +16,7 @@ import { MessageModel } from '../../../../js/models/messages'; import { SessionLastSeenIndicator } from './SessionLastSeedIndicator'; import { VerificationNotification } from '../../conversation/VerificationNotification'; import { ToastUtils } from '../../../session/utils'; +import { TypingBubble } from '../../conversation/TypingBubble'; interface State { showScrollButton: boolean; @@ -119,15 +120,29 @@ export class SessionMessagesList extends React.Component { } public render() { - const { messages } = this.props; + const { conversationKey, conversation, messages } = this.props; const { showScrollButton } = this.state; + let displayedName = null; + if (conversation.type === 'direct') { + displayedName = window.ConversationController.getContactProfileNameOrShortenedPubKey( + conversationKey + ); + } + return (
+ + {this.renderMessages(messages)}