From 83bbeeb526e47019e00fc1299a53f5bb596f0a14 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 7 Apr 2020 11:35:56 +1000 Subject: [PATCH] Public Sans and Mono ONLY --- stylesheets/_session.scss | 41 +- stylesheets/_session_left_pane.scss | 6 +- stylesheets/_session_signin.scss | 2 +- .../conversation/SessionCompositionBox.tsx | 409 ++++++++++++++++++ 4 files changed, 435 insertions(+), 23 deletions(-) create mode 100644 ts/components/session/conversation/SessionCompositionBox.tsx diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 129fef126..dd22cebf2 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -1,22 +1,22 @@ @font-face { - font-family: 'SpaceMono'; + font-family: $session-font-mono; src: url('../fonts/SpaceMono-Regular.ttf') format('truetype'); } @font-face { - font-family: 'SpaceMono'; + font-family: $session-font-mono; src: url('../fonts/SpaceMono-Bold.ttf') format('truetype'); font-weight: bold; } @font-face { - font-family: 'SpaceMono'; + font-family: $session-font-mono; src: url('../fonts/SpaceMono-Italic.ttf') format('truetype'); font-style: italic; } @font-face { - font-family: 'SpaceMono'; + font-family: $session-font-mono; src: url('../fonts/SpaceMono-BoldItalic.ttf') format('truetype'); font-weight: bold; font-style: italic; @@ -87,8 +87,11 @@ } // Session Colors -$session-font-accent: 'Public Sans'; -$session-font-default: 'Karla'; +// Fonts changed to 'Public Sans' and Space Mono' as per req. Chad. +$session-font-default: 'Public Sans'; +$session-font-mono: 'SpaceMono'; +// $session-font-default: 'Public Sans'; +// $session-font-default: 'Karla'; $session-color-green: #00f782; $session-color-green-alt-1: #00f480; @@ -551,7 +554,7 @@ $session-element-border-green: 4px solid $session-color-green; &-text { @include session-color-subtle($session-color-white); - font-family: $session-font-accent; + font-family: $session-font-default; font-weight: 300; font-size: $session-font-xs; line-height: $session-font-xs; @@ -802,7 +805,7 @@ label { &__body { padding: 0px $session-margin-lg $session-margin-lg $session-margin-lg; - font-family: $session-font-accent; + font-family: $session-font-default; line-height: $session-font-md; font-size: $session-font-sm; @@ -1081,7 +1084,7 @@ label { word-break: break-all; font-size: $session-font-md; padding: 0px $session-margin-lg; - font-family: $session-font-accent; + font-family: $session-font-default; font-weight: 100; color: rgba($session-color-white, 0.8); font-size: $session-font-md; @@ -1283,7 +1286,7 @@ label { } &__description { - font-family: $session-font-accent; + font-family: $session-font-default; font-size: $session-font-sm; font-weight: 100; max-width: 700px; @@ -1358,7 +1361,7 @@ label { text-align: center; font-size: $session-font-xl; letter-spacing: 5px; - font-family: $session-font-accent; + font-family: $session-font-default; } } } @@ -1393,7 +1396,7 @@ label { .discussion-container { .module-message { - font-family: $session-font-accent; + font-family: $session-font-default; border-radius: 5px; &__text--incoming { @@ -1606,7 +1609,7 @@ input { /* Memberlist */ .member-list-container .member { &-item { - font-family: $session-font-accent; + font-family: $session-font-default; padding: $session-margin-sm $session-margin-md; background-color: $session-shade-5; @@ -1673,7 +1676,7 @@ input { } &-container { - font-family: $session-font-accent; + font-family: $session-font-default; color: $session-color-white; display: inline-flex; flex-direction: column; @@ -1723,7 +1726,7 @@ input { text-align: center; font-size: 24px; letter-spacing: 5px; - font-family: $session-font-accent; + font-family: $session-font-default; } } } @@ -1757,7 +1760,7 @@ input { height: 400px; text-align: center; - font-family: $session-font-accent; + font-family: $session-font-default; } &__title h1 { @@ -1783,7 +1786,7 @@ input { } &--subtitle { - font-family: $session-font-accent; + font-family: $session-font-default; font-weight: 300; line-height: $session-font-md; opacity: 0.8; @@ -1823,7 +1826,7 @@ input { } &__no-contacts { - font-family: 'SpaceMono'; + font-family: $session-font-mono; text-align: center; padding: 20px; } @@ -1844,7 +1847,7 @@ input { .session-member-item { cursor: pointer; - font-family: $session-font-accent; + font-family: $session-font-default; padding: 0px $session-margin-sm; height: 50px; display: flex; diff --git a/stylesheets/_session_left_pane.scss b/stylesheets/_session_left_pane.scss index bbc1c09ba..e545205fa 100644 --- a/stylesheets/_session_left_pane.scss +++ b/stylesheets/_session_left_pane.scss @@ -302,7 +302,7 @@ $session-compose-margin: 20px; font-size: $session-font-sm; line-height: $session-font-h3; margin: 0px 20px; - font-family: $session-font-accent; + font-family: $session-font-default; } .session-id-editable { @@ -375,7 +375,7 @@ $session-compose-margin: 20px; border: none; flex-grow: 1; font-size: $session-font-sm; - font-family: $session-font-accent; + font-family: $session-font-default; &:focus { outline: none !important; @@ -658,7 +658,7 @@ $session-compose-margin: 20px; border-radius: 50px; color: $session-color-light-grey; border: 1px solid $session-color-dark-grey; - font-family: $session-font-accent; + font-family: $session-font-default; font-size: $session-font-sm; } } diff --git a/stylesheets/_session_signin.scss b/stylesheets/_session_signin.scss index c1443297b..83a6ac984 100644 --- a/stylesheets/_session_signin.scss +++ b/stylesheets/_session_signin.scss @@ -244,7 +244,7 @@ overflow-wrap: break-word; padding: 0px 5px 20px 5px; display: inline-block; - font-family: 'SpaceMono'; + font-family: $session-font-mono; user-select: all; } } diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx new file mode 100644 index 000000000..407a97980 --- /dev/null +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -0,0 +1,409 @@ +import React from 'react'; +import { debounce } from 'lodash'; + +import { Attachment } from '../../../types/Attachment'; +import * as MIME from '../../../types/MIME'; + +import TextareaAutosize from 'react-autosize-textarea'; + +import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; +import { SessionEmojiPanel } from './SessionEmojiPanel'; +import { SessionRecording } from './SessionRecording'; + +import { SignalService } from '../../../../ts/protobuf'; + + +interface Props { + placeholder?: string; + + sendMessage: any; + onMessageSending: any; + onMessageSuccess: any; + onMessageFailure: any; + + onLoadVoiceNoteView: any; + onExitVoiceNoteView: any; +} + +interface State { + message: string; + showRecordingView: boolean; + + mediaSetting: boolean | null; + showEmojiPanel: boolean; + attachments: Array; + voiceRecording?: Blob; +} + +export class SessionCompositionBox extends React.Component { + private readonly textarea: React.RefObject; + private readonly fileInput: React.RefObject; + private emojiPanel: any; + + constructor(props: any) { + super(props); + + this.state = { + message: '', + attachments: [], + voiceRecording: undefined, + showRecordingView: false, + mediaSetting: null, + showEmojiPanel: false, + }; + + this.textarea = React.createRef(); + this.fileInput = React.createRef(); + + // Emojis + this.emojiPanel = null; + this.toggleEmojiPanel = debounce(this.toggleEmojiPanel.bind(this), 100); + this.hideEmojiPanel = this.hideEmojiPanel.bind(this); + this.onEmojiClick = this.onEmojiClick.bind(this); + this.handleClick = this.handleClick.bind(this); + + this.renderRecordingView = this.renderRecordingView.bind(this); + this.renderCompositionView = this.renderCompositionView.bind(this); + + // Recording view functions + this.sendVoiceMessage = this.sendVoiceMessage.bind(this); + this.onLoadVoiceNoteView = this.onLoadVoiceNoteView.bind(this); + this.onExitVoiceNoteView = this.onExitVoiceNoteView.bind(this); + + // Attachments + this.onChoseAttachment = this.onChoseAttachment.bind(this); + this.onChooseAttachment = this.onChooseAttachment.bind(this); + + this.onKeyDown = this.onKeyDown.bind(this); + this.onChange = this.onChange.bind(this); + + } + + public componentWillReceiveProps(){ + console.log(`[vince][info] Here are my composition props: `, this.props); + } + + public async componentWillMount(){ + const mediaSetting = await window.getSettingValue('media-permissions'); + this.setState({mediaSetting}); + } + + public render() { + const { showRecordingView } = this.state; + + return ( +
+ { showRecordingView ? ( + <>{this.renderRecordingView()} + ) : ( + <>{this.renderCompositionView()} + )} +
+ ); + } + + private handleClick(e: any) { + if (this.emojiPanel && this.emojiPanel.contains(e.target)) { + return; + } + + this.toggleEmojiPanel(); + }; + + private showEmojiPanel() { + document.addEventListener('mousedown', this.handleClick, false); + + this.setState({ + showEmojiPanel: true, + }); + } + + private hideEmojiPanel() { + document.removeEventListener('mousedown', this.handleClick, false); + + this.setState({ + showEmojiPanel: false, + }); + } + + private toggleEmojiPanel() { + if (this.state.showEmojiPanel) { + this.hideEmojiPanel(); + } else { + this.showEmojiPanel(); + } + } + + private renderRecordingView() { + return ( + + ); + } + + private renderCompositionView() { + const { placeholder } = this.props; + const { showEmojiPanel, message } = this.state; + + return ( + <> + + + + + + +
+ +
+ + +
+ +
+ +
(this.emojiPanel = ref)} + onKeyDown={this.onKeyDown} + role="button" + > + +
+ + ); + } + + + + private onChooseAttachment() { + const fileInput = this.fileInput.current; + if(fileInput) fileInput.click(); + } + + private onChoseAttachment() { + // Build attachments list + const attachmentsFileList = this.fileInput.current?.files; + if (!attachmentsFileList) return; + + const attachments: Array = []; + Array.from(attachmentsFileList).forEach(async (file: File) => { + + const fileBlob = new Blob([file]); + const fileBuffer = await new Response(fileBlob).arrayBuffer(); + + const attachment = { + fileName: file.name, + flags: undefined, + // FIXME VINCE: Set appropriate type + contentType: MIME.AUDIO_WEBM, + size: file.size, + data: fileBuffer, + }; + + // Push if size is nonzero + if (attachment.data.byteLength) { + attachments.push(attachment); + } + }); + + this.setState({attachments}); + } + + private onKeyDown(event: any) { + if (event.key === 'Enter' && !event.shiftKey) { + // If shift, newline. Else send message. + event.preventDefault(); + this.onSendMessage(); + } else if (event.key === 'Escape' && this.state.showEmojiPanel) { + this.hideEmojiPanel(); + } + } + + + private onSendMessage() { + const messageInput = this.textarea.current; + if (!messageInput) { + return; + } + + // Verify message length + const messagePlaintext = messageInput.value; + const msgLen = messagePlaintext.length; + if (msgLen === 0 || msgLen > window.CONSTANTS.MAX_MESSAGE_BODY_LENGTH) { + return; + } + + + // handle Attachments + const {attachments} = this.state; + + console.log(`[vince][msg] Message:`, messagePlaintext); + console.log(`[vince][msg] fileAttachments:`, attachments); + + + // Handle emojis + + + // Send message + this.props.onMessageSending(); + + this.props.sendMessage( + messagePlaintext, + attachments, + undefined, + undefined, + null, + {} + ).then(() => { + // Message sending sucess + this.props.onMessageSuccess(); + + // Empty attachments + // Empty composition box + this.setState({ + message: '', + attachments: [], + }); + }).catch(() => { + // Message sending failed + this.props.onMessageFailure(); + }); + + } + + private async sendVoiceMessage(audioBlob: Blob) { + if (!this.state.showRecordingView) { + return; + } + + const fileBuffer = await new Response(audioBlob).arrayBuffer(); + + const audioAttachment: Attachment = { + data: fileBuffer, + flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE, + contentType: MIME.AUDIO_MP3, + }; + + const messageSuccess = this.props.sendMessage( + '', + [audioAttachment], + undefined, + undefined, + null, + {}, + ); + + if (messageSuccess) { + // success! + } + + console.log(`[compositionbox] Sending voice message:`, audioBlob); + + + this.onExitVoiceNoteView(); + } + + private onLoadVoiceNoteView() { + // Do stuff for component, then run callback to SessionConversation + const {mediaSetting} = this.state; + + if (mediaSetting) { + this.setState({ + showRecordingView: true, + showEmojiPanel: false, + }); + this.props.onLoadVoiceNoteView(); + + return; + } + + window.pushToast({ + id: 'audioPermissionNeeded', + title: window.i18n('audioPermissionNeededTitle'), + description: window.i18n('audioPermissionNeededDescription'), + type: 'info', + }); + + } + + private onExitVoiceNoteView() { + // Do stuff for component, then run callback to SessionConversation + this.setState({ showRecordingView: false }); + this.props.onExitVoiceNoteView(); + } + + private onDrop() { + // On drop attachments! + // this.textarea.current?.ondrop; + // Look into react-dropzone + } + + private onChange(event: any) { + this.setState({message: event.target.value}); + } + + private onEmojiClick({native}: any) { + const messageBox = this.textarea.current; + if (!messageBox) { + return; + } + + const { message } = this.state; + const currentSelectionStart = Number(messageBox.selectionStart); + const currentSelectionEnd = Number(messageBox.selectionEnd); + const before = message.slice(0, currentSelectionStart); + const end = message.slice(currentSelectionEnd); + const newMessage = `${before}${native}${end}`; + + this.setState({ message: newMessage }, () => { + // update our selection because updating text programmatically + // will put the selection at the end of the textarea + const selectionStart = currentSelectionStart + Number(native.length); + messageBox.selectionStart = selectionStart; + messageBox.selectionEnd = selectionStart; + + // Sometimes, we have to repeat the set of the selection position with a timeout to be effective + setTimeout(() => { + messageBox.selectionStart = selectionStart; + messageBox.selectionEnd = selectionStart; + }, 20); + }); + } + +}