trying to figure out jest issue.

This commit is contained in:
Jordan Hewitt 2025-01-29 17:17:59 -08:00
parent 0876db2dea
commit 889d898062
18 changed files with 7410 additions and 4790 deletions

215
README.md
View File

@ -1,50 +1,195 @@
# Welcome to your Expo app 👋
# react-native-linear-gradient
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
A `<LinearGradient>` element for React Native
## Get started
[![ci][1]][2]
[![npm version][3]][4]
[![npm downloads][5]][4]
1. Install dependencies
<p align="center">
<img src="https://github.com/react-native-linear-gradient/react-native-linear-gradient/assets/743291/8ff2a78b-f0b1-463a-aa5b-555df2e71360" width="300"> <img src="https://github.com/react-native-linear-gradient/react-native-linear-gradient/assets/743291/9c738be3-6fba-43d5-9c9f-1db1c10fd377" width="300">
</p>
```bash
npm install
```
## Table of Contents
2. Start the app
- [Installation](#installation)
- [Usage and Examples](#examples)
- [Props](#props)
- [Example App](#an-example-app)
- [Troubleshooting](#troubleshooting)
- [Other Platforms](#other-platforms)
```bash
npx expo start
```
## Installation
In the output, you'll find options to open the app in a
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash
npm run reset-project
```sh
yarn add react-native-linear-gradient
```
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
Or, using npm: `npm install react-native-linear-gradient`
## Learn more
## Examples
To learn more about developing your project with Expo, look at the following resources:
[react-native-login](https://github.com/brentvatne/react-native-login) is a
legacy component which showcases the use of `<LinearGradient>`.
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
### Simple
## Join the community
The following code will produce something like this:
Join our community of developers creating universal apps.
![Example code result](https://raw.githubusercontent.com/react-native-community/react-native-linear-gradient/HEAD/images/example.png)
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
```javascript
import LinearGradient from 'react-native-linear-gradient';
// Within your render function
<LinearGradient colors={['#4c669f', '#3b5998', '#192f6a']} style={styles.linearGradient}>
<Text style={styles.buttonText}>
Sign in with Facebook
</Text>
</LinearGradient>
// Later on in your styles..
var styles = StyleSheet.create({
linearGradient: {
flex: 1,
paddingLeft: 15,
paddingRight: 15,
borderRadius: 5
},
buttonText: {
fontSize: 18,
fontFamily: 'Gill Sans',
textAlign: 'center',
margin: 10,
color: '#ffffff',
backgroundColor: 'transparent',
},
});
```
### Horizontal gradient
Using the styles from above, set `start` and `end` like this to make the gradient go from left to right, instead of from top to bottom:
```javascript
<LinearGradient start={{x: 0, y: 0}} end={{x: 1, y: 0}} colors={['#4c669f', '#3b5998', '#192f6a']} style={styles.linearGradient}>
<Text style={styles.buttonText}>
Sign in with Facebook
</Text>
</LinearGradient>
```
### Text gradient (iOS)
On iOS you can use the `MaskedViewIOS` to display text with a gradient. The trick here is to render the text twice; once for the mask, and once to let the gradient have the correct size (hence the `opacity: 0`):
```jsx
<MaskedViewIOS maskElement={<Text style={styles.text} />}>
<LinearGradient colors={['#f00', '#0f0']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}>
<Text style={[styles.text, { opacity: 0 }]} />
</LinearGradient>
</MaskedViewIOS>
```
### Animated Gradient
Check out the [example app](https://github.com/react-native-linear-gradient/react-native-linear-gradient/tree/HEAD/example/) (`git clone` this project, cd into it, npm install, open in Xcode and run) to see how this is done:
![Example with extra props](https://raw.githubusercontent.com/react-native-community/react-native-linear-gradient/HEAD/images/example-animated.gif)
*This gif was created using [licecap](http://www.cockos.com/licecap/) - a great piece of free OSS*
### Transparent Gradient
The use of `transparent` color will most likely not lead to the expected result. `transparent` is actually a transparent black color (`rgba(0, 0, 0, 0)`). If you need a gradient in which the color is "fading", you need to have the same color with changing alpha channel. Example:
```jsx
// RGBA
<LinearGradient colors={['rgba(255, 255, 255, 0)', 'rgba(255, 255, 255, 1)']} {...otherGradientProps} />
// Hex
<LinearGradient colors={['#FFFFFF00', '#FFFFFF']} {...otherGradientProps} />
```
## Props
In addition to regular `View` props, you can also provide additional props to customize your gradient look:
#### colors
An array of at least two color values that represent gradient colors. Example: `['red', 'blue']` sets gradient from red to blue.
#### start
An optional object of the following type: `{ x: number, y: number }`. Coordinates declare the position that the gradient starts at, as a fraction of the overall size of the gradient, starting from the top left corner. Example: `{ x: 0.1, y: 0.1 }` means that the gradient will start 10% from the top and 10% from the left.
#### end
Same as start, but for the end of the gradient.
#### locations
An optional array of numbers defining the location of each gradient color stop, mapping to the color with the same index in `colors` prop. Example: `[0.1, 0.75, 1]` means that first color will take 0% - 10%, second color will take 10% - 75% and finally third color will occupy 75% - 100%.
```javascript
<LinearGradient
start={{x: 0.0, y: 0.25}} end={{x: 0.5, y: 1.0}}
locations={[0,0.5,0.6]}
colors={['#4c669f', '#3b5998', '#192f6a']}
style={styles.linearGradient}>
<Text style={styles.buttonText}>
Sign in with Facebook
</Text>
</LinearGradient>
```
![Example with extra props](https://raw.githubusercontent.com/react-native-community/react-native-linear-gradient/HEAD/images/example-other-props.png)
#### useAngle / angle / angleCenter
You may want to achieve an angled gradient effect, similar to those in image editors like Photoshop.
One issue is that you have to calculate the angle based on the view's size, which only happens asynchronously and will cause unwanted flickr.
In order to do that correctly you can set `useAngle={true} angle={45} angleCenter={{x:0.5,y:0.5}}`, to achieve a gradient with a 45 degrees angle, with its center positioned in the view's exact center.
`useAngle` is used to turn on/off angle based calculation (as opposed to `start`/`end`).
`angle` is the angle in degrees.
`angleCenter` is the center point of the angle (will control the weight and stretch of the gradient like it does in photoshop.
## An example app
You can see this component in action in [brentvatne/react-native-login](https://github.com/brentvatne/react-native-login/blob/HEAD/App/Screens/LoginScreen.js#L58-L62).
## Troubleshooting
### iOS build fails: library not found, "BVLinearGradient" was not found in the UIManager
1. Ensure to run `pod install` before running the app on iOS
2. Ensure you use `ios/**.xcworkspace` file instead of `ios./**.xcodeproj`
### Other
Clearing build caches and reinstalling dependencies sometimes solve some issues. Try next steps:
1. Reinstalling `node_modules` with `rm -rf node_modules && yarn`
2. Clearing Android Gradle cache with `(cd android && ./gradlew clean)`
3. Reinstalling iOS CocoaPods with `(cd ios && rm -rf ./ios/Pods/**) && npx pod-install`
4. Clearing Xcode Build cache (open Xcode and go to Product -> Clean Build Folder)
For other troubleshooting issues, go to [React Native Troubleshooting](https://reactnative.dev/docs/troubleshooting.html)
## Other platforms
- Web: [react-native-web-community/react-native-web-linear-gradient](https://github.com/react-native-web-community/react-native-web-linear-gradient)
## License
MIT
[1]: https://github.com/react-native-linear-gradient/react-native-linear-gradient/workflows/ci/badge.svg
[2]: https://github.com/react-native-linear-gradient/react-native-linear-gradient/actions
[3]: https://img.shields.io/npm/v/react-native-linear-gradient.svg
[4]: https://www.npmjs.com/package/react-native-linear-gradient
[5]: https://img.shields.io/npm/dm/react-native-linear-gradient.svg

View File

@ -0,0 +1,195 @@
# react-native-linear-gradient
A `<LinearGradient>` element for React Native
[![ci][1]][2]
[![npm version][3]][4]
[![npm downloads][5]][4]
<p align="center">
<img src="https://github.com/react-native-linear-gradient/react-native-linear-gradient/assets/743291/8ff2a78b-f0b1-463a-aa5b-555df2e71360" width="300"> <img src="https://github.com/react-native-linear-gradient/react-native-linear-gradient/assets/743291/9c738be3-6fba-43d5-9c9f-1db1c10fd377" width="300">
</p>
## Table of Contents
- [Installation](#installation)
- [Usage and Examples](#examples)
- [Props](#props)
- [Example App](#an-example-app)
- [Troubleshooting](#troubleshooting)
- [Other Platforms](#other-platforms)
## Installation
```sh
yarn add react-native-linear-gradient
```
Or, using npm: `npm install react-native-linear-gradient`
## Examples
[react-native-login](https://github.com/brentvatne/react-native-login) is a
legacy component which showcases the use of `<LinearGradient>`.
### Simple
The following code will produce something like this:
![Example code result](https://raw.githubusercontent.com/react-native-community/react-native-linear-gradient/HEAD/images/example.png)
```javascript
import LinearGradient from 'react-native-linear-gradient';
// Within your render function
<LinearGradient colors={['#4c669f', '#3b5998', '#192f6a']} style={styles.linearGradient}>
<Text style={styles.buttonText}>
Sign in with Facebook
</Text>
</LinearGradient>
// Later on in your styles..
var styles = StyleSheet.create({
linearGradient: {
flex: 1,
paddingLeft: 15,
paddingRight: 15,
borderRadius: 5
},
buttonText: {
fontSize: 18,
fontFamily: 'Gill Sans',
textAlign: 'center',
margin: 10,
color: '#ffffff',
backgroundColor: 'transparent',
},
});
```
### Horizontal gradient
Using the styles from above, set `start` and `end` like this to make the gradient go from left to right, instead of from top to bottom:
```javascript
<LinearGradient start={{x: 0, y: 0}} end={{x: 1, y: 0}} colors={['#4c669f', '#3b5998', '#192f6a']} style={styles.linearGradient}>
<Text style={styles.buttonText}>
Sign in with Facebook
</Text>
</LinearGradient>
```
### Text gradient (iOS)
On iOS you can use the `MaskedViewIOS` to display text with a gradient. The trick here is to render the text twice; once for the mask, and once to let the gradient have the correct size (hence the `opacity: 0`):
```jsx
<MaskedViewIOS maskElement={<Text style={styles.text} />}>
<LinearGradient colors={['#f00', '#0f0']} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}>
<Text style={[styles.text, { opacity: 0 }]} />
</LinearGradient>
</MaskedViewIOS>
```
### Animated Gradient
Check out the [example app](https://github.com/react-native-linear-gradient/react-native-linear-gradient/tree/HEAD/example/) (`git clone` this project, cd into it, npm install, open in Xcode and run) to see how this is done:
![Example with extra props](https://raw.githubusercontent.com/react-native-community/react-native-linear-gradient/HEAD/images/example-animated.gif)
*This gif was created using [licecap](http://www.cockos.com/licecap/) - a great piece of free OSS*
### Transparent Gradient
The use of `transparent` color will most likely not lead to the expected result. `transparent` is actually a transparent black color (`rgba(0, 0, 0, 0)`). If you need a gradient in which the color is "fading", you need to have the same color with changing alpha channel. Example:
```jsx
// RGBA
<LinearGradient colors={['rgba(255, 255, 255, 0)', 'rgba(255, 255, 255, 1)']} {...otherGradientProps} />
// Hex
<LinearGradient colors={['#FFFFFF00', '#FFFFFF']} {...otherGradientProps} />
```
## Props
In addition to regular `View` props, you can also provide additional props to customize your gradient look:
#### colors
An array of at least two color values that represent gradient colors. Example: `['red', 'blue']` sets gradient from red to blue.
#### start
An optional object of the following type: `{ x: number, y: number }`. Coordinates declare the position that the gradient starts at, as a fraction of the overall size of the gradient, starting from the top left corner. Example: `{ x: 0.1, y: 0.1 }` means that the gradient will start 10% from the top and 10% from the left.
#### end
Same as start, but for the end of the gradient.
#### locations
An optional array of numbers defining the location of each gradient color stop, mapping to the color with the same index in `colors` prop. Example: `[0.1, 0.75, 1]` means that first color will take 0% - 10%, second color will take 10% - 75% and finally third color will occupy 75% - 100%.
```javascript
<LinearGradient
start={{x: 0.0, y: 0.25}} end={{x: 0.5, y: 1.0}}
locations={[0,0.5,0.6]}
colors={['#4c669f', '#3b5998', '#192f6a']}
style={styles.linearGradient}>
<Text style={styles.buttonText}>
Sign in with Facebook
</Text>
</LinearGradient>
```
![Example with extra props](https://raw.githubusercontent.com/react-native-community/react-native-linear-gradient/HEAD/images/example-other-props.png)
#### useAngle / angle / angleCenter
You may want to achieve an angled gradient effect, similar to those in image editors like Photoshop.
One issue is that you have to calculate the angle based on the view's size, which only happens asynchronously and will cause unwanted flickr.
In order to do that correctly you can set `useAngle={true} angle={45} angleCenter={{x:0.5,y:0.5}}`, to achieve a gradient with a 45 degrees angle, with its center positioned in the view's exact center.
`useAngle` is used to turn on/off angle based calculation (as opposed to `start`/`end`).
`angle` is the angle in degrees.
`angleCenter` is the center point of the angle (will control the weight and stretch of the gradient like it does in photoshop.
## An example app
You can see this component in action in [brentvatne/react-native-login](https://github.com/brentvatne/react-native-login/blob/HEAD/App/Screens/LoginScreen.js#L58-L62).
## Troubleshooting
### iOS build fails: library not found, "BVLinearGradient" was not found in the UIManager
1. Ensure to run `pod install` before running the app on iOS
2. Ensure you use `ios/**.xcworkspace` file instead of `ios./**.xcodeproj`
### Other
Clearing build caches and reinstalling dependencies sometimes solve some issues. Try next steps:
1. Reinstalling `node_modules` with `rm -rf node_modules && yarn`
2. Clearing Android Gradle cache with `(cd android && ./gradlew clean)`
3. Reinstalling iOS CocoaPods with `(cd ios && rm -rf ./ios/Pods/**) && npx pod-install`
4. Clearing Xcode Build cache (open Xcode and go to Product -> Clean Build Folder)
For other troubleshooting issues, go to [React Native Troubleshooting](https://reactnative.dev/docs/troubleshooting.html)
## Other platforms
- Web: [react-native-web-community/react-native-web-linear-gradient](https://github.com/react-native-web-community/react-native-web-linear-gradient)
## License
MIT
[1]: https://github.com/react-native-linear-gradient/react-native-linear-gradient/workflows/ci/badge.svg
[2]: https://github.com/react-native-linear-gradient/react-native-linear-gradient/actions
[3]: https://img.shields.io/npm/v/react-native-linear-gradient.svg
[4]: https://www.npmjs.com/package/react-native-linear-gradient
[5]: https://img.shields.io/npm/dm/react-native-linear-gradient.svg

1
__mocks__/jestSetup.ts Normal file
View File

@ -0,0 +1 @@
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');

View File

@ -4,14 +4,24 @@ import { Text } from 'react-native';
import { Image, StyleSheet, Platform } from 'react-native';
import { Conversation, Speaker } from '../lib/conversation';
import { language_matrix_entry, Translator } from '../i18n/api';
import ConversationThread from '@/components/ConversationThread';
export default function HomeScreen() {
const [language, setLanguage] = useState<language_matrix_entry | undefined>()
const [conversation, setConversation] = useState<Conversation|undefined>();
return (
language ? <ConversationPlay language={language} /> :
<LanguageSelection onLanguageSelected={(l) => setLanguage(l)} />
conversation ? <ConversationThread conversation={conversation} /> :
<LanguageSelection onLanguageSelected={
(language) => {
setConversation(new Conversation(
new Translator("en", language.code),
{id: "host", language: "en"},
{id: "guest", language: language.code},
))
}
} />
);
}

View File

@ -9,21 +9,21 @@ describe('Conversation', () => {
beforeEach(() => {
const translator = new Translator("en", "es");
const s1: Speaker = { language: "en", id: "s1" };
const s2: Speaker = { id: "s2", language: "es" }
const s1: Speaker = { language: "en", id: "host" };
const s2: Speaker = { id: "guest", language: "es" }
conversation = new Conversation(translator, s1, s2);
});
it('should add a message to the conversation', () => {
conversation.addMessage({ id: "s1", language: "en" }, "Hello");
conversation.addMessage({ id: "host", language: "en" }, "Hello");
expect(conversation.length).toBe(1);
expect(conversation[0].speaker.id).toEqual("s1");
expect(conversation[0].speaker.id).toEqual("host");
expect(conversation[0].text).toEqual("Hello");
});
it('should translate the last message in the conversation', async () => {
conversation.addMessage({ id: "s1", language: "en" }, "Hello");
conversation.addMessage({ id: "host", language: "en" }, "Hello");
await conversation.translateLast();
expect(conversation[conversation.length - 1].translation).toEqual("Hola");

View File

@ -5,33 +5,54 @@ export type Speaker = {
language : string,
}
export class Message {
public translation? : string;
constructor (public text : string, public speaker : Speaker) {}
public onTextUpdate?: (message: Message) => void;
public onTextDone?: (message: Message) => Promise<void>;
public onTranslationDone?: (message: Message) => void;
constructor (public conversation : Conversation, public speaker : Speaker, public text? : string) {}
public async translate(translator : Translator, language? : string) {
if (!this.text) throw new Error("No text")
this.translation = await translator.translate(this.text, language);
}
get otherSpeaker() {
return this.speaker.id === this.conversation.host.id ? this.conversation.guest : this.conversation.host
}
get otherLanguage() {
return this.otherSpeaker.language
}
}
export class Conversation extends Array<Message> {
public onAddMessage? : (conversation : Conversation) => Promise<any>;
public onTranslationDone? : (conversation : Conversation) => Promise<any>;
constructor (
private translator : Translator,
public s1 : Speaker,
public s2 : Speaker,
public host : Speaker,
public guest : Speaker,
) {
super();
}
public addMessage(speaker : Speaker, text : string) {
this.push(new Message(text, speaker));
public addMessage(speaker : Speaker, text? : string) {
this.push(new Message(this, speaker, text));
}
public async translateMessage(i : number) {
if (!this[i]) throw new Error(`${i} is not a valid message number`);
const otherSpeaker = this[i].speaker.id === this.s1.id ? this.s2 : this.s1
console.log(`Translating sentence to %s: %s`, otherSpeaker.language, this[i].text)
await this[i].translate(this.translator, otherSpeaker.language);
console.log(`Translating sentence to %s: %s`, this[i].otherLanguage, this[i].text)
await this[i].translate(this.translator, this[i].otherLanguage);
}
get lastMessage() {
return this[this.length-1]
}
public async translateLast() {

View File

@ -1,6 +1,8 @@
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
'@babel/preset-typescript',
],
};
// module.exports = {
// presets: [
// ['@babel/preset-env', {targets: {node: 'current'}}],
// '@babel/preset-typescript',
// ],
// };
export const presets = ['module:metro-react-native-babel-preset', 'module:@babel/plugin-transform-private-methods',];

View File

@ -1,107 +0,0 @@
interface ConversationPlayProps {
translator: Translator;
}
function ConversationPlay (props : ConversationPlayProps) {
const [language, setLanguage] = useState<string>(languages[0].code);
const [message, setMessage] = useState<string>('');
const [conversation, setConversation] = useState<Conversation | null>(null);
const startConversation = () => {
const speaker: Speaker = { id: "user", language };
const newConversation = new Conversation(translator, speaker);
setConversation(newConversation);
};
const addMessage = async () => {
if (conversation) {
conversation.addMessage({ id: "user", language }, message);
await conversation.translateLast();
setMessage('');
}
};
return (
<View>
<Text>Select Language:</Text>
{languages.map(lang => (
<Button key={lang.code} title={lang.name} onPress={() => setLanguage(lang.code)} />
))}
<Button title="Start Conversation" onPress={startConversation} />
{conversation && (
<>
<TextInput
placeholder="Type a message"
value={message}
onChangeText={setMessage}
/>
<Button title="Send Message" onPress={addMessage} />
{conversation.messages.map((msg, index) => (
<View key={index}>
<Text>{msg.text}</Text>
{msg.translation && <Text>Translation: {msg.translation}</Text>}
</View>
))}
</>
)}
</View>
);
};
export default ConversationPlay;import React, { useState } from 'react';
import { View, Text, TextInput, Button } from 'react-native';
import { Conversation, Speaker, Translator } from './index';
import {LANG_FLAGS} from "@/app/i18n/lang"
interface ConversationPlayProps {
translator: Translator;
}
const ConversationPlay: React.FC<ConversationPlayProps> = ({ translator }) => {
const [language, setLanguage] = useState<string>(languages[0].code);
const [message, setMessage] = useState<string>('');
const [conversation, setConversation] = useState<Conversation | null>(null);
const startConversation = () => {
const speaker: Speaker = { id: "user", language };
const newConversation = new Conversation(translator, speaker);
setConversation(newConversation);
};
const addMessage = async () => {
if (conversation) {
conversation.addMessage({ id: "user", language }, message);
await conversation.translateLast();
setMessage('');
}
};
return (
<View>
<Text>Select Language:</Text>
{languages.map(lang => (
<Button key={lang.code} title={lang.name} onPress={() => setLanguage(lang.code)} />
))}
<Button title="Start Conversation" onPress={startConversation} />
{conversation && (
<>
<TextInput
placeholder="Type a message"
value={message}
onChangeText={setMessage}
/>
<Button title="Send Message" onPress={addMessage} />
{conversation.messages.map((msg, index) => (
<View key={index}>
<Text>{msg.text}</Text>
{msg.translation && <Text>Translation: {msg.translation}</Text>}
</View>
))}
</>
)}
</View>
);
};
export default ConversationPlay;

View File

@ -0,0 +1,40 @@
import React, { useState, useEffect } from 'react';
import { View } from 'react-native';
import { Conversation } from '@/app/lib/conversation';
import MessageBubble from '@/components/ui/MessageBubble';
interface ConversationThreadProps {
conversation: Conversation;
}
const ConversationThread: React.FC<ConversationThreadProps> = ({ conversation }) => {
const [messages, setMessages] = useState<Message[]>(conversation.messages);
useEffect(() => {
const updateMessages = () => {
setMessages([...conversation.messages]);
};
conversation.on('messageAdded', updateMessages);
conversation.on('messageUpdated', updateMessages);
return () => {
conversation.off('messageAdded', updateMessages);
conversation.off('messageUpdated', updateMessages);
};
}, [conversation]);
const renderMessages = () => (
messages.map((message, index) => (
<MessageBubble key={index} message={message} />
))
);
return (
<View style={{ flex: 1 }}>
{renderMessages()}
</View>
);
};
export default ConversationThread;

View File

@ -36,30 +36,6 @@ const intelligence: Record<string, string> = {
H: "High",
};
// This is the configuration schema
const settings: SettingsElement[] = [
{
label: "Host Language",
type: "enum",
// You can override the way the value is displayed
values: ["en", "es", "fr"],
display: (v) => {
return longLang(v);
},
get: confGet.bind(null, "@hostLanguage", ""),
set: confSet.bind(null, "@hostLanguage"),
},
// Choose from a list, uses the standard phone navigation screens
{
label: "Intelligence",
title: "Select Intelligence",
type: "enum",
values: Object.keys(intelligence),
display: (v: string) => intelligence[v],
get: confGet.bind(null, "@int", "M"),
set: confSet.bind(null, "@int"),
},
];
export default function Settings() {
// Simply pass the schema here
@ -68,6 +44,32 @@ export default function Settings() {
const [languages, setLanguages] = useState<language_matrix | undefined>();
const [languagesLoaded, setLanguagesLoaded] = useState<boolean>(false);
// This is the configuration schema
const settings: SettingsElement[] = [
{
label: "Host Language",
type: "enum",
// You can override the way the value is displayed
values: ["en", "es", "fr"],
display: (v) => {
return longLang(v);
},
get: confGet.bind(null, "@hostLanguage", ""),
set: confSet.bind(null, "@hostLanguage"),
},
// Choose from a list, uses the standard phone navigation screens
{
label: "Intelligence",
title: "Select Intelligence",
type: "enum",
values: Object.keys(intelligence),
display: (v: string) => intelligence[v],
get: confGet.bind(null, "@int", "M"),
set: confSet.bind(null, "@int"),
},
];
useEffect(() => {
const fetchData = async () => {
try {
@ -80,7 +82,7 @@ export default function Settings() {
}
};
});
return (
<NavigationContainer>
<View style={styles.container}>

View File

@ -0,0 +1,64 @@
import { Translator } from "@/app/i18n/api";
import { Message, Speaker } from "@/app/lib/conversation";
import { useState } from "react";
import { StyleSheet, Text } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
type MessageProps = {
message: Message;
translator: Translator;
}
const MessageBubble = (props: MessageProps) => {
const [text, setText] = useState(props.message.text);
const [translatedText, setTranslatedText] = useState(props.message.text);
const [isTranslating, setIsTranslating] = useState<boolean>(false);
props.message.onTextUpdate = (message: Message) => {
setText(message.text);
}
props.message.onTextDone = async (message: Message) => {
setIsTranslating(true);
await props.message.translate(props.translator)
}
props.message.onTranslationDone = (message: Message) => {
if (!message.translation) throw new Error("Missing translation");
setTranslatedText(message.translation);
setIsTranslating(false);
}
const spId = props.message.speaker.id
return (
<SafeAreaView>
{text && (
<Text>{text}</Text>
)}
{translatedText &&
<Text>{translatedText}</Text>
}
</SafeAreaView>
)
}
// const bubbleStyle = StyleSheet.create({
// host: {
// },
// guest: {
// },
// })
// const textStyles = StyleSheet.create({
// native: {
// },
// translation: {
// },
// });
export default MessageBubble;

View File

@ -0,0 +1,51 @@
import React, { act } from 'react';
import { render, screen } from '@testing-library/react-native'
import MessageBubble from '@/components/ui/MessageBubble';
import { Conversation, Speaker } from '@/app/lib/conversation';
import {Translator} from '@/app/i18n/api';
import { View } from 'react-native';
describe('Message Component', () => {
const translator = new Translator('en', 'es');
const host : Speaker = {id : "host", language : "en"}
const guest : Speaker = {id : "guest", language: "es"}
let conversation : Conversation;
beforeEach(() => {
jest.clearAllMocks();
conversation = new Conversation(translator, host, guest);
});
it('renders the message text correctly', async () => {
conversation.addMessage(host, "Hello, World!");
const message = conversation[0];
render(<View></View>);
// render(<MessageBubble message={message} />);
expect(await screen.findByText(message.text as string)).toBeOnTheScreen();
});
it('translates the message when target language is provided', async () => {
const translatedText = 'Hola, ¿cómo estás?';
conversation.addMessage(host, "Hello, how are you?");
await conversation.translateLast();
render(<MessageBubble message={conversation[0]} translator={translator} />);
expect(await screen.findByText(translatedText)).toBeOnTheScreen();
});
it('widget still renders pre-translation', async () => {
const text = "Hello, how are you?"
const translatedText = 'Hola, ¿cómo estás?';
conversation.addMessage(host, text);
render(<MessageBubble message={conversation[0]} translator={translator} />);
expect(await screen.findByText(text)).toBeOnTheScreen();
expect(await screen.findByText(translatedText)).not.toBeOnTheScreen();
await act(async () => {
await conversation.translateLast();
});
expect(await screen.findByText(text)).toBeOnTheScreen();
expect(await screen.findByText(translatedText)).toBeOnTheScreen();
});
});

View File

@ -1,17 +1,22 @@
import type { Config } from '@jest/types';
import {resolve} from "path"
// Sync object
const config: Config.InitialOptions = {
verbose: true,
transform: {
"^.+.tsx?$": ["ts-jest", {}],
},
testEnvironment: "node",
// runner: "jest-runner-tsc",
moduleNameMapper: {
'^@/(.*)$': "<rootDir>/$1"
},
resolver: ""
};
// jest.config.ts
import { resolve } from "path"
import { createJsWithBabelPreset, JestConfigWithTsJest } from 'ts-jest'
export default config;
const jsWithBabelPreset = createJsWithBabelPreset({
tsconfig: 'tsconfig.spec.json',
babelConfig: true,
})
const jestConfig: JestConfigWithTsJest = {
preset: 'react-native',
transform: jsWithBabelPreset.transform,
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
// moduleNameMapper: {
// '^@/(.*)$': resolve("<rootDir>", "$1")
// },
transformIgnorePatterns: [
'<rootDir>/node_modules/(react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|@unimodules/.*|native-base|react-native-code-push)',
],
}
export default jestConfig

View File

@ -15,19 +15,19 @@
"@babel/runtime": "^7.26.7",
"@expo/vector-icons": "^14.0.4",
"@mmomtchev/react-native-settings": "^1.1.0",
"@react-native-async-storage/async-storage": "^1.23.1",
"@react-native-async-storage/async-storage": "^1.24.0",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/elements": "^2.2.5",
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.2.0",
"@types/jsdom": "^21.1.7",
"cheerio": "^1.0.0",
"expo-blur": "~14.0.2",
"expo-constants": "~17.0.4",
"expo-blur": "~14.0.3",
"expo-constants": "~17.0.5",
"expo-doctor": "^1.12.5",
"expo-font": "~13.0.3",
"expo-haptics": "~14.0.1",
"expo-linking": "~7.0.4",
"expo-linking": "~7.0.5",
"expo-router": "~4.0.17",
"expo-splash-screen": "~0.29.21",
"expo-sqlite": "~15.0.6",
@ -44,7 +44,8 @@
"react-native-cache": "^2.0.3",
"react-native-country-flag": "^2.0.2",
"react-native-gesture-handler": "~2.20.2",
"react-native-onyx": "^2.0.91",
"react-native-linear-gradient": "^2.8.3",
"react-native-onyx": "^2.0.92",
"react-native-quick-sqlite": "^8.2.7",
"react-native-reanimated": "~3.16.7",
"react-native-safe-area-context": "4.12.0",
@ -52,23 +53,27 @@
"react-native-sqlite-storage": "^6.0.1",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
"ts-node": "^10.9.2"
"ts-node": "^10.9.2",
"whisper.rn": "^0.3.9"
},
"devDependencies": {
"@babel/core": "^7.26.7",
"@babel/plugin-transform-private-methods": "^7.25.9",
"@babel/preset-typescript": "^7.26.0",
"@jest/globals": "^29.7.0",
"@jest/types": "^29.6.3",
"@testing-library/react-native": "^13.0.1",
"@types/cheerio": "^0.22.35",
"@types/jest": "^29.5.14",
"@types/jquery": "^3.5.32",
"@types/react": "~18.3.18",
"@types/react-native-sqlite-storage": "^6.0.5",
"@types/react-test-renderer": "^18.3.1",
"expo": "~52.0.27",
"expo": "~52.0.28",
"jest": "^29.7.0",
"jest-expo": "~52.0.3",
"jest-runner-tsc": "^1.6.0",
"metro-react-native-babel-preset": "^0.77.0",
"react-test-renderer": "18.3.1",
"ts-jest": "^29.2.5",
"typescript": "^5.7.3"

11336
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
{
"extends": "expo/tsconfig.base",
"jsx": "react",
"compilerOptions": {
"strict": true,
"paths": {

7
tsconfig.spec.json Normal file
View File

@ -0,0 +1,7 @@
// tsconfig.spec.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react-native"
}
}