Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ target 'Ecency' do
target.build_configurations.each do |cfg|
# requirement to run dev builds on m series devices
cfg.build_settings['ONLY_ACTIVE_ARCH'] = 'NO'

# Fix for react-native-vision-camera requiring iOS 15.0+
cfg.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end

# TcpSockets duplicate symbols workaround (only if still needed)
Expand Down
16 changes: 11 additions & 5 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- ExpoSpeech (13.1.7):
- ExpoModulesCore
- fast_float (6.1.4)
- FBLazyVector (0.79.5)
- Firebase/CoreOnly (12.3.0):
Expand Down Expand Up @@ -2595,9 +2597,9 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- SDWebImage (5.21.3):
- SDWebImage/Core (= 5.21.3)
- SDWebImage/Core (5.21.3)
- SDWebImage (5.21.5):
- SDWebImage/Core (= 5.21.5)
- SDWebImage/Core (5.21.5)
- SDWebImageAVIFCoder (0.11.1):
- libavif/core (>= 0.11.0)
- SDWebImage (~> 5.10)
Expand Down Expand Up @@ -2633,6 +2635,7 @@ DEPENDENCIES:
- ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`)
- ExpoLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
- ExpoSpeech (from `../node_modules/expo-speech/ios`)
- fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- Firebase/Messaging
Expand Down Expand Up @@ -2798,6 +2801,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-local-authentication/ios"
ExpoModulesCore:
:path: "../node_modules/expo-modules-core"
ExpoSpeech:
:path: "../node_modules/expo-speech/ios"
fast_float:
:podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec"
FBLazyVector:
Expand Down Expand Up @@ -3031,6 +3036,7 @@ SPEC CHECKSUMS:
ExpoLinearGradient: ce334cff9859da4635c1d8eff6e291b11b04ccbb
ExpoLocalAuthentication: 78f74d187ee51126e1a789d73fee32d6d7e60f1f
ExpoModulesCore: fdcc7469efc73d085a42bbe73dfb0fed21e7e836
ExpoSpeech: fceabf6bf31aa9530ef0696e6793571edb702068
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52
Firebase: f5439b235721ceeef14ca1f327c0da8e4e8556b5
Expand Down Expand Up @@ -3150,7 +3156,7 @@ SPEC CHECKSUMS:
RNSentry: 54e867946f0600e07ce5e850175c903a0ae98c60
RNSVG: 1fa61293cb54a97d8ee3b182d2fb60443edcdf69
RNVectorIcons: 32cc786d5a3c2d8e20ee5c1c7c63c2609f411eb7
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Expand All @@ -3161,6 +3167,6 @@ SPEC CHECKSUMS:
VisionCamera: 3ea10c46a5c612f5f89fb46a54bef4a0de8b58a7
Yoga: bfcce202dba74007f8974ee9c5f903a9a286c445

PODFILE CHECKSUM: 5c55737fddd1f8200df3a2f590cca3c06309b345
PODFILE CHECKSUM: 43ff3f5c0713e64ab4d744d0650205bd54277781

COCOAPODS: 1.16.2
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"expo-image": "~2.4.0",
"expo-linear-gradient": "~14.1.5",
"expo-local-authentication": "~16.0.5",
"expo-speech": "^13.1.7",
"he": "^1.2.0",
"hive-auth-wrapper": "git+https://github.com/noumantahir/hive-auth-wrapper.git",
"hive-uri": "^0.2.5",
Expand All @@ -104,7 +105,7 @@
"react-intl": "^3.9.2",
"react-native": "0.79.5",
"react-native-actions-sheet": "^0.9.7",
"react-native-actionsheet": "git+https://github.com/ecency/react-native-actionsheet",
"react-native-actionsheet": "https://github.com/ecency/react-native-actionsheet",
"react-native-autoheight-webview": "^1.6.5",
"react-native-background-timer": "^2.4.1",
"react-native-bootsplash": "^6.3.10",
Expand Down
2 changes: 2 additions & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import { LinkPreview, HiveLinkPreview } from './linkPreview';
import { CrossPostModal } from './crossPostModal';
import { ChatOptionsSheet } from './chatOptionsSheet';
import { ChatChannelOptionsSheet } from './chatChannelOptionsSheet';
import { TTSControls } from './textToSpeech/ttsControls';

// Basic UI Elements
import {
Expand Down Expand Up @@ -287,4 +288,5 @@ export {
CopyModal,
ChatOptionsSheet,
ChatChannelOptionsSheet,
TTSControls,
};
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,10 @@ const PostOptionsModal = ({ pageType, isWave, isVisibleTranslateModal }: Props,
},
});
break;
case 'voice':
await delay(700);
SheetManager.show(SheetNames.TTS_SETTINGS);
break;
case 'delete-post':
await delay(700);
_deletePost();
Expand Down
190 changes: 190 additions & 0 deletions src/components/textToSpeech/ttsControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React, { useState, useEffect, useRef } from 'react';
import { View, ActivityIndicator } from 'react-native';
import * as Speech from 'expo-speech';
import EStyleSheet from 'react-native-extended-stylesheet';
import { IconButton } from '../iconButton';
import { extractPlainTextForTTS, hasReadableContent } from '../../utils/textToSpeech';
import { loadTTSSettings, TTSSettings } from '../../utils/ttsSettings';

interface TTSControlsProps {
post: any;
style?: any;
}

export const TTSControls = ({ post, style }: TTSControlsProps) => {
const [isPlaying, setIsPlaying] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const settingsRef = useRef<TTSSettings | null>(null);
const isMountedRef = useRef(true);

useEffect(() => {
isMountedRef.current = true;

// Load settings on mount
loadTTSSettings().then((settings) => {
if (isMountedRef.current) {
settingsRef.current = settings;
}
});

return () => {
isMountedRef.current = false;
// Stop TTS when component unmounts
Speech.stop();
};
}, []);

// Stop TTS when post changes
useEffect(() => {
return () => {
Speech.stop();
setIsPlaying(false);
setIsPaused(false);
};
}, [post?.permlink]);

const handlePlayPause = async () => {
if (isPlaying && !isPaused) {
// Pause
try {
await Speech.pause();
setIsPaused(true);
} catch (error) {
console.error('TTS pause failed:', error);
Speech.stop();
if (isMountedRef.current) {
setIsPlaying(false);
setIsPaused(false);
setIsLoading(false);
}
}
} else if (isPaused) {
// Resume
try {
await Speech.resume();
setIsPaused(false);
} catch (error) {
console.error('TTS resume failed:', error);
Speech.stop();
if (isMountedRef.current) {
setIsPlaying(false);
setIsPaused(false);
setIsLoading(false);
}
}
} else {
// Start playing
setIsLoading(true);

try {
const text = extractPlainTextForTTS(post);

if (!text || text.length < 10) {
console.warn('No readable text found in post');
setIsLoading(false);
return;
}

// Reload settings in case they changed
const settings = await loadTTSSettings();
settingsRef.current = settings;

const speechOptions: Speech.SpeechOptions = {
language: settings.language,
pitch: settings.pitch,
rate: settings.rate,
onStart: () => {
if (isMountedRef.current) {
setIsPlaying(true);
setIsLoading(false);
}
},
onDone: () => {
if (isMountedRef.current) {
setIsPlaying(false);
setIsPaused(false);
}
},
onStopped: () => {
if (isMountedRef.current) {
setIsPlaying(false);
setIsPaused(false);
}
},
onError: (error) => {
console.error('TTS error:', error);
if (isMountedRef.current) {
setIsPlaying(false);
setIsPaused(false);
setIsLoading(false);
}
},
};

// Add voice if specified
if (settings.voice) {
speechOptions.voice = settings.voice;
}

Speech.speak(text, speechOptions);
} catch (error) {
console.error('Failed to start TTS:', error);
setIsLoading(false);
}
}
};

const handleStop = () => {
Speech.stop();
setIsPlaying(false);
setIsPaused(false);
};

// Don't show TTS controls if post has no readable content
if (!hasReadableContent(post)) {
return null;
}

return (
<View style={[styles.container, style]}>
{isLoading ? (
<ActivityIndicator size="small" color={EStyleSheet.value('$primaryBlue')} />
) : (
<>
<IconButton
iconType="MaterialCommunityIcons"
name={isPlaying && !isPaused ? 'pause' : 'play'}
onPress={handlePlayPause}
size={24}
color={EStyleSheet.value('$primaryBlack')}
style={styles.playButton}
/>
{isPlaying && (
<IconButton
iconType="MaterialCommunityIcons"
name="stop"
onPress={handleStop}
size={24}
color={EStyleSheet.value('$primaryBlack')}
style={styles.stopButton}
/>
)}
</>
)}
</View>
);
};

const styles = EStyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
},
playButton: {
marginRight: 0,
},
stopButton: {
marginRight: 0,
},
});
Loading