├── .gitignore ├── README.md ├── animations.ts ├── app.json ├── app ├── (tabs) │ ├── _layout.tsx │ ├── home.tsx │ ├── organize │ │ ├── _layout.tsx │ │ └── index.tsx │ ├── profile │ │ ├── _layout.tsx │ │ └── index.tsx │ └── review │ │ ├── _layout.tsx │ │ └── index.tsx ├── +html.tsx ├── [...missing].tsx ├── _layout.tsx ├── about.tsx ├── block │ └── [id] │ │ ├── connect.tsx │ │ └── index.tsx ├── changelog.tsx ├── collection │ └── [id] │ │ └── index.tsx ├── dev.tsx ├── errors.tsx ├── export.tsx ├── feedback.tsx ├── icons.tsx ├── index.tsx ├── intro.tsx ├── modal.tsx ├── settings.tsx └── support.tsx ├── assets ├── fonts │ └── SpaceMono-Regular.ttf └── images │ ├── arena-inverted.png │ ├── arena.png │ ├── favicon.png │ ├── flower-blue.png │ ├── flower-orange.png │ ├── flower-pink.png │ ├── flower-yellow.png │ ├── icon-clouds.png │ ├── icon-hand.png │ ├── icon-water.png │ ├── icon.png │ ├── placeholder-image.jpg │ ├── spencer-happy-taiwan.png │ └── splash.png ├── babel.config.js ├── builds └── .gitkeep ├── components ├── BlockContent.tsx ├── BlockCreatedByAvatar.tsx ├── BlockDetailView.tsx ├── BlockSummary.tsx ├── BlockTexts.tsx ├── CollectionDetailView.tsx ├── CollectionSelect.tsx ├── CollectionSummary.tsx ├── ConnectionSummary.tsx ├── ContributionsList.tsx ├── CreateCollectionButton.tsx ├── ExternalLink.tsx ├── ImportArenaChannelSelect.tsx ├── MediaView.tsx ├── RapidCreateCollection.tsx ├── RemoteSourceLabel.tsx ├── SelectCollectionsList.tsx ├── SlidingScalePayment.tsx ├── TextForageView.tsx ├── Themed.tsx ├── UsageInfo.tsx └── arena │ ├── ArenaChannelMultiSelect.tsx │ └── ArenaChannelSummary.tsx ├── constants └── Styles.ts ├── expo-env.d.ts ├── hooks ├── useShareIntent.tsx └── useTime.tsx ├── metro.config.js ├── package-lock.json ├── package.json ├── patches ├── expo-config-plugin-ios-share-extension+0.0.4.patch ├── expo-dynamic-app-icon+1.2.0.patch ├── react-native-hold-menu+0.1.6.patch ├── react-native-receive-sharing-intent+2.0.0.patch └── xcode+3.0.1.patch ├── plugins └── withAndroidShareExtension │ ├── constants.js │ ├── index.js │ ├── withAndroidBuildProperties.js │ ├── withAndroidIntentFilters.js │ ├── withAndroidMainActivityAttributes.js │ └── withAndroidMainActivityExtension.js ├── tamagui.config.ts ├── tsconfig.json ├── utils ├── afterAnimations.tsx ├── arena.ts ├── background.ts ├── blobs.ts ├── celebrations.tsx ├── common.ts ├── constants.ts ├── dataTypes.ts ├── date.ts ├── db.tsx ├── db │ └── migrations.ts ├── dbUtils.ts ├── errors.tsx ├── hooks │ ├── useArenaChannelBlocks.ts │ ├── useArenaUserChannels.ts │ └── useContributions.ts ├── index.ts ├── location.tsx ├── mimeTypes.ts ├── mmkv.tsx ├── network.tsx ├── react.ts ├── remote.ts ├── router.ts ├── schemas.sql ├── search.ts ├── url.ts └── user.tsx ├── views ├── ArenaLogin.tsx ├── ChatDetailView.tsx ├── CollectionChatsView.tsx ├── InternalDevTools.tsx ├── ReviewView.tsx └── UncategorizedView.tsx └── website ├── README.md ├── bun.lockb ├── index.html ├── package.json ├── privacy.html ├── public ├── arena.png ├── assets │ └── cardboard.png ├── cover-no-bg.png ├── gather-app-chats.png ├── gather-app-collection-select.png ├── gather-app-icon-clouds.png ├── gather-app-icon-hand.png ├── gather-app-icon-moon.png ├── gather-app-icon-water.png ├── gather-app-organize.png ├── gather-app-review.png ├── gather-app-texts.png ├── gather-collections-screen.png ├── gather-title-sticky-1.png ├── gather-title-sticky-2.png ├── gather-title-sticky-3.png ├── gather-title-sticky-4.png ├── icon.png └── splash.png ├── src ├── App.scss ├── App.tsx ├── assets │ └── cardboard.png ├── components │ └── ImageZoom.tsx ├── index.scss ├── main.tsx └── vite-env.d.ts ├── terms.html ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 38 | # The following patterns were generated by expo-cli 39 | 40 | expo-env.d.ts 41 | # @end expo-cli 42 | 43 | # native code that is generated 44 | ios 45 | android 46 | 47 | # local builds 48 | builds/*.ipa 49 | android-builds/*.aab 50 | 51 | .env 52 | 53 | # credentials 54 | ./utils/credentials.ts 55 | eas.json 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gather 2 | 3 | 4 | https://github.com/user-attachments/assets/66c510f4-7d62-43cc-8b61-f7432d42cb9b 5 | 6 | 7 | a handheld companion for your curiosity, a local-first client for collecting, curating, and cultivating multimedia collections. 8 | 9 | Available on the [App Store]([url](https://apps.apple.com/us/app/gather-handheld-curiosity/id6468843059?platform=iphone)) and [Google Play Store](https://play.google.com/store/apps/details?id=net.tiny_inter.gather&hl=en_US ) 10 | 11 | ## Development 12 | 13 | - run `npm install` to install dependencies 14 | - `npx expo start` starts development servers cross-platform 15 | 16 | ### Adding Data Sources 17 | I still have to do some setup work to make this really easy, but you can refer to all the Are.na code for adapters to making this work. See `arena.ts` for syncing data and arena logic in `db.ts`. 18 | 19 | ### IOS 20 | 21 | if you add any native plugins, make sure to run 22 | 23 | ``` 24 | npm run prebuild 25 | npm run ios 26 | ``` 27 | 28 | ### Are.na 29 | 30 | To develop with Are.na syncing, you must set up a proper .env file with your own Are.na keys. Please message me if you want this, and I can help you set it up 31 | 32 | ### Release 33 | 34 | #### IOS 35 | 36 | in order to release a new version, you have to bump the version in `app.json` and then run 37 | 38 | ```bash 39 | npm run build-ios 40 | ``` 41 | 42 | this will produce a file like `build-1698192300094.ipa` inside the `builds` folder which you can then provide to `eas submit`. 43 | 44 | ```bash 45 | eas submit -p ios 46 | ``` 47 | 48 | provide the filepath and then it will submit it to testflight automatically. 49 | 50 | ## Common Errors 51 | 52 | ### `npm run ios` fails with `CommandError: Failed to build iOS project. "xcodebuild" exited with error code 65. Duplicate tasks error.` 53 | 54 | open `ios` folder in XCode and delete duplicate ShareExtension tasks. re-run. 55 | 56 | ### `npm run build-ios` fails with `See Xcode logs for full errors` 57 | 58 | run EAS_LOCAL_BUILD_SKIP_CLEANUP=1 npm run build-ios to keep logs around in local `logs`. inspect the logs to see what went wrong. 59 | -------------------------------------------------------------------------------- /animations.ts: -------------------------------------------------------------------------------- 1 | import { createAnimations } from "@tamagui/animations-react-native"; 2 | 3 | export const RawAnimations: Parameters[0] = { 4 | bouncy: { 5 | type: "spring", 6 | damping: 10, 7 | mass: 0.9, 8 | stiffness: 100, 9 | }, 10 | lazy: { 11 | type: "spring", 12 | damping: 20, 13 | stiffness: 60, 14 | }, 15 | quick: { 16 | type: "spring", 17 | damping: 20, 18 | mass: 1.2, 19 | stiffness: 250, 20 | }, 21 | }; 22 | 23 | export const animations = createAnimations(RawAnimations); 24 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Gather", 4 | "slug": "gather", 5 | "privacy": "unlisted", 6 | "version": "1.1.49", 7 | "orientation": "portrait", 8 | "icon": "./assets/images/icon.png", 9 | "scheme": "net.tiny-inter.gather", 10 | "userInterfaceStyle": "automatic", 11 | "splash": { 12 | "image": "./assets/images/splash.png", 13 | "resizeMode": "cover", 14 | "backgroundColor": "#BD9361" 15 | }, 16 | "assetBundlePatterns": ["**/*"], 17 | "ios": { 18 | "supportsTablet": true, 19 | "buildNumber": "0", 20 | "bundleIdentifier": "net.tiny-inter.gather", 21 | "config": { 22 | "usesNonExemptEncryption": false 23 | }, 24 | "splash": { 25 | "image": "./assets/images/splash.png", 26 | "resizeMode": "cover", 27 | "backgroundColor": "#BD9361" 28 | }, 29 | "entitlements": { 30 | "com.apple.security.application-groups": [ 31 | "group.net.tiny-inter.gather.widget" 32 | ] 33 | } 34 | }, 35 | "android": { 36 | "permissions": ["android.permission.RECORD_AUDIO"], 37 | "package": "net.tiny_inter.gather", 38 | "versionCode": 112 39 | }, 40 | "web": { 41 | "bundler": "metro", 42 | "output": "static", 43 | "favicon": "./assets/images/favicon.png" 44 | }, 45 | "plugins": [ 46 | "expo-router", 47 | "react-native-iap", 48 | [ 49 | "expo-image-picker", 50 | { 51 | "photosPermission": "The app accesses your photos to let you upload it to the app.", 52 | "cameraPermission": "Allow Gather to access your camera", 53 | "microphonePermission": "Allow Gather to access your microphone", 54 | "recordAudioAndroid": true 55 | } 56 | ], 57 | [ 58 | "expo-location", 59 | { 60 | "locationAlwaysAndWhenInUsePermission": "Allow Gather to use your location to add location metadata to your items." 61 | } 62 | ], 63 | [ 64 | "expo-config-plugin-ios-share-extension", 65 | { 66 | "activationRules": { 67 | "NSExtensionActivationSupportsText": true, 68 | "NSExtensionActivationSupportsWebURLWithMaxCount": 1, 69 | "NSExtensionActivationSupportsWebPageWithMaxCount": 1, 70 | "NSExtensionActivationSupportsImageWithMaxCount": 1 71 | } 72 | } 73 | ], 74 | [ 75 | "expo-build-properties", 76 | { 77 | "ios": { 78 | "deploymentTarget": "15.5" 79 | } 80 | } 81 | ], 82 | [ 83 | "expo-dynamic-app-icon", 84 | { 85 | "moon": { 86 | "image": "./assets/images/icon.png", 87 | "prerendered": true 88 | }, 89 | "water": { 90 | "image": "./assets/images/icon-water.png", 91 | "prerendered": true 92 | }, 93 | "clouds": { 94 | "image": "./assets/images/icon-clouds.png", 95 | "prerendered": true 96 | }, 97 | "hand": { 98 | "image": "./assets/images/icon-hand.png", 99 | "prerendered": true 100 | } 101 | } 102 | ] 103 | ], 104 | "experiments": { 105 | "typedRoutes": true 106 | }, 107 | "extra": { 108 | "router": { 109 | "origin": false 110 | }, 111 | "eas": { 112 | "projectId": "9972ca9d-51d6-4d33-8b10-3d507e901e37" 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Ionicons } from "@expo/vector-icons"; 2 | import FontAwesome from "@expo/vector-icons/FontAwesome"; 3 | import { IconProps } from "@expo/vector-icons/build/createIconSet"; 4 | import { Link, LinkProps, Tabs } from "expo-router"; 5 | import { Pressable, useColorScheme } from "react-native"; 6 | import { XStack, YStack, useTheme } from "tamagui"; 7 | import Colors from "../../constants/Styles"; 8 | import { useSafeAreaInsets } from "react-native-safe-area-context"; 9 | 10 | /** 11 | * You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ 12 | */ 13 | function TabBarIcon(props: { 14 | name: React.ComponentProps["name"]; 15 | color: string; 16 | }) { 17 | return ; 18 | } 19 | 20 | export default function TabLayout() { 21 | const colorScheme = useColorScheme(); 22 | const headerIcons = ; 23 | const insets = useSafeAreaInsets(); 24 | 25 | return ( 26 | 37 | , 42 | headerRight: () => headerIcons, 43 | }} 44 | /> 45 | ( 51 | 52 | ), 53 | headerRight: () => headerIcons, 54 | }} 55 | /> 56 | ( 66 | 67 | ), 68 | headerRight: () => headerIcons, 69 | }} 70 | /> 71 | ( 77 | 78 | ), 79 | headerRight: () => headerIcons, 80 | }} 81 | /> 82 | 83 | ); 84 | } 85 | 86 | export function MainHeaderIcons() { 87 | const theme = useTheme(); 88 | return null; 89 | return ( 90 | 97 | 98 | 99 | {({ pressed }) => ( 100 | 106 | )} 107 | 108 | 109 | 110 | ); 111 | } 112 | 113 | export function HeaderIcon({ 114 | href, 115 | icon, 116 | }: { 117 | href: LinkProps["href"]; 118 | icon: IconProps["name"]; 119 | }) { 120 | const theme = useTheme(); 121 | 122 | return ( 123 | 124 | 125 | 126 | {({ pressed }) => ( 127 | 133 | )} 134 | 135 | 136 | 137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /app/(tabs)/home.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, useLocalSearchParams } from "expo-router"; 2 | import { ChatDetailView } from "../../views/ChatDetailView"; 3 | import { AppSettingType, getAppSetting } from "../settings"; 4 | 5 | export default function HomeScreen() { 6 | const { collectionId } = useLocalSearchParams(); 7 | const defaultCollectionId = getAppSetting(AppSettingType.DefaultCollection); 8 | 9 | return ( 10 | <> 11 | 12 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/(tabs)/organize/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | 3 | export default function OrganizeLayout() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(tabs)/organize/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSafeAreaInsets } from "react-native-safe-area-context"; 2 | import { StyledView } from "../../../components/Themed"; 3 | import { UncategorizedView } from "../../../views/UncategorizedView"; 4 | 5 | export default function OrganizeScreen() { 6 | const insets = useSafeAreaInsets(); 7 | 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/(tabs)/profile/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | 3 | export default function ProfileLayout() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(tabs)/review/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | 3 | export default function ReviewLayout() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/(tabs)/review/index.tsx: -------------------------------------------------------------------------------- 1 | import { SafeAreaView } from "react-native-safe-area-context"; 2 | import { ReviewView } from "../../../views/ReviewView"; 3 | import { afterAnimations } from "../../../utils/afterAnimations"; 4 | 5 | export default function ReviewScreen() { 6 | return ( 7 | 12 | {/* TODO: this takes too long to load rn.. eventually figure out how to do progressive loading on the page */} 13 | {afterAnimations(ReviewView)()} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/+html.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollViewStyleReset } from 'expo-router/html'; 2 | 3 | // This file is web-only and used to configure the root HTML for every 4 | // web page during static rendering. 5 | // The contents of this function only run in Node.js environments and 6 | // do not have access to the DOM or browser APIs. 7 | export default function Root({ children }: { children: React.ReactNode }) { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | {/* 15 | This viewport disables scaling which makes the mobile website act more like a native app. 16 | However this does reduce built-in accessibility. If you want to enable scaling, use this instead: 17 | 18 | */} 19 | 23 | {/* 24 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. 25 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. 26 | */} 27 | 28 | 29 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} 30 |