forked from Ivasoft/mattermost-mobile
Compare commits
12 Commits
voice-mess
...
voice-mess
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c9e9357a5 | ||
|
|
335b8f2925 | ||
|
|
bd9bb48384 | ||
|
|
04030a93f7 | ||
|
|
56987dcf37 | ||
|
|
494df6e0b1 | ||
|
|
c7040707f4 | ||
|
|
d6eb40776f | ||
|
|
375412833d | ||
|
|
7d2f23b5f4 | ||
|
|
7fbc59b322 | ||
|
|
92c18c4893 |
@@ -1,15 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
file: FileInfo;
|
||||
}
|
||||
|
||||
function VoiceRecordingFile({
|
||||
file,
|
||||
}: Props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default VoiceRecordingFile;
|
||||
113
app/components/files/voice_recording_file/index.tsx
Normal file
113
app/components/files/voice_recording_file/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {StyleSheet, Text, useWindowDimensions, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import PlayBack from '@components/files/voice_recording_file/playback';
|
||||
import {MIC_SIZE, VOICE_MESSAGE_CARD_RATIO} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
//i18n
|
||||
const VOICE_MESSAGE = 'Voice message';
|
||||
const UPLOADING_TEXT = 'Uploading..(0%)';
|
||||
|
||||
type Props = {
|
||||
file: FileInfo;
|
||||
uploading: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 6,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 3,
|
||||
},
|
||||
alignItems: 'center',
|
||||
},
|
||||
centerContainer: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
title: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 200),
|
||||
},
|
||||
uploading: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
...typography('Body', 75),
|
||||
},
|
||||
close: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.56),
|
||||
},
|
||||
mic: {
|
||||
borderRadius: MIC_SIZE / 2,
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
|
||||
height: MIC_SIZE,
|
||||
width: MIC_SIZE,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 12,
|
||||
},
|
||||
playBackContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const VoiceRecordingFile = ({file, uploading}: Props) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const dimensions = useWindowDimensions();
|
||||
const isVoiceMessage = file.is_voice_recording;
|
||||
|
||||
const voiceStyle = useMemo(() => {
|
||||
return {
|
||||
width: dimensions.width * VOICE_MESSAGE_CARD_RATIO,
|
||||
};
|
||||
}, [dimensions.width]);
|
||||
|
||||
const getUploadingView = useCallback(() => {
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={styles.mic}
|
||||
>
|
||||
<CompassIcon
|
||||
name='microphone'
|
||||
size={24}
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.centerContainer}>
|
||||
<Text style={styles.title}>{VOICE_MESSAGE}</Text>
|
||||
<Text style={styles.uploading}>{UPLOADING_TEXT}</Text>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}, [uploading]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
isVoiceMessage && voiceStyle,
|
||||
]}
|
||||
>
|
||||
{uploading ? getUploadingView() : <PlayBack/>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceRecordingFile;
|
||||
61
app/components/files/voice_recording_file/playback.tsx
Normal file
61
app/components/files/voice_recording_file/playback.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import {TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import SoundWave from '@components/post_draft/draft_input/voice_input/sound_wave';
|
||||
import TimeElapsed from '@components/post_draft/draft_input/voice_input/time_elapsed';
|
||||
import {MIC_SIZE} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
mic: {
|
||||
borderRadius: MIC_SIZE / 2,
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
|
||||
height: MIC_SIZE,
|
||||
width: MIC_SIZE,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 12,
|
||||
},
|
||||
playBackContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
const PlayBack = () => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
|
||||
const play = preventDoubleTap(() => {
|
||||
return setPlaying((p) => !p);
|
||||
});
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.playBackContainer}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.mic}
|
||||
onPress={play}
|
||||
>
|
||||
<CompassIcon
|
||||
color={theme.buttonBg}
|
||||
name='play'
|
||||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<SoundWave animating={playing}/>
|
||||
<TimeElapsed/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayBack;
|
||||
@@ -2,12 +2,15 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {LayoutChangeEvent, Platform, ScrollView} from 'react-native';
|
||||
import {LayoutChangeEvent, Platform, ScrollView, View} from 'react-native';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import QuickActions from '@components/post_draft/quick_actions';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import RecordAction from '../record_action';
|
||||
import SendAction from '../send_action';
|
||||
import Typing from '../typing';
|
||||
|
||||
import MessageInput from './message_input';
|
||||
@@ -65,37 +68,92 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
},
|
||||
actionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingBottom: Platform.select({
|
||||
ios: 1,
|
||||
android: 2,
|
||||
}),
|
||||
},
|
||||
sendVoiceMessage: {
|
||||
position: 'absolute',
|
||||
right: -5,
|
||||
top: 16,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default function DraftInput({
|
||||
testID,
|
||||
addFiles,
|
||||
canSend,
|
||||
channelId,
|
||||
currentUserId,
|
||||
cursorPosition,
|
||||
files,
|
||||
maxMessageLength,
|
||||
rootId = '',
|
||||
value,
|
||||
uploadFileError,
|
||||
sendMessage,
|
||||
canSend,
|
||||
updateValue,
|
||||
addFiles,
|
||||
testID,
|
||||
updateCursorPosition,
|
||||
cursorPosition,
|
||||
updatePostInputTop,
|
||||
updateValue,
|
||||
uploadFileError,
|
||||
value,
|
||||
voiceMessageEnabled,
|
||||
}: Props) {
|
||||
const [recording, setRecording] = useState(false);
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
updatePostInputTop(e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const [recording, setRecording] = useState(false);
|
||||
const onPresRecording = useCallback(() => {
|
||||
setRecording(true);
|
||||
}, []);
|
||||
|
||||
// Render
|
||||
const style = getStyleSheet(theme);
|
||||
const onCloseRecording = useCallback(() => {
|
||||
setRecording(false);
|
||||
}, []);
|
||||
|
||||
const isHandlingVoice = files[0]?.is_voice_recording || recording;
|
||||
const sendActionTestID = `${testID}.send_action`;
|
||||
const recordActionTestID = `${testID}.record_action`;
|
||||
|
||||
const getActionButton = useCallback(() => {
|
||||
if (value.length === 0 && files.length === 0 && voiceMessageEnabled) {
|
||||
return (
|
||||
<RecordAction
|
||||
onPress={onPresRecording}
|
||||
testID={recordActionTestID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SendAction
|
||||
disabled={!canSend}
|
||||
sendMessage={sendMessage}
|
||||
testID={sendActionTestID}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
canSend,
|
||||
files.length,
|
||||
onCloseRecording,
|
||||
onPresRecording,
|
||||
sendMessage,
|
||||
testID,
|
||||
value.length,
|
||||
voiceMessageEnabled,
|
||||
isHandlingVoice,
|
||||
]);
|
||||
|
||||
const quickActionsTestID = `${testID}.quick_actions`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -109,25 +167,25 @@ export default function DraftInput({
|
||||
style={style.inputWrapper}
|
||||
testID={testID}
|
||||
>
|
||||
|
||||
<ScrollView
|
||||
style={style.inputContainer}
|
||||
contentContainerStyle={style.inputContentContainer}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
pinchGestureEnabled={false}
|
||||
overScrollMode={'never'}
|
||||
disableScrollViewPanResponder={true}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
overScrollMode={'never'}
|
||||
pinchGestureEnabled={false}
|
||||
scrollEnabled={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={style.inputContainer}
|
||||
>
|
||||
{recording && (
|
||||
<VoiceInput
|
||||
addFiles={addFiles}
|
||||
onClose={onCloseRecording}
|
||||
setRecording={setRecording}
|
||||
/>
|
||||
)}
|
||||
{!recording && (
|
||||
{!recording &&
|
||||
<MessageInput
|
||||
addFiles={addFiles}
|
||||
canSend={canSend}
|
||||
@@ -136,16 +194,34 @@ export default function DraftInput({
|
||||
cursorPosition={cursorPosition}
|
||||
files={files}
|
||||
maxMessageLength={maxMessageLength}
|
||||
rootId={rootId}
|
||||
sendMessage={sendMessage}
|
||||
setRecording={setRecording}
|
||||
testID={testID}
|
||||
updateCursorPosition={updateCursorPosition}
|
||||
updateValue={updateValue}
|
||||
uploadFileError={uploadFileError}
|
||||
value={value}
|
||||
rootId={rootId}
|
||||
testID={testID}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
<View style={style.actionsContainer}>
|
||||
{!isHandlingVoice &&
|
||||
<QuickActions
|
||||
testID={quickActionsTestID}
|
||||
fileCount={files.length}
|
||||
addFiles={addFiles}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
/>
|
||||
}
|
||||
{!isHandlingVoice && getActionButton()}
|
||||
</View>
|
||||
<SendAction
|
||||
disabled={!canSend}
|
||||
sendMessage={sendMessage}
|
||||
testID={sendActionTestID}
|
||||
containerStyle={isHandlingVoice && style.sendVoiceMessage}
|
||||
/>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Platform, StyleSheet, View} from 'react-native';
|
||||
|
||||
import PostInput from '../post_input';
|
||||
import QuickActions from '../quick_actions';
|
||||
import SendAction from '../send_action';
|
||||
import Uploads from '../uploads';
|
||||
|
||||
type Props = {
|
||||
@@ -33,19 +30,6 @@ type Props = {
|
||||
setRecording: (v: boolean) => void;
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
actionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingBottom: Platform.select({
|
||||
ios: 1,
|
||||
android: 2,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export default function MessageInput({
|
||||
testID,
|
||||
channelId,
|
||||
@@ -56,33 +40,31 @@ export default function MessageInput({
|
||||
value,
|
||||
uploadFileError,
|
||||
sendMessage,
|
||||
canSend,
|
||||
updateValue,
|
||||
addFiles,
|
||||
updateCursorPosition,
|
||||
cursorPosition,
|
||||
setRecording,
|
||||
}: Props) {
|
||||
// Render
|
||||
const postInputTestID = `${testID}.post.input`;
|
||||
const quickActionsTestID = `${testID}.quick_actions`;
|
||||
const sendActionTestID = `${testID}.send_action`;
|
||||
const isHandlingVoice = files[0]?.is_voice_recording;
|
||||
|
||||
const showAsRecord = files[0]?.is_voice_recording;
|
||||
return (
|
||||
<>
|
||||
<PostInput
|
||||
testID={postInputTestID}
|
||||
channelId={channelId}
|
||||
maxMessageLength={maxMessageLength}
|
||||
rootId={rootId}
|
||||
cursorPosition={cursorPosition}
|
||||
updateCursorPosition={updateCursorPosition}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
addFiles={addFiles}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
{!isHandlingVoice && (
|
||||
<PostInput
|
||||
testID={postInputTestID}
|
||||
channelId={channelId}
|
||||
maxMessageLength={maxMessageLength}
|
||||
rootId={rootId}
|
||||
cursorPosition={cursorPosition}
|
||||
updateCursorPosition={updateCursorPosition}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
addFiles={addFiles}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
)}
|
||||
<Uploads
|
||||
currentUserId={currentUserId}
|
||||
files={files}
|
||||
@@ -90,22 +72,6 @@ export default function MessageInput({
|
||||
channelId={channelId}
|
||||
rootId={rootId}
|
||||
/>
|
||||
<View style={style.actionsContainer}>
|
||||
{!showAsRecord &&
|
||||
<QuickActions
|
||||
testID={quickActionsTestID}
|
||||
fileCount={files.length}
|
||||
addFiles={addFiles}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
/>
|
||||
}
|
||||
<SendAction
|
||||
testID={sendActionTestID}
|
||||
disabled={!canSend}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
setRecording: (v: boolean) => void;
|
||||
addFiles: (f: FileInfo[]) => void;
|
||||
}
|
||||
|
||||
function VoiceInput({
|
||||
setRecording,
|
||||
addFiles,
|
||||
}: Props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default VoiceInput;
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {View} from 'react-native';
|
||||
import Animated, {interpolate, SharedValue, useAnimatedStyle, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {MIC_SIZE} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const iconCommon = {
|
||||
height: MIC_SIZE,
|
||||
width: MIC_SIZE,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
const round = {
|
||||
borderRadius: MIC_SIZE / 2,
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
|
||||
};
|
||||
|
||||
return {
|
||||
mic: {
|
||||
...iconCommon,
|
||||
...round,
|
||||
},
|
||||
abs: {
|
||||
position: 'absolute',
|
||||
},
|
||||
concentric: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const useConcentricStyles = (circleId: number, sharedValue: SharedValue<number>) => {
|
||||
const circles = [1.5, 2.5, 3.5];
|
||||
return useAnimatedStyle(() => {
|
||||
const scale = interpolate(sharedValue.value, [0, 1], [circles[circleId], 1]);
|
||||
const opacity = interpolate(sharedValue.value, [0, 1], [1, 0]);
|
||||
|
||||
return {
|
||||
opacity,
|
||||
transform: [{scale}],
|
||||
borderRadius: MIC_SIZE / 2,
|
||||
};
|
||||
}, [sharedValue]);
|
||||
};
|
||||
|
||||
const AnimatedMicrophone = () => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const val = useSharedValue(0);
|
||||
|
||||
const firstCircleAnimx = useConcentricStyles(0, val);
|
||||
const secondCircleAnimx = useConcentricStyles(1, val);
|
||||
const thirdCircleAnimx = useConcentricStyles(2, val);
|
||||
|
||||
useEffect(() => {
|
||||
val.value = withRepeat(
|
||||
withTiming(1, {duration: 1000}),
|
||||
800,
|
||||
true,
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={[styles.mic]}>
|
||||
<View style={styles.concentric} >
|
||||
<Animated.View style={[styles.mic, styles.abs, firstCircleAnimx]}/>
|
||||
<Animated.View style={[styles.mic, styles.abs, secondCircleAnimx]}/>
|
||||
<Animated.View style={[styles.mic, styles.abs, thirdCircleAnimx]}/>
|
||||
</View>
|
||||
<View
|
||||
style={[styles.mic, styles.abs]}
|
||||
>
|
||||
<CompassIcon
|
||||
name='microphone'
|
||||
size={24}
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default AnimatedMicrophone;
|
||||
|
||||
94
app/components/post_draft/draft_input/voice_input/index.tsx
Normal file
94
app/components/post_draft/draft_input/voice_input/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {MIC_SIZE} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import AnimatedMicrophone from './animated_microphone';
|
||||
import SoundWave from './sound_wave';
|
||||
import TimeElapsed from './time_elapsed';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const iconCommon = {
|
||||
height: MIC_SIZE,
|
||||
width: MIC_SIZE,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
const round = {
|
||||
borderRadius: MIC_SIZE / 2,
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
|
||||
};
|
||||
|
||||
return {
|
||||
mainContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
height: 88,
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-around',
|
||||
},
|
||||
mic: {
|
||||
...iconCommon,
|
||||
...round,
|
||||
},
|
||||
check: {
|
||||
...iconCommon,
|
||||
...round,
|
||||
backgroundColor: theme.buttonBg,
|
||||
},
|
||||
close: {
|
||||
...iconCommon,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type VoiceInputProps = {
|
||||
setRecording: (v: boolean) => void;
|
||||
addFiles: (f: FileInfo[]) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
const VoiceInput = ({onClose}: VoiceInputProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.mainContainer}>
|
||||
<AnimatedMicrophone/>
|
||||
<SoundWave/>
|
||||
<TimeElapsed/>
|
||||
<TouchableOpacity
|
||||
style={styles.close}
|
||||
onPress={onClose}
|
||||
>
|
||||
<CompassIcon
|
||||
color={theme.buttonBg}
|
||||
name='close'
|
||||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.check}
|
||||
onPress={onClose} // to be fixed when wiring is completed
|
||||
>
|
||||
<CompassIcon
|
||||
color={theme.buttonColor}
|
||||
name='check'
|
||||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceInput;
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {random} from 'lodash';
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import Animated, {cancelAnimation, Extrapolation, interpolate, useAnimatedStyle, useSharedValue, withRepeat, withSpring} from 'react-native-reanimated';
|
||||
|
||||
import {WAVEFORM_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
height: WAVEFORM_HEIGHT,
|
||||
width: 165,
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
singleBar: {
|
||||
height: WAVEFORM_HEIGHT,
|
||||
width: 2,
|
||||
backgroundColor: theme.buttonBg,
|
||||
marginRight: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type SoundWaveProps = {
|
||||
animating?: boolean;
|
||||
};
|
||||
|
||||
const SoundWave = ({animating = true}: SoundWaveProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const animatedValue = useSharedValue(5);
|
||||
|
||||
const animatedStyles = useAnimatedStyle(() => {
|
||||
const newHeight = interpolate(
|
||||
animatedValue.value,
|
||||
[5, 40],
|
||||
[0, 40],
|
||||
Extrapolation.EXTEND,
|
||||
);
|
||||
return {
|
||||
height: newHeight,
|
||||
};
|
||||
}, []);
|
||||
|
||||
useDidUpdate(() => {
|
||||
if (animating) {
|
||||
animatedValue.value = withRepeat(
|
||||
withSpring(40, {
|
||||
damping: 10,
|
||||
mass: 0.6,
|
||||
overshootClamping: true,
|
||||
}),
|
||||
800,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
cancelAnimation(animatedValue);
|
||||
}
|
||||
}, [animating]);
|
||||
|
||||
const getAudioBars = () => {
|
||||
const bars = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
let height;
|
||||
if (random(i, 50) % 2 === 0) {
|
||||
height = random(5, 30);
|
||||
}
|
||||
bars.push(
|
||||
<Animated.View
|
||||
key={i}
|
||||
style={[
|
||||
styles.singleBar,
|
||||
{height},
|
||||
!height && animatedStyles,
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return bars;
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{getAudioBars()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoundWave;
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState} from 'react';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
//fixme: hook up the time elapsed progress from the lib in here
|
||||
|
||||
const TimeElapsed = () => {
|
||||
const [timeElapsed] = useState('00:00');
|
||||
|
||||
return (
|
||||
<Text>
|
||||
{timeElapsed}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeElapsed;
|
||||
48
app/components/post_draft/record_action/index.tsx
Normal file
48
app/components/post_draft/record_action/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
||||
type Props = {
|
||||
onPress: () => void;
|
||||
testID: string;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
recordButtonContainer: {
|
||||
justifyContent: 'flex-end',
|
||||
paddingRight: 8,
|
||||
},
|
||||
recordButton: {
|
||||
borderRadius: 4,
|
||||
height: 24,
|
||||
width: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
};
|
||||
|
||||
function RecordButton({onPress, testID}: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={onPress}
|
||||
style={styles.recordButtonContainer}
|
||||
testID={testID}
|
||||
type={'opacity'}
|
||||
>
|
||||
<CompassIcon
|
||||
color={theme.centerChannelColor}
|
||||
name='microphone'
|
||||
size={24}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecordButton;
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {StyleProp, View, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
testID: string;
|
||||
disabled: boolean;
|
||||
sendMessage: () => void;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
@@ -39,6 +40,7 @@ function SendButton({
|
||||
testID,
|
||||
disabled,
|
||||
sendMessage,
|
||||
containerStyle,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const sendButtonTestID = disabled ? `${testID}.send.button.disabled` : `${testID}.send.button`;
|
||||
@@ -57,7 +59,7 @@ function SendButton({
|
||||
<TouchableWithFeedback
|
||||
testID={sendButtonTestID}
|
||||
onPress={sendMessage}
|
||||
style={style.sendButtonContainer}
|
||||
style={[style.sendButtonContainer, containerStyle]}
|
||||
type={'opacity'}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -156,17 +156,14 @@ function Uploads({
|
||||
{buildFilePreviews()}
|
||||
</ScrollView>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View
|
||||
style={[style.errorContainer, errorAnimatedStyle]}
|
||||
>
|
||||
{Boolean(uploadFileError) &&
|
||||
<View style={style.errorTextContainer}>
|
||||
|
||||
<Text style={style.warning}>
|
||||
{uploadFileError}
|
||||
</Text>
|
||||
|
||||
</View>
|
||||
}
|
||||
</Animated.View>
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native';
|
||||
import {StyleSheet, TouchableWithoutFeedback, useWindowDimensions, View} from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
|
||||
import {updateDraftFile} from '@actions/local/draft';
|
||||
import VoiceRecordingFile from '@app/components/files/voice_recording_file';
|
||||
import FileIcon from '@components/files/file_icon';
|
||||
import ImageFile from '@components/files/image_file';
|
||||
import VoiceRecordingFile from '@components/files/voice_recording_file';
|
||||
import ProgressBar from '@components/progress_bar';
|
||||
import {VOICE_MESSAGE_CARD_RATIO} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
@@ -64,8 +65,10 @@ export default function UploadItem({
|
||||
const serverUrl = useServerUrl();
|
||||
const removeCallback = useRef<(() => void)|null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const dimensions = useWindowDimensions();
|
||||
|
||||
const loading = DraftUploadManager.isUploading(file.clientId!);
|
||||
const isVoiceMessage = file.is_voice_recording;
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
openGallery(file);
|
||||
@@ -116,13 +119,16 @@ export default function UploadItem({
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (file.is_voice_recording) {
|
||||
|
||||
if (isVoiceMessage) {
|
||||
return (
|
||||
<VoiceRecordingFile
|
||||
file={file}
|
||||
uploading={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FileIcon
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.08)}
|
||||
@@ -132,17 +138,35 @@ export default function UploadItem({
|
||||
);
|
||||
}, [file]);
|
||||
|
||||
const voiceStyle = useMemo(() => {
|
||||
return {
|
||||
width: dimensions.width * VOICE_MESSAGE_CARD_RATIO,
|
||||
};
|
||||
}, [dimensions.width]);
|
||||
|
||||
return (
|
||||
<View
|
||||
key={file.clientId}
|
||||
style={style.preview}
|
||||
style={[
|
||||
style.preview,
|
||||
isVoiceMessage && voiceStyle,
|
||||
]}
|
||||
>
|
||||
<View style={style.previewContainer}>
|
||||
<View
|
||||
style={[
|
||||
style.previewContainer,
|
||||
]}
|
||||
>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onGestureEvent}
|
||||
disabled={file.is_voice_recording}
|
||||
>
|
||||
<Animated.View style={[styles, style.filePreview]}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles,
|
||||
style.filePreview,
|
||||
]}
|
||||
>
|
||||
{filePreviewComponent}
|
||||
</Animated.View>
|
||||
</TouchableWithoutFeedback>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View, Platform} from 'react-native';
|
||||
import {View, Platform, StyleProp, ViewStyle} from 'react-native';
|
||||
|
||||
import {removeDraftFile} from '@actions/local/draft';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -14,8 +14,9 @@ import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
rootId: string;
|
||||
clientId: string;
|
||||
rootId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
@@ -30,7 +31,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
removeButton: {
|
||||
borderRadius: 12,
|
||||
alignSelf: 'center',
|
||||
|
||||
// alignSelf: 'center',
|
||||
marginTop: Platform.select({
|
||||
ios: 5.4,
|
||||
android: 4.75,
|
||||
@@ -44,8 +46,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
|
||||
export default function UploadRemove({
|
||||
channelId,
|
||||
rootId,
|
||||
containerStyle,
|
||||
clientId,
|
||||
rootId,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -58,7 +61,7 @@ export default function UploadRemove({
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
style={style.tappableContainer}
|
||||
style={[style.tappableContainer, containerStyle]}
|
||||
onPress={onPress}
|
||||
type={'opacity'}
|
||||
>
|
||||
|
||||
@@ -27,6 +27,9 @@ export const JOIN_CALL_BAR_HEIGHT = 38;
|
||||
export const CURRENT_CALL_BAR_HEIGHT = 74;
|
||||
|
||||
export const QUICK_OPTIONS_HEIGHT = 270;
|
||||
export const VOICE_MESSAGE_CARD_RATIO = 0.72;
|
||||
export const MIC_SIZE = 40;
|
||||
export const WAVEFORM_HEIGHT = 40;
|
||||
|
||||
export default {
|
||||
BOTTOM_TAB_HEIGHT,
|
||||
@@ -50,5 +53,8 @@ export default {
|
||||
HEADER_SEARCH_HEIGHT,
|
||||
HEADER_SEARCH_BOTTOM_MARGIN,
|
||||
QUICK_OPTIONS_HEIGHT,
|
||||
VOICE_MESSAGE_CARD_RATIO,
|
||||
MIC_SIZE,
|
||||
WAVEFORM_HEIGHT,
|
||||
};
|
||||
|
||||
|
||||
@@ -516,9 +516,9 @@ PODS:
|
||||
- RNScreens (3.15.0):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
- RNSentry (4.2.2):
|
||||
- RNSentry (4.6.0):
|
||||
- React-Core
|
||||
- Sentry (= 7.22.0)
|
||||
- Sentry (= 7.27.0)
|
||||
- RNShare (7.7.0):
|
||||
- React-Core
|
||||
- RNSVG (12.4.3):
|
||||
@@ -532,9 +532,9 @@ PODS:
|
||||
- SDWebImageWebPCoder (0.8.4):
|
||||
- libwebp (~> 1.0)
|
||||
- SDWebImage/Core (~> 5.10)
|
||||
- Sentry (7.22.0):
|
||||
- Sentry/Core (= 7.22.0)
|
||||
- Sentry/Core (7.22.0)
|
||||
- Sentry (7.27.0):
|
||||
- Sentry/Core (= 7.27.0)
|
||||
- Sentry/Core (7.27.0)
|
||||
- simdjson (1.0.0)
|
||||
- SocketRocket (0.6.0)
|
||||
- Starscream (4.0.4)
|
||||
@@ -952,14 +952,14 @@ SPEC CHECKSUMS:
|
||||
RNReanimated: 2cf7451318bb9cc430abeec8d67693f9cf4e039c
|
||||
RNRudderSdk: 14c176adb1557f3832cb385fcd14250f76a7e268
|
||||
RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7
|
||||
RNSentry: f03937a2cf86c7029ba31fbbf5618c55d223cd62
|
||||
RNSentry: 73f65d6ef3e5a313d6bb225e2c92c84be2e17921
|
||||
RNShare: ab582e93876d9df333a531390c658c31b50a767d
|
||||
RNSVG: f3b60aeeaa81960e2e0536c3a9eef50b667ef3a9
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
Rudder: 59634aec6dd26a4672f74f40fc45bc17f91af3f0
|
||||
SDWebImage: 53179a2dba77246efa8a9b85f5c5b21f8f43e38f
|
||||
SDWebImageWebPCoder: f93010f3f6c031e2f8fb3081ca4ee6966c539815
|
||||
Sentry: 30b51086ca9aac23337880152e95538f7e177f7f
|
||||
Sentry: 026b36fdc09531604db9279e55f047fe652e3f4a
|
||||
simdjson: c96317b3a50dff3468a42f586ab7ed22c6ab2fd9
|
||||
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
|
||||
Starscream: 5178aed56b316f13fa3bc55694e583d35dd414d9
|
||||
|
||||
Reference in New Issue
Block a user