Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rn): add Expo quickstart app #47

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
40 changes: 40 additions & 0 deletions react-native-expo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts
android
ios

# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

app-example
251 changes: 251 additions & 0 deletions react-native-expo/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import React, { useState, useEffect, useRef } from "react";
import {
Text,
StyleSheet,
PermissionsAndroid,
Platform,
View,
SafeAreaView,
Alert,
FlatList,
Button,
} from "react-native";
import {
Ditto,
IdentityOnlinePlayground,
StoreObserver,
SyncSubscription,
TransportConfig,
} from "@dittolive/ditto";
import { DITTO_APP_ID, DITTO_PLAYGROUND_TOKEN } from "@env";

import Fab from "./components/Fab";
import NewTaskModal from "./components/NewTaskModal";
import DittoInfo from "./components/DittoInfo";
import DittoSync from "./components/DittoSync";
import TaskDone from "./components/TaskDone";
import EditTaskModal from "./components/EditTaskModal";

type Task = {
id: string;
title: string;
done: boolean;
deleted: boolean;
};

const identity: IdentityOnlinePlayground = {
type: "onlinePlayground",
appID: DITTO_APP_ID,
token: DITTO_PLAYGROUND_TOKEN,
};

async function requestPermissions() {
const permissions = [
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE,
PermissionsAndroid.PERMISSIONS.NEARBY_WIFI_DEVICES,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
];

const granted = await PermissionsAndroid.requestMultiple(permissions);
return Object.values(granted).every(
(result) => result === PermissionsAndroid.RESULTS.GRANTED
);
}

const App = () => {
const ditto = useRef<Ditto | null>(null);
const taskSubscription = useRef<SyncSubscription | null>(null);
const taskObserver = useRef<StoreObserver | null>(null);

const [modalVisible, setModalVisible] = useState(false);
const [syncEnabled, setSyncEnabled] = useState(true);
const [editingTask, setEditingTask] = useState<Task | null>(null);
const [tasks, setTasks] = useState<Task[]>([]);

const toggleSync = () => {
if (syncEnabled) {
ditto.current?.stopSync();
} else {
ditto.current?.startSync();
}
setSyncEnabled(!syncEnabled);
};

const createTask = async (title: string) => {
if (title === "") {
return;
}
await ditto.current?.store.execute("INSERT INTO tasks DOCUMENTS (:task)", {
task: {
title,
done: false,
deleted: false,
},
});
};

const toggleTask = async (task: Task) => {
await ditto.current?.store.execute(
"UPDATE tasks SET done=:done WHERE _id=:id",
{
id: task.id,
done: !task.done,
}
);
};

const deleteTask = async (task: Task) => {
await ditto.current?.store.execute(
"UPDATE tasks SET deleted=true WHERE _id=:id",
{
id: task.id,
}
);
};

const updateTaskTitle = async (taskId: string, newTitle: string) => {
await ditto.current?.store.execute(
"UPDATE tasks SET title=:title WHERE _id=:id",
{
id: taskId,
title: newTitle,
}
);
};

const initDitto = async () => {
try {
ditto.current = new Ditto(identity);

// Initialize transport config
{
const transportsConfig = new TransportConfig();
transportsConfig.peerToPeer.bluetoothLE.isEnabled = true;
transportsConfig.peerToPeer.lan.isEnabled = true;
transportsConfig.peerToPeer.lan.isMdnsEnabled = true;

if (Platform.OS === "ios") {
transportsConfig.peerToPeer.awdl.isEnabled = true;
}
ditto.current.setTransportConfig(transportsConfig);
}

ditto.current.startSync();
taskSubscription.current = ditto.current.sync.registerSubscription(
"SELECT * FROM tasks"
);

// Subscribe to task updates
taskObserver.current = ditto.current.store.registerObserver(
"SELECT * FROM tasks WHERE NOT deleted",
(response) => {
const fetchedTasks: Task[] = response.items.map((doc) => ({
id: doc.value._id,
title: doc.value.title as string,
done: doc.value.done,
deleted: doc.value.deleted,
}));

setTasks(fetchedTasks);
}
);
} catch (error) {
console.error("Error syncing tasks:", error);
}
};

useEffect(() => {
(async () => {
const granted =
Platform.OS === "android" ? await requestPermissions() : true;
if (granted) {
initDitto();
} else {
Alert.alert(
"Permission Denied",
"You need to grant all permissions to use this app."
);
}
})();
}, []);

const renderItem = ({ item }: { item: Task }) => (
<View key={item.id} style={styles.taskContainer}>
<TaskDone checked={item.done} onPress={() => toggleTask(item)} />
<Text style={styles.taskTitle} onLongPress={() => setEditingTask(item)}>
{item.title}
</Text>
<View style={styles.taskButton}>
<Button
title="Delete"
color="#DC2626"
onPress={() => deleteTask(item)}
/>
</View>
</View>
);

return (
<SafeAreaView style={styles.container}>
<DittoInfo appId={identity.appID} token={identity.token} />
<DittoSync value={syncEnabled} onChange={toggleSync} />
<Fab onPress={() => setModalVisible(true)} />
<NewTaskModal
visible={modalVisible}
onRequestClose={() => setModalVisible(false)}
onSubmit={(task) => {
createTask(task);
setModalVisible(false);
}}
onClose={() => setModalVisible(false)}
/>
<EditTaskModal
visible={editingTask !== null}
task={editingTask}
onRequestClose={() => setEditingTask(null)}
onSubmit={(taskId, newTitle) => {
updateTaskTitle(taskId, newTitle);
setEditingTask(null);
}}
onClose={() => setEditingTask(null)}
/>
<FlatList
contentContainerStyle={styles.listContainer}
data={tasks}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
</SafeAreaView>
);
};

const styles = StyleSheet.create({
container: {
height: "100%",
padding: 20,
backgroundColor: "#fff",
},
listContainer: {
gap: 5,
},
taskContainer: {
flex: 1,
gap: 5,
flexDirection: "row",
paddingVertical: 10,
paddingHorizontal: 20,
},
taskTitle: {
fontSize: 20,
alignSelf: "center",
flexGrow: 1,
flexShrink: 1,
},
taskButton: {
flexShrink: 1,
alignSelf: "center",
},
});

export default App;
55 changes: 55 additions & 0 deletions react-native-expo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Welcome to your Expo app 👋

This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
Ditto is already installed.

⚠️ Expo Go and Expo Web are <b>not</b> compatible with Ditto.

## Get started

1. Install dependencies

```bash
yarn
```

2. Generate iOS and Android folders (Expo CNG)

```bash
yarn expo prebuild
```

3. Run either `yarn run android` or `yarn run ios` based on the targeted platform

## Learn more

To learn more about developing your project with Expo, look at the following resources:

- [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.
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)

## Join the community

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.

## Troubleshooting

1. Bumping into something like this?

```sh
const stringWidth = require('string-width');
^

Error [ERR_REQUIRE_ESM]: require() of ES Module /
```

Clean everything and start fresh:

```sh
yarn clean
```
16 changes: 16 additions & 0 deletions react-native-expo/android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# OSX
#
.DS_Store

# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/

# Bundle artifacts
*.jsbundle
Loading
Loading