diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 690436a24..31681bb6d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -418,5 +418,6 @@ "unknownCountry": "Unknown Country", "device": "Device", "destination": "Destination", - "learnMore": "Learn more" + "learnMore": "Learn more", + "playAtCustomSpeed": "Play at $multipler$x speed" } diff --git a/ts/components/OnionStatusDialog.tsx b/ts/components/OnionStatusDialog.tsx index 686f00a99..465b77989 100644 --- a/ts/components/OnionStatusDialog.tsx +++ b/ts/components/OnionStatusDialog.tsx @@ -16,7 +16,6 @@ import { SessionWrapperModal } from '../components/session/SessionWrapperModal'; import ip2country from 'ip2country'; import countryLookup from 'country-code-lookup'; import { useTheme } from 'styled-components'; -import { useNetwork } from '../hooks/useNetwork'; import { Snode } from '../data/data'; import { onionPathModal } from '../state/ducks/modalDialog'; import { @@ -25,6 +24,9 @@ import { getOnionPathsCount, } from '../state/selectors/onions'; +// tslint:disable-next-line: no-submodule-imports +import useNetworkState from 'react-use/lib/useNetworkState'; + export type StatusLightType = { glowStartDelay: number; glowDuration: number; @@ -132,7 +134,7 @@ export const ActionPanelOnionStatusLight = (props: { const theme = useTheme(); const onionPathsCount = useSelector(getOnionPathsCount); const firstPathLength = useSelector(getFirstOnionPathLength); - const isOnline = useNetwork(); + const isOnline = useNetworkState().online; // Set icon color based on result const red = theme.colors.destructive; diff --git a/ts/components/conversation/H5AudioPlayer.tsx b/ts/components/conversation/H5AudioPlayer.tsx new file mode 100644 index 000000000..33dcb490f --- /dev/null +++ b/ts/components/conversation/H5AudioPlayer.tsx @@ -0,0 +1,54 @@ +// Audio Player +import React, { createRef, useEffect } from 'react'; +import H5AudioPlayer from 'react-h5-audio-player'; +import { useTheme } from 'styled-components'; +import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch'; +import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon'; + +export const AudioPlayerWithEncryptedFile = (props: { + src: string; + contentType: string; + playbackSpeed: number; +}) => { + const theme = useTheme(); + const { urlToLoad } = useEncryptedFileFetch(props.src, props.contentType); + const { playbackSpeed } = props; + const player = createRef(); + + useEffect(() => { + // updates playback speed to value selected in context menu + if (player.current?.audio.current?.playbackRate) { + player.current.audio.current.playbackRate = playbackSpeed; + } + }, [playbackSpeed]); + + return ( + + ), + pause: ( + + ), + }} + /> + ); +}; diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 870d35612..5e5de1f73 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -9,42 +9,6 @@ import { Image } from './Image'; import { ContactName } from './ContactName'; import { Quote } from './Quote'; -// Audio Player -import H5AudioPlayer from 'react-h5-audio-player'; - -const AudioPlayerWithEncryptedFile = (props: { src: string; contentType: string }) => { - const theme = useTheme(); - const { urlToLoad } = useEncryptedFileFetch(props.src, props.contentType); - return ( - - ), - pause: ( - - ), - }} - /> - ); -}; - import { canDisplayImage, getExtensionForDisplay, @@ -61,16 +25,14 @@ import { AttachmentType } from '../../types/Attachment'; import { getIncrement } from '../../util/timer'; import { isFileDangerous } from '../../util/isFileDangerous'; -import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon'; import _ from 'lodash'; import { animation, contextMenu, Item, Menu } from 'react-contexify'; import uuid from 'uuid'; import { InView } from 'react-intersection-observer'; -import { useTheme, withTheme } from 'styled-components'; +import { withTheme } from 'styled-components'; import { MessageMetadata } from './message/MessageMetadata'; import { PubKey } from '../../session/types'; import { MessageRegularProps } from '../../models/messageType'; -import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch'; import { addSenderAsModerator, removeSenderFromModerator, @@ -78,6 +40,7 @@ import { import { updateUserDetailsModal } from '../../state/ducks/modalDialog'; import { MessageInteraction } from '../../interactions'; import autoBind from 'auto-bind'; +import { AudioPlayerWithEncryptedFile } from './H5AudioPlayer'; // Same as MIN_WIDTH in ImageGrid.tsx const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200; @@ -86,6 +49,7 @@ interface State { expiring: boolean; expired: boolean; imageBroken: boolean; + playbackSpeed: number; } const EXPIRATION_CHECK_MINIMUM = 2000; @@ -98,13 +62,13 @@ class MessageInner extends React.PureComponent { public constructor(props: MessageRegularProps) { super(props); - autoBind(this); this.state = { expiring: false, expired: false, imageBroken: false, + playbackSpeed: 1, }; this.ctxMenuID = `ctx-menu-message-${uuid()}`; } @@ -235,6 +199,7 @@ class MessageInner extends React.PureComponent { }} > @@ -602,6 +567,11 @@ class MessageInner extends React.PureComponent { ) : null} + {isAudio(attachments) ? ( + + {window.i18n('playAtCustomSpeed', this.state.playbackSpeed === 1 ? 2 : 1)} + + ) : null} { MessageInteraction.copyBodyToClipboard(text); @@ -848,6 +818,15 @@ class MessageInner extends React.PureComponent { ); } + /** + * Doubles / halves the playback speed based on the current playback speed. + */ + private updatePlaybackSpeed() { + this.setState(prevState => ({ + playbackSpeed: prevState.playbackSpeed === 1 ? 2 : 1, + })); + } + private handleContextMenu(e: any) { e.preventDefault(); e.stopPropagation(); diff --git a/ts/components/session/icon/SessionIcon.tsx b/ts/components/session/icon/SessionIcon.tsx index 2b1bb9934..e698b71d5 100644 --- a/ts/components/session/icon/SessionIcon.tsx +++ b/ts/components/session/icon/SessionIcon.tsx @@ -62,11 +62,11 @@ const rotate = keyframes` * Creates a glow animation made for multiple element sequentially */ const glow = (color: string, glowDuration: number, glowStartDelay: number) => { - const dropShadowType = `drop-shadow(0px 0px 6px ${color}) `; + const dropShadowType = `drop-shadow(0px 0px 4px ${color}) `; + //increase shadow intensity by 3 - const dropShadow = `${dropShadowType.repeat(2)};`; + const dropShadow = `${dropShadowType.repeat(1)};`; - // TODO: Decrease dropshadow for last frame // creating keyframe for sequential animations let kf = ''; for (let i = 0; i <= glowDuration; i++) { @@ -75,10 +75,12 @@ const glow = (color: string, glowDuration: number, glowStartDelay: number) => { if (i === glowStartDelay) { kf += `${percent}% { filter: ${dropShadow} + transform: scale(1.5); }`; } else { kf += `${percent}% { filter: none; + transform: scale(0.8); }`; } } @@ -92,7 +94,11 @@ const animation = (props: any) => { `; } else if (props.glowDuration !== undefined && props.glowStartDelay !== undefined) { return css` - ${glow(props.iconColor, props.glowDuration, props.glowStartDelay)} ${2}s ease-in infinite; + ${glow( + props.iconColor, + props.glowDuration, + props.glowStartDelay + )} ${props.glowDuration}s ease-in infinite; `; } else { return;