Skip to content

Commit

Permalink
Merge pull request #3283 from tloncorp/db/contact-sync
Browse files Browse the repository at this point in the history
Add Realm + Contact sync
  • Loading branch information
dnbrwstr authored Feb 28, 2024
2 parents 75eef86 + 56993c4 commit 119d7bb
Show file tree
Hide file tree
Showing 13 changed files with 527 additions and 46 deletions.
3 changes: 3 additions & 0 deletions apps/tlon-mobile/android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }

# realm
-keep class io.realm.react.**

# Add any project specific keep options here:
22 changes: 17 additions & 5 deletions apps/tlon-mobile/ios/Landscape.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@
mainGroup = 83CBB9F61A601CBA00E9B192;
packageReferences = (
70A62C5F2A5A6B1A00EBED16 /* XCRemoteSwiftPackageReference "SimpleKeychain" */,
70D386462A6098F800AFB46E /* XCRemoteSwiftPackageReference "Alamofire" */,
70D386462A6098F800AFB46E /* XCRemoteSwiftPackageReference "Alamofire.git" */,
70D3866D2A60A3B300AFB46E /* XCRemoteSwiftPackageReference "UrsusSigil" */,
);
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
Expand Down Expand Up @@ -1177,6 +1177,12 @@
" ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
" ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
" ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
);
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down Expand Up @@ -1250,6 +1256,12 @@
" ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
" ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
" ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
" ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
);
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down Expand Up @@ -1318,7 +1330,7 @@
minimumVersion = 1.0.0;
};
};
70D386462A6098F800AFB46E /* XCRemoteSwiftPackageReference "Alamofire" */ = {
70D386462A6098F800AFB46E /* XCRemoteSwiftPackageReference "Alamofire.git" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/Alamofire.git";
requirement = {
Expand All @@ -1342,7 +1354,7 @@
minimumVersion = 1.0.0;
};
};
70DBBFE32B7C60B50021EA96 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
70DBBFE32B7C60B50021EA96 /* XCRemoteSwiftPackageReference "Alamofire.git" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/Alamofire.git";
requirement = {
Expand All @@ -1368,7 +1380,7 @@
};
70D386472A6098F800AFB46E /* Alamofire */ = {
isa = XCSwiftPackageProductDependency;
package = 70D386462A6098F800AFB46E /* XCRemoteSwiftPackageReference "Alamofire" */;
package = 70D386462A6098F800AFB46E /* XCRemoteSwiftPackageReference "Alamofire.git" */;
productName = Alamofire;
};
70D3866E2A60A3B300AFB46E /* UrsusSigil */ = {
Expand All @@ -1383,7 +1395,7 @@
};
70DBBFE22B7C60B50021EA96 /* Alamofire */ = {
isa = XCSwiftPackageProductDependency;
package = 70DBBFE32B7C60B50021EA96 /* XCRemoteSwiftPackageReference "Alamofire" */;
package = 70DBBFE32B7C60B50021EA96 /* XCRemoteSwiftPackageReference "Alamofire.git" */;
productName = Alamofire;
};
70DBBFE42B7C60B50021EA96 /* UrsusSigil */ = {
Expand Down
8 changes: 7 additions & 1 deletion apps/tlon-mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,8 @@ PODS:
- React-jsi (= 0.73.4)
- React-logger (= 0.73.4)
- React-perflogger (= 0.73.4)
- RealmJS (12.6.0):
- React
- recaptcha-enterprise-react-native (18.4.0):
- React-Core
- RecaptchaEnterprise (= 18.4.0)
Expand Down Expand Up @@ -1289,6 +1291,7 @@ DEPENDENCIES:
- React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- RealmJS (from `../../../node_modules/realm`)
- "recaptcha-enterprise-react-native (from `../../../node_modules/@google-cloud/recaptcha-enterprise-react-native`)"
- "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)"
- RNDeviceInfo (from `../../../node_modules/react-native-device-info`)
Expand Down Expand Up @@ -1468,6 +1471,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/react/utils"
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
RealmJS:
:path: "../../../node_modules/realm"
recaptcha-enterprise-react-native:
:path: "../../../node_modules/@google-cloud/recaptcha-enterprise-react-native"
RNCAsyncStorage:
Expand Down Expand Up @@ -1579,6 +1584,7 @@ SPEC CHECKSUMS:
React-runtimescheduler: 1c054b58fef2ce74cdcbdcd70db190e10f56a617
React-utils: 21a798438d45e70ed9c2e2fe0894ee32ba7b7c5b
ReactCommon: dcc65c813041388dead6c8b477444757425ce961
RealmJS: a62dc7a1f94b888fe9e8712cd650167ad97dc636
recaptcha-enterprise-react-native: 7d63c5bdde3b48996b984a86ac2b536a1d8f5f16
RecaptchaEnterprise: dc302910b77963a0cc6f6908407e30b35268a755
RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21
Expand All @@ -1593,7 +1599,7 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
sqlite3: f163dbbb7aa3339ad8fc622782c2d9d7b72f7e9c
UMAppLoader: 5df85360d65cabaef544be5424ac64672e648482
Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312
Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70

PODFILE CHECKSUM: 82da24eb176d4abdeaf445b3581717ec492dd7e8

Expand Down
2 changes: 2 additions & 0 deletions apps/tlon-mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@react-navigation/bottom-tabs": "^6.5.12",
"@react-navigation/native": "^6.1.7",
"@react-navigation/native-stack": "^6.9.13",
"@realm/react": "^0.6.2",
"@tloncorp/shared": "*",
"@urbit/aura": "^1.0.0",
"classnames": "^2.3.2",
Expand Down Expand Up @@ -71,6 +72,7 @@
"react-native-storage": "^1.0.1",
"react-native-svg": "^14.1.0",
"react-native-webview": "13.6.4",
"realm": "^12.6.0",
"tailwind-rn": "^4.2.0"
},
"devDependencies": {
Expand Down
30 changes: 20 additions & 10 deletions apps/tlon-mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import { useTailwind } from 'tailwind-rn';

import { LoadingSpinner } from './components/LoadingSpinner';
import { ShipProvider, useShip } from './contexts/ship';
import * as db from './db';
import { useDeepLink } from './hooks/useDeepLink';
import { useIsDarkMode } from './hooks/useIsDarkMode';
import { useScreenOptions } from './hooks/useScreenOptions';
import { inviteShipWithLure } from './lib/hostingApi';
import { syncContacts } from './lib/sync';
import { TabStack } from './navigation/TabStack';
import { CheckVerifyScreen } from './screens/CheckVerifyScreen';
import { EULAScreen } from './screens/EULAScreen';
Expand Down Expand Up @@ -55,6 +57,12 @@ const App = ({ wer: initialWer }: Props) => {
const screenOptions = useScreenOptions();
const gotoPath = wer ?? initialWer;

useEffect(() => {
if (isAuthenticated) {
syncContacts();
}
}, [isAuthenticated]);

useEffect(() => {
const unsubscribeFromNetInfo = NetInfo.addEventListener(
({ isConnected }) => {
Expand Down Expand Up @@ -223,17 +231,19 @@ const App = ({ wer: initialWer }: Props) => {
);
};

export default function AnalyticsApp(props: Props) {
export default function ConnectedApp(props: Props) {
const isDarkMode = useIsDarkMode();
return (
<TamaguiProvider defaultTheme={isDarkMode ? 'dark' : 'light'}>
<ShipProvider>
<NavigationContainer theme={isDarkMode ? DarkTheme : DefaultTheme}>
<PostHogProvider client={posthogAsync} autocapture>
<App {...props} />
</PostHogProvider>
</NavigationContainer>
</ShipProvider>
</TamaguiProvider>
<db.RealmProvider>
<TamaguiProvider defaultTheme={isDarkMode ? 'dark' : 'light'}>
<ShipProvider>
<NavigationContainer theme={isDarkMode ? DarkTheme : DefaultTheme}>
<PostHogProvider client={posthogAsync} autocapture>
<App {...props} />
</PostHogProvider>
</NavigationContainer>
</ShipProvider>
</TamaguiProvider>
</db.RealmProvider>
);
}
2 changes: 2 additions & 0 deletions apps/tlon-mobile/src/db/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './realm';
export * from './schemas';
90 changes: 90 additions & 0 deletions apps/tlon-mobile/src/db/realm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createRealmContext } from '@realm/react';
import React from 'react';
import type { PropsWithChildren } from 'react';
import type Realm from 'realm';

import type { SchemaMap, SchemaName } from './schemas';
import { schemas } from './schemas';

// This is a copy of Realm's `UpdateMode` enum. Not ideal, but realm only
// exports `UpdateMode` as a type, which causes a lint error if we try to use it
// directly.
export enum UpdateMode {
Never = 'never',
Modified = 'modified',
All = 'all',
}

// Realm provider setup

const config: Realm.Configuration = {
schema: schemas,
schemaVersion: 0,
};

const {
RealmProvider: BaseRealmProvider,
useObject,
useQuery,
useRealm,
} = createRealmContext(config);

let realmInstance: Realm | null = null;

function realm() {
if (!realmInstance) {
throw new Error('Realm instance not available');
}
return realmInstance;
}

// The only straightforward way to get the realm instance here is to use the
// `realmRef` property. Since the property takes a ref, we use a proxy to
// synchronously mirror the set value to the local `realm` variable.
const realmRefProxy = {
set current(val: Realm | null) {
realmInstance = val;
},
};

const RealmProvider = ({ children }: PropsWithChildren) => {
return (
<BaseRealmProvider realmRef={realmRefProxy}>{children}</BaseRealmProvider>
);
};

export { RealmProvider, useObject, useQuery, useRealm };

// Utility functions

export function createBatch<T extends SchemaName>(
model: T,
data: SchemaMap[T][],
updateMode = UpdateMode.Modified
): SchemaMap[T][] {
return realm().write(() =>
data.map((d) => {
return realm().create<SchemaMap[T]>(model, d, updateMode);
})
);
}

export function create<T extends SchemaName>(
model: T,
data: SchemaMap[T],
updateMode = UpdateMode.Modified
): SchemaMap[T] {
return realm().write(() =>
realm().create<SchemaMap[T]>(model, data, updateMode)
);
}

export function update<T extends SchemaName>(
model: T,
data: Partial<SchemaMap[T]>,
updateMode = UpdateMode.Modified
): SchemaMap[T] {
return realm().write(() =>
realm().create<SchemaMap[T]>(model, data, updateMode)
);
}
35 changes: 35 additions & 0 deletions apps/tlon-mobile/src/db/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type Contact = {
id: string;
nickname: string | null;
bio: string | null;
status: string | null;
color: string | null;
avatarImage: string | null;
coverImage: string | null;
pinnedGroupIds: string[];
};

const contactSchema = {
name: 'Contact',
properties: {
id: 'string',
nickname: 'string?',
bio: 'string?',
status: 'string?',
color: 'string?',
avatarImage: 'string?',
coverImage: 'string?',
pinnedGroupIds: 'string[]',
},
primaryKey: 'id',
};

// Should contain all schemas, will be passed to Realm constructor
export const schemas = [contactSchema];

// Should contain all schema types, used to map Realm object types to TypeScript types
export type SchemaMap = {
Contact: Contact;
};

export type SchemaName = keyof SchemaMap;
10 changes: 10 additions & 0 deletions apps/tlon-mobile/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,13 @@ export const poke = async ({
mark,
json,
});

export const scry = async <T>({ app, path }: { app: string; path: string }) => {
return fetch(`${config.shipUrl}/~/scry/${app}${path}.json`, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
credentials: 'include',
}).then((res) => res.json()) as Promise<T>;
};
47 changes: 47 additions & 0 deletions apps/tlon-mobile/src/lib/contactsApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect, test } from 'vitest';

import { toClientContact, toClientContacts } from './contactsApi';

const inputContact: [string, any] = [
'test',
{
status: 'listening to music',
avatar: null,
cover:
'https://20-urbit.s3.us-west-1.amazonaws.com/ravmel-ropdyl/2021.2.13..00.31.09-Manaslu-crevasses.jpg',
bio: 'happy to chat, send a dm any time',
nickname: 'galen',
color: '0xff.ffff',
groups: [
'~ravmel-ropdyl/audio-video-images',
'~nibset-napwyn/tlon',
'~ravmel-ropdyl/crate',
],
},
];

const outputContact = {
id: 'test',
avatarImage: null,
coverImage:
'https://20-urbit.s3.us-west-1.amazonaws.com/ravmel-ropdyl/2021.2.13..00.31.09-Manaslu-crevasses.jpg',
bio: 'happy to chat, send a dm any time',
nickname: 'galen',
status: 'listening to music',
color: '#ffffff',
pinnedGroupIds: [
'~ravmel-ropdyl/audio-video-images',
'~nibset-napwyn/tlon',
'~ravmel-ropdyl/crate',
],
};

test('converts a contact from server to client format', () => {
expect(toClientContact(...inputContact)).toStrictEqual(outputContact);
});

test('converts an array of contacts from server to client format', () => {
expect(
toClientContacts({ [inputContact[0]]: inputContact[1] })
).toStrictEqual([outputContact]);
});
Loading

0 comments on commit 119d7bb

Please sign in to comment.