trying to figure out jest issue.
This commit is contained in:
parent
0876db2dea
commit
889d898062
215
README.md
215
README.md
@ -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.
|
||||

|
||||
|
||||
- [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:
|
||||
|
||||

|
||||
|
||||
*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>
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### 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
|
||||
|
195
README.react-native-linear-gradient.md
Normal file
195
README.react-native-linear-gradient.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
```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:
|
||||
|
||||

|
||||
|
||||
*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>
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### 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
1
__mocks__/jestSetup.ts
Normal file
@ -0,0 +1 @@
|
||||
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
|
@ -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},
|
||||
))
|
||||
}
|
||||
} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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() {
|
||||
|
@ -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',];
|
@ -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;
|
40
components/ConversationThread.tsx
Normal file
40
components/ConversationThread.tsx
Normal 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;
|
@ -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}>
|
||||
|
64
components/ui/MessageBubble.tsx
Normal file
64
components/ui/MessageBubble.tsx
Normal 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;
|
51
components/ui/__tests__/Message.spec.tsx
Normal file
51
components/ui/__tests__/Message.spec.tsx
Normal 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();
|
||||
});
|
||||
});
|
@ -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
|
19
package.json
19
package.json
@ -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
11336
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"jsx": "react",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
|
7
tsconfig.spec.json
Normal file
7
tsconfig.spec.json
Normal file
@ -0,0 +1,7 @@
|
||||
// tsconfig.spec.json
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-native"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user