Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add READMEs to modules (#10306)

authored by

Samuel Newman and committed by
GitHub
52b8201d dddc0227

+1564 -6
+134
modules/BlueskyClip/README.md
··· 1 + # BlueskyClip 2 + 3 + An iOS App Clip implementation for Bluesky starter packs. App Clips are lightweight app experiences that allow users to preview and join Bluesky through starter packs without installing the full app. 4 + 5 + ## What It Does 6 + 7 + BlueskyClip provides a minimal, on-demand iOS app experience for viewing and joining Bluesky starter packs. When a user encounters a starter pack link (e.g., `bsky.app/start/...` or `go.bsky.app/...`), iOS can present the App Clip instead of requiring a full app install. The App Clip: 8 + 9 + 1. Loads the starter pack web page in a WKWebView 10 + 2. Allows users to browse the starter pack content 11 + 3. Presents the App Store overlay when the user decides to join 12 + 4. Passes the starter pack URI to the main app via shared UserDefaults 13 + 14 + ## Architecture 15 + 16 + ### Native iOS Implementation 17 + 18 + The App Clip is a standalone iOS target with its own minimal Swift implementation: 19 + 20 + - **AppDelegate.swift**: Standard app delegate that sets up the view controller and handles URL routing (both direct URL opens and universal links) 21 + - **ViewController.swift**: Main view controller that manages the WKWebView, detects starter pack URLs, and communicates with the web layer 22 + 23 + ### Communication Flow 24 + 25 + ``` 26 + User taps starter pack link 27 + 28 + iOS presents BlueskyClip App Clip 29 + 30 + WKWebView loads bsky.app with ?clip=true parameter 31 + 32 + Web app detects clip mode and sends actions via postMessage 33 + 34 + ViewController receives messages and: 35 + - Presents App Store overlay (action: "present") 36 + - Stores starter pack URI in shared UserDefaults (action: "store") 37 + 38 + User downloads main app 39 + 40 + Main app reads starterPackUri from shared UserDefaults 41 + 42 + Main app displays starter pack onboarding flow 43 + ``` 44 + 45 + ### Key Implementation Details 46 + 47 + **URL Detection** (`isStarterPackUrl`): 48 + - Matches `bsky.app/start/*` and `bsky.app/starter-pack/*` paths (4 path components) 49 + - Matches short links `go.bsky.app/*` (2 path components) 50 + 51 + **WebView Communication** (`WKScriptMessageHandler`): 52 + - Listens for messages on the "onMessage" channel 53 + - Handles two action types: 54 + - `present`: Shows the App Store overlay using `SKOverlay` 55 + - `store`: Writes JSON data to shared UserDefaults with the specified key 56 + 57 + **Data Sharing**: 58 + - Uses UserDefaults suite `group.app.bsky` (App Group) 59 + - Primary key: `starterPackUri` - stores the starter pack URL 60 + - The main app reads this value on launch via `SharedPrefs.getString('starterPackUri')` (see `src/components/hooks/useStarterPackEntry.native.ts`) 61 + 62 + ## Configuration 63 + 64 + ### Build Configuration 65 + 66 + The App Clip target is automatically configured via Expo config plugins located in `/plugins/starterPackAppClipExtension/`: 67 + 68 + - **withStarterPackAppClip.js**: Main plugin that orchestrates all configuration 69 + - **withXcodeTarget.js**: Creates the App Clip target in Xcode with proper build settings 70 + - **withAppEntitlements.js**: Configures main app entitlements for App Clip association 71 + - **withClipEntitlements.js**: Sets up App Clip entitlements (App Groups, parent app identifier, associated domains) 72 + - **withClipInfoPlist.js**: Generates the Info.plist for the App Clip target 73 + - **withFiles.js**: Copies Swift source files and assets from `modules/BlueskyClip/` to the iOS build directory 74 + 75 + ### Entitlements 76 + 77 + **Main App** (`app.entitlements`): 78 + - `com.apple.security.application-groups`: `group.app.bsky` 79 + - `com.apple.developer.associated-appclip-app-identifiers`: Links to the App Clip bundle ID 80 + 81 + **App Clip** (`BlueskyClip.entitlements`): 82 + - `com.apple.security.application-groups`: `group.app.bsky` (for data sharing) 83 + - `com.apple.developer.parent-application-identifiers`: Links to the main app bundle ID 84 + - `com.apple.developer.associated-domains`: Inherits from main app config (for universal links) 85 + 86 + ### Build Settings 87 + 88 + - Deployment target: iOS 15.1+ 89 + - Bundle ID: `[main-app-bundle-id].AppClip` 90 + - Product type: `com.apple.product-type.application.on-demand-install-capable` 91 + - Development team: `B3LX46C5HS` 92 + - Device family: iPhone only (1) 93 + 94 + ## Platform Support 95 + 96 + - **iOS**: Full support via native App Clip 97 + - **Android**: Not applicable (no App Clip equivalent) 98 + - **Web**: Not applicable (web uses standard starter pack landing pages) 99 + 100 + ## Integration with Main App 101 + 102 + The main app detects App Clip-originated starter packs through `useStarterPackEntry` hook: 103 + 104 + **Native** (`src/components/hooks/useStarterPackEntry.native.ts`): 105 + - Reads `starterPackUri` from `SharedPrefs` (App Group) 106 + - Clears the value after reading to prevent re-use 107 + - Sets active starter pack in app state 108 + 109 + **Web** (`src/components/hooks/useStarterPackEntry.ts`): 110 + - Detects `?clip=true` URL parameter 111 + - Extracts starter pack URI from URL 112 + - Sets active starter pack with `isClip: true` flag 113 + 114 + ## Files 115 + 116 + ``` 117 + modules/BlueskyClip/ 118 + ├── AppDelegate.swift # App lifecycle and URL handling 119 + ├── ViewController.swift # WebView management and message handling 120 + └── Images.xcassets/ # App Clip icon assets 121 + ├── AppIcon.appiconset/ 122 + │ ├── App-Icon-1024x1024@1x.png 123 + │ └── Contents.json 124 + └── Contents.json 125 + ``` 126 + 127 + ## Development Notes 128 + 129 + - The App Clip is built as part of the main Xcode project when running `yarn prebuild` 130 + - Source files are copied during the prebuild process, not directly referenced 131 + - Changes to Swift files require running `yarn prebuild` to take effect 132 + - The App Clip shares the same version number as the main app 133 + - App Clips have a 15MB size limit (enforced by Apple) 134 + - Users can convert an App Clip session into a full app install without losing data (via shared App Group)
+135
modules/BlueskyNSE/README.md
··· 1 + # BlueskyNSE 2 + 3 + BlueskyNSE is an iOS Notification Service Extension that processes push notifications before they are displayed to the user. NSE stands for "Notification Service Extension", a native iOS app extension type. 4 + 5 + ## What It Does 6 + 7 + This extension intercepts incoming push notifications and performs processing before displaying them: 8 + 9 + 1. Manages badge counts for app icon 10 + 2. Applies custom notification sounds based on user preferences 11 + 3. Enables notification customization without requiring the main app to be running 12 + 13 + ## How It Works 14 + 15 + When a push notification arrives on iOS, the system can invoke this extension to modify the notification content before displaying it. The extension runs in a separate process from the main app and has strict time limits (approximately 30 seconds) to complete its work. 16 + 17 + ### Architecture 18 + 19 + The extension uses shared UserDefaults (via App Groups) to access preferences set by the main app: 20 + 21 + - **App Group**: `group.app.bsky` allows data sharing between the main app and the extension 22 + - **Shared Preferences**: Stored in UserDefaults suite accessible by both processes 23 + - **Thread Safety**: Uses a dedicated serial DispatchQueue (`NSEPrefsQueue`) to prevent race conditions when multiple notifications arrive simultaneously 24 + 25 + ### Notification Processing Flow 26 + 27 + 1. System receives push notification 28 + 2. `NotificationService.didReceive()` is called 29 + 3. Extension creates mutable copy of notification content 30 + 4. Based on notification type (determined by `reason` field): 31 + - **Chat messages** (`reason == "chat-message"`): Applies custom DM sound if user preference `playSoundChat` is enabled 32 + - **Other notifications**: Increments and applies badge count 33 + 5. Extension delivers modified notification to system via `contentHandler` 34 + 35 + ### Badge Count Management 36 + 37 + Badge counts are managed centrally by the extension: 38 + - Each non-chat notification increments the badge count 39 + - Count is synchronized across notification instances using the serial queue 40 + - Main app can reset the count via the `expo-background-notification-handler` module 41 + 42 + ### Notification Sounds 43 + 44 + Two sound types are supported: 45 + - **Default system sound**: Standard iOS notification sound 46 + - **DM sound**: Custom `dm.aiff` sound file for chat messages 47 + 48 + DM sound only plays if the user has enabled the `playSoundChat` preference in the main app's chat settings. 49 + 50 + ## Key Files 51 + 52 + | File | Purpose | 53 + |------|---------| 54 + | `NotificationService.swift` | Main service extension implementation | 55 + | `BlueskyNSE.entitlements` | iOS entitlements configuration for App Group access | 56 + | `Info.plist` | Extension metadata and configuration | 57 + 58 + ### NotificationService.swift 59 + 60 + Contains two main classes: 61 + 62 + **NotificationService**: The main extension class that implements `UNNotificationServiceExtension` 63 + - `didReceive(_:withContentHandler:)`: Processes incoming notifications 64 + - `serviceExtensionTimeWillExpire()`: Handles timeout scenarios 65 + - Mutation methods for modifying notification content 66 + 67 + **NSEUtil**: Singleton utility class for shared state management 68 + - Provides shared `UserDefaults` instance for the App Group 69 + - Manages serial queue for thread-safe preference access 70 + - Helper methods for notification content manipulation 71 + 72 + ## Configuration 73 + 74 + ### App Group Setup 75 + 76 + The extension requires the `group.app.bsky` App Group to be configured in: 77 + 1. Main app target capabilities 78 + 2. Extension target capabilities (defined in `BlueskyNSE.entitlements`) 79 + 80 + ### Shared Preferences 81 + 82 + The following preferences are shared between the main app and extension: 83 + 84 + | Preference Key | Type | Purpose | 85 + |----------------|------|---------| 86 + | `badgeCount` | Int | Current badge count for app icon | 87 + | `playSoundChat` | Bool | Whether to play sound for chat notifications | 88 + 89 + These are managed by the `expo-background-notification-handler` module in the main app. 90 + 91 + ### Sound Files 92 + 93 + The custom DM sound file (`dm.aiff`) must be included in the extension's bundle. The iOS project configuration handles copying this resource during the build. 94 + 95 + ## Platform Support 96 + 97 + - **iOS**: Fully supported (primary platform for this extension) 98 + - **Android**: Not applicable (Android uses different notification handling mechanisms) 99 + - **Web**: Not applicable (web notifications are handled by browser APIs) 100 + 101 + ## Integration with Main App 102 + 103 + The extension coordinates with the main app through: 104 + 105 + 1. **expo-background-notification-handler** module: Provides JavaScript API for managing shared preferences 106 + 2. **App Group shared storage**: Enables data synchronization between processes 107 + 3. **Push notification payload**: Must include `reason` field to determine notification type 108 + 109 + ### Setting User Preferences 110 + 111 + Users can control notification sounds via the Chat Settings screen (`src/screens/Messages/Settings.tsx`): 112 + 113 + ```typescript 114 + import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 115 + 116 + const {preferences, setPref} = useBackgroundNotificationPreferences() 117 + setPref('playSoundChat', true) // Enable DM sounds 118 + ``` 119 + 120 + ## Limitations 121 + 122 + 1. **Time constraints**: Extension must complete processing within ~30 seconds or the system will terminate it 123 + 2. **Process isolation**: Runs in separate process with limited memory and resources 124 + 3. **iOS only**: Notification Service Extensions are an iOS-specific feature 125 + 4. **Concurrent processing**: Multiple notifications may arrive simultaneously, requiring careful state management 126 + 127 + ## Best Practices 128 + 129 + When modifying this extension: 130 + 131 + 1. Keep processing fast and synchronous when possible 132 + 2. Use the shared serial queue for any UserDefaults mutations 133 + 3. Avoid network requests that could cause timeouts 134 + 4. Always call `contentHandler` with modified content, even on errors 135 + 5. Test with multiple concurrent notifications to verify thread safety
+140
modules/Share-with-Bluesky/README.md
··· 1 + # Share-with-Bluesky 2 + 3 + iOS Share Extension for the Bluesky Social app that enables users to share content from other apps directly to Bluesky. 4 + 5 + ## Overview 6 + 7 + This module implements an iOS Share Extension (Action Extension) that appears in the system share sheet when users tap the share button in other iOS apps. It allows sharing text, URLs, images, and videos to create a new Bluesky post. 8 + 9 + ## Features 10 + 11 + - Share plain text 12 + - Share URLs (web links) 13 + - Share images (up to 4 images, supports PNG, JPG, JPEG, GIF, HEIC) 14 + - Share videos (single video, supports MOV, MP4, M4V) 15 + - Automatic image dimension extraction 16 + - Automatic video dimension extraction 17 + - App group file sharing for media access 18 + 19 + ## Architecture 20 + 21 + ### iOS Share Extension 22 + 23 + The extension is implemented as a native iOS Share Extension using Swift. When a user shares content: 24 + 25 + 1. The `ShareViewController` receives the shared content from the extension context 26 + 2. Content is processed based on its type (text, URL, image, or video) 27 + 3. Media files are copied to a shared App Group container (`group.app.bsky`) for access by the main app 28 + 4. Image and video dimensions are extracted and encoded into the URI 29 + 5. The extension constructs a deep link URL with the content encoded in query parameters 30 + 6. The main Bluesky app is opened with the deep link 31 + 7. The extension completes and dismisses 32 + 33 + ### Deep Link Format 34 + 35 + The extension communicates with the main app using deep links with the `bluesky://` scheme: 36 + 37 + ``` 38 + bluesky://intent/compose?text=<encoded-text> 39 + bluesky://intent/compose?imageUris=<uri1>|<width>|<height>,<uri2>|<width>|<height> 40 + bluesky://intent/compose?videoUri=<uri>|<width>|<height> 41 + ``` 42 + 43 + The scheme can be customized by setting the `MainAppScheme` key in `Info.plist` to support forks. 44 + 45 + ### Main App Integration 46 + 47 + The main app handles these deep links in `src/lib/hooks/useIntentHandler.ts`: 48 + 49 + - Parses the deep link parameters 50 + - Validates image/video URIs for security (filters out external URLs) 51 + - Opens the composer with the pre-populated content 52 + - Supports up to 4 images or 1 video per share 53 + 54 + ## Key Files 55 + 56 + ### Module Files 57 + 58 + - `ShareViewController.swift` - Main view controller that handles share requests and processes content 59 + - `Info.plist` - Extension configuration (activation rules, supported content types) 60 + - `Share-with-Bluesky.entitlements` - App group entitlements for shared file access 61 + 62 + ### App Integration 63 + 64 + - `src/lib/hooks/useIntentHandler.ts` - Main app hook that handles incoming deep links 65 + - `android/app/src/main/AndroidManifest.xml` - Android share intent configuration (lines 57-76) 66 + 67 + ## Configuration 68 + 69 + ### Supported Content Types 70 + 71 + Defined in `Info.plist` under `NSExtensionActivationRule`: 72 + 73 + - Text: Plain text strings 74 + - Web URLs: Up to 1 URL 75 + - Images: Up to 10 images 76 + - Videos: Up to 1 video 77 + 78 + ### App Group 79 + 80 + The extension uses the `group.app.bsky` App Group identifier to share files with the main app. This is configured in: 81 + 82 + - `Share-with-Bluesky.entitlements` 83 + - Main app's entitlements file 84 + 85 + ### Custom Scheme 86 + 87 + The `MainAppScheme` in `Info.plist` defaults to `bluesky` but can be changed for forks to use a custom URL scheme. 88 + 89 + ## Platform Support 90 + 91 + - iOS: Native Share Extension (this module) 92 + - Android: Native share intents handled via MainActivity intent filters in AndroidManifest.xml 93 + - Web: Not applicable (browser share APIs use different mechanisms) 94 + 95 + ## Implementation Details 96 + 97 + ### Image Processing 98 + 99 + When images are shared: 100 + 101 + 1. Images are loaded from the extension's temporary directory or as UIImage objects 102 + 2. Images are converted to JPEG format at maximum quality 103 + 3. Dimensions are extracted from the UIImage 104 + 4. Files are saved to the App Group container with unique names 105 + 5. URIs are formatted as `<file-url>|<width>|<height>` 106 + 107 + ### Video Processing 108 + 109 + When videos are shared: 110 + 111 + 1. Videos are copied from the source URL to the App Group container 112 + 2. AVURLAsset is used to extract video track dimensions 113 + 3. Track dimensions are adjusted for video rotation using preferredTransform 114 + 4. URI is formatted as `<file-url>|<width>|<height>` 115 + 116 + ### Security 117 + 118 + - External URLs in image URIs are filtered out in the main app to prevent potential security issues 119 + - Only file:// URLs from the App Group container are accepted 120 + - URI format is validated with a regex pattern before processing 121 + 122 + ## Development 123 + 124 + This module is built as part of the main Xcode project. The extension target is included in the iOS build configuration. 125 + 126 + To modify the extension: 127 + 128 + 1. Open the Xcode project in `/ios` 129 + 2. Navigate to the Share-with-Bluesky target 130 + 3. Edit `ShareViewController.swift` for logic changes 131 + 4. Edit `Info.plist` for configuration changes 132 + 5. Rebuild the iOS app 133 + 134 + ## Limitations 135 + 136 + - Images: Maximum of 4 images per share (limited in main app handler) 137 + - Videos: Only 1 video per share 138 + - Mixed media: Cannot share images and videos together 139 + - File size: No explicit limits, but large files may cause issues 140 + - Formats: Only supports common image/video formats listed in constants
+248
modules/bottom-sheet/README.md
··· 1 + # Bottom Sheet Expo Module 2 + 3 + A custom Expo module that provides native bottom sheet functionality for iOS and Android, using platform-specific native bottom sheet implementations (UISheetPresentationController on iOS, Material BottomSheetDialog on Android). 4 + 5 + ## Overview 6 + 7 + This module wraps native bottom sheet components to provide a React Native interface with cross-platform consistency. It uses native presentation APIs rather than JavaScript-based animations for better performance and native behavior. 8 + 9 + Key features: 10 + - Native bottom sheet presentation on iOS and Android 11 + - Automatic content height detection (no JS bridge round-trip) 12 + - Configurable snap points (hidden, partial, full) 13 + - Drag-to-dismiss with prevention controls 14 + - Portal-based rendering for proper z-index layering 15 + - Edge-to-edge support on modern Android versions 16 + - iOS 26+ zoom transition support 17 + 18 + ## Platform Support 19 + 20 + - **iOS**: Uses `UISheetPresentationController` (iOS 15+) 21 + - **Android**: Uses Material Design `BottomSheetDialog` with `BottomSheetBehavior` 22 + - **Web**: Not supported (throws error) 23 + 24 + ## Architecture 25 + 26 + ### TypeScript Layer 27 + 28 + The module exposes a React component that handles rendering and state management: 29 + 30 + - **BottomSheet.tsx** (Native): Main component wrapping the native view 31 + - **BottomSheet.web.tsx** (Web): Stub that throws an error 32 + - **BottomSheetNativeComponent.tsx**: React wrapper with portal integration 33 + - **BottomSheetPortal.tsx**: Portal system for rendering sheets above app content 34 + - **Portal.tsx**: Generic portal implementation for managing component hierarchy 35 + 36 + The component uses a class-based approach to expose imperative methods (`present()`, `dismiss()`, `dismissAll()`). 37 + 38 + ### Native Layer 39 + 40 + #### iOS Implementation 41 + 42 + - **BottomSheetModule.swift**: Expo module definition with event handlers and prop bindings 43 + - **SheetView.swift**: Main view component that creates and manages `SheetViewController` 44 + - Observes content height via KVO (Key-Value Observing) on bounds 45 + - Manages sheet lifecycle and state transitions 46 + - Implements `UISheetPresentationControllerDelegate` for drag events 47 + - **SheetViewController.swift**: UIViewController subclass with sheet presentation 48 + - Configures detents (snap points) based on content height 49 + - Handles iOS 26+ safe area adjustments for floating sheet style 50 + - Animates detent changes when content resizes 51 + - **SheetManager.swift**: Singleton that tracks all active sheets with weak references 52 + - **Util.swift**: Helper for calculating screen height minus safe area insets 53 + 54 + #### Android Implementation 55 + 56 + - **BottomSheetModule.kt**: Expo module definition mirroring iOS functionality 57 + - **BottomSheetView.kt**: Main view component managing Material BottomSheetDialog 58 + - Uses `OnLayoutChangeListener` to observe content height natively 59 + - Configures `BottomSheetBehavior` for drag and snap behavior 60 + - Handles edge-to-edge display across Android versions (API 29-35+) 61 + - Preserves status/nav bar appearance from host activity 62 + - **DialogRootViewGroup.kt**: Custom ViewGroup acting as RootView for the dialog 63 + - Forwards touch events to React Native event system 64 + - Updates shadow node size to match window dimensions 65 + - Based on React Native's ReactModalHostView pattern 66 + - **SheetManager.kt**: Singleton for tracking sheets (same pattern as iOS) 67 + 68 + ### Content Height Detection 69 + 70 + Both platforms detect content height changes natively without JS bridge round-trips: 71 + 72 + - **iOS**: KVO observation on the content view's `bounds` property 73 + - **Android**: `OnLayoutChangeListener` on child views (catches React Native's direct `layout()` calls) 74 + 75 + This eliminates layout jank when content changes (e.g., keyboard appearance, dynamic content loading). 76 + 77 + ## Props 78 + 79 + ```typescript 80 + interface BottomSheetViewProps { 81 + children: React.ReactNode 82 + 83 + // Appearance 84 + cornerRadius?: number 85 + backgroundColor?: ColorValue 86 + containerBackgroundColor?: ColorValue 87 + 88 + // Behavior 89 + preventDismiss?: boolean // Disable swipe-to-dismiss 90 + preventExpansion?: boolean // Lock to initial height (no full-screen) 91 + disableDrag?: boolean // Disable drag handle (Android only) 92 + fullHeight?: boolean // Start at full screen height 93 + 94 + // Height constraints 95 + minHeight?: number // Minimum height in dp 96 + maxHeight?: number // Maximum height in dp 97 + 98 + // iOS 26+ transition 99 + sourceViewTag?: number // View tag for zoom transition origin 100 + 101 + // Events 102 + onAttemptDismiss?: (event: BottomSheetAttemptDismissEvent) => void 103 + onSnapPointChange?: (event: BottomSheetSnapPointChangeEvent) => void 104 + onStateChange?: (event: BottomSheetStateChangeEvent) => void 105 + } 106 + ``` 107 + 108 + ## States and Snap Points 109 + 110 + ### States 111 + - `closed`: Sheet is dismissed 112 + - `closing`: Sheet is animating closed 113 + - `open`: Sheet is fully visible 114 + - `opening`: Sheet is animating open 115 + 116 + ### Snap Points 117 + - `Hidden` (0): Dismissed 118 + - `Partial` (1): Half-expanded / content height 119 + - `Full` (2): Expanded to screen height 120 + 121 + ## Usage 122 + 123 + ### Basic Example 124 + 125 + ```tsx 126 + import {BottomSheet, BottomSheetProvider, BottomSheetOutlet} from '@modules/bottom-sheet' 127 + 128 + // In your app root: 129 + function App() { 130 + return ( 131 + <BottomSheetProvider> 132 + <YourApp /> 133 + <BottomSheetOutlet /> 134 + </BottomSheetProvider> 135 + ) 136 + } 137 + 138 + // In a component: 139 + function MyComponent() { 140 + const sheetRef = useRef<BottomSheet>(null) 141 + 142 + const openSheet = () => { 143 + sheetRef.current?.present() 144 + } 145 + 146 + const closeSheet = () => { 147 + sheetRef.current?.dismiss() 148 + } 149 + 150 + return ( 151 + <> 152 + <Button onPress={openSheet} title="Open Sheet" /> 153 + 154 + <BottomSheet 155 + ref={sheetRef} 156 + cornerRadius={16} 157 + backgroundColor="white" 158 + onStateChange={(e) => console.log(e.nativeEvent.state)} 159 + > 160 + <View style={{padding: 20}}> 161 + <Text>Sheet content</Text> 162 + <Button onPress={closeSheet} title="Close" /> 163 + </View> 164 + </BottomSheet> 165 + </> 166 + ) 167 + } 168 + ``` 169 + 170 + ### Nested Sheets 171 + 172 + The module supports nesting sheets by using `BottomSheetPortalProvider` within sheet content: 173 + 174 + ```tsx 175 + <BottomSheet ref={outerSheetRef}> 176 + <BottomSheetPortalProvider> 177 + <Button onPress={() => innerSheetRef.current?.present()} /> 178 + <BottomSheet ref={innerSheetRef}> 179 + <Text>Inner sheet content</Text> 180 + </BottomSheet> 181 + </BottomSheetPortalProvider> 182 + </BottomSheet> 183 + ``` 184 + 185 + ### Dismiss All Sheets 186 + 187 + ```tsx 188 + import {BottomSheetNativeComponent} from '@modules/bottom-sheet' 189 + 190 + BottomSheetNativeComponent.dismissAll() 191 + ``` 192 + 193 + ## Key Implementation Details 194 + 195 + ### iOS Specific 196 + 197 + 1. **iOS 15 Compatibility**: On iOS 15, custom detents are not available, so the module uses `.medium()` detent and applies extra styling to prevent visual issues. 198 + 199 + 2. **iOS 26+ Zoom Transitions**: When `sourceViewTag` is provided on iOS 26+, the sheet zooms from the specified view. 200 + 201 + 3. **Detent Selection**: The module automatically chooses between custom detents, `.medium()`, and `.large()` based on content height and screen size. 202 + 203 + ### Android Specific 204 + 205 + 1. **Edge-to-Edge**: The module handles edge-to-edge display correctly across API levels: 206 + - API 35+: Mandatory edge-to-edge 207 + - API 30-34: Uses `currentWindowMetrics` 208 + - API <30: Uses deprecated `getRealSize()` 209 + 210 + 2. **Status/Nav Bar Appearance**: Preserves light/dark appearance from the host activity and reapplies it to the sheet dialog. 211 + 212 + 3. **Drag Handling**: On full-height sheets with `preventDismiss`, dragging is disabled to prevent accidental dismissal (since there's no half-expanded snap point to land on). 213 + 214 + 4. **Layout Updates During Gestures**: Content height changes are deferred during drag gestures to prevent fighting the user's input. 215 + 216 + ### Platform Differences 217 + 218 + - **cornerRadius**: Applied to sheet on iOS, to content wrapper on Android (Android clips with `overflow: hidden`) 219 + - **disableDrag**: Android-only prop (iOS drag behavior is controlled via `preventDismiss` + `preventExpansion`) 220 + - **sourceViewTag**: iOS 26+ only (ignored on Android) 221 + 222 + ## Files Reference 223 + 224 + ### TypeScript 225 + - `index.ts` - Public API exports 226 + - `src/BottomSheet.types.ts` - TypeScript type definitions 227 + - `src/BottomSheet.tsx` - Native component (re-export) 228 + - `src/BottomSheet.web.tsx` - Web stub 229 + - `src/BottomSheetNativeComponent.tsx` - Native wrapper with portal integration 230 + - `src/BottomSheetNativeComponent.web.tsx` - Web stub for native component 231 + - `src/BottomSheetPortal.tsx` - Portal context and providers 232 + - `src/lib/Portal.tsx` - Generic portal implementation 233 + 234 + ### iOS 235 + - `ios/BottomSheetModule.swift` - Module definition 236 + - `ios/SheetView.swift` - Main view implementation 237 + - `ios/SheetViewController.swift` - View controller for sheet presentation 238 + - `ios/SheetManager.swift` - Singleton for tracking active sheets 239 + - `ios/Util.swift` - Screen height utility 240 + 241 + ### Android 242 + - `android/src/main/java/expo/modules/bottomsheet/BottomSheetModule.kt` - Module definition 243 + - `android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt` - Main view implementation 244 + - `android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt` - Dialog root view group 245 + - `android/src/main/java/expo/modules/bottomsheet/SheetManager.kt` - Sheet tracking singleton 246 + 247 + ### Configuration 248 + - `expo-module.config.json` - Expo module configuration
+162
modules/expo-background-notification-handler/README.md
··· 1 + # expo-background-notification-handler 2 + 3 + A custom Expo module for managing shared notification preferences and handling background notifications in the Bluesky Social app. This module enables communication between the main app and notification service extensions through shared storage. 4 + 5 + ## Purpose 6 + 7 + This module solves a critical problem in native notification handling: notification service extensions run in a separate process from the main app and cannot directly access React Native state or APIs. The module provides a bridge by storing notification preferences in shared storage that both the main app and notification service extension can access. 8 + 9 + The primary use case is storing user preferences (like notification sound settings) while the app is foregrounded or backgrounded, minimizing the need for background fetches when processing notifications. 10 + 11 + ## Platform Support 12 + 13 + - **iOS**: Full support via UserDefaults with App Groups 14 + - **Android**: Full support via SharedPreferences 15 + - **Web**: Stub implementation (no-op) 16 + 17 + ## Architecture 18 + 19 + ### iOS Implementation 20 + 21 + Uses iOS App Groups (`group.app.bsky`) to share UserDefaults between the main app and the notification service extension. This allows the notification service extension to read preferences set by the main app without launching the app. 22 + 23 + **Key Files:** 24 + - `ios/ExpoBackgroundNotificationHandlerModule.swift` - Native module implementation 25 + - `ios/ExpoBackgroundNotificationHandler.podspec` - CocoaPods specification 26 + 27 + ### Android Implementation 28 + 29 + Uses SharedPreferences with Firebase Cloud Messaging (FCM) to handle background notifications. The module tracks app foreground/background state and conditionally processes notifications based on whether the app is foregrounded. 30 + 31 + **Key Files:** 32 + - `android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt` - Expo module definition 33 + - `android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt` - SharedPreferences wrapper 34 + - `android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt` - Notification processing logic 35 + - `android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandlerInterface.kt` - Interface for showing notifications 36 + - `android/build.gradle` - Build configuration 37 + 38 + ### TypeScript/React API 39 + 40 + **Key Files:** 41 + - `index.ts` - Module entry point 42 + - `src/ExpoBackgroundNotificationHandlerModule.ts` - Native module binding (iOS/Android) 43 + - `src/ExpoBackgroundNotificationHandlerModule.web.ts` - Web stub 44 + - `src/ExpoBackgroundNotificationHandler.types.ts` - TypeScript type definitions 45 + - `src/BackgroundNotificationHandlerProvider.tsx` - React Context provider for preferences 46 + 47 + ## Stored Preferences 48 + 49 + The module manages the following notification preferences: 50 + 51 + ```typescript 52 + { 53 + playSoundChat: boolean, // Currently exposed to TypeScript 54 + playSoundFollow: boolean, // Native only (not yet exposed) 55 + playSoundLike: boolean, // Native only (not yet exposed) 56 + playSoundMention: boolean, // Native only (not yet exposed) 57 + playSoundQuote: boolean, // Native only (not yet exposed) 58 + playSoundReply: boolean, // Native only (not yet exposed) 59 + playSoundRepost: boolean, // Native only (not yet exposed) 60 + mutedThreads: [String: [String]], // iOS only 61 + badgeCount: number // iOS only 62 + } 63 + ``` 64 + 65 + Default values are initialized when the module is created, with most sound preferences defaulting to `false` except `playSoundChat` which defaults to `true`. 66 + 67 + ## API 68 + 69 + ### Core Methods 70 + 71 + ```typescript 72 + // Get all preferences 73 + getAllPrefsAsync(): Promise<BackgroundNotificationHandlerPreferences> 74 + 75 + // Get individual values 76 + getBoolAsync(forKey: string): Promise<boolean> 77 + getStringAsync(forKey: string): Promise<string> 78 + getStringArrayAsync(forKey: string): Promise<string[]> 79 + 80 + // Set individual values 81 + setBoolAsync(forKey: string, value: boolean): Promise<void> 82 + setStringAsync(forKey: string, value: string): Promise<void> 83 + setStringArrayAsync(forKey: string, value: string[]): Promise<void> 84 + 85 + // Array manipulation 86 + addToStringArrayAsync(forKey: string, value: string): Promise<void> 87 + removeFromStringArrayAsync(forKey: string, value: string): Promise<void> 88 + addManyToStringArrayAsync(forKey: string, value: string[]): Promise<void> 89 + removeManyFromStringArrayAsync(forKey: string, value: string[]): Promise<void> 90 + 91 + // Badge count (iOS only) 92 + setBadgeCountAsync(count: number): Promise<void> 93 + ``` 94 + 95 + ### React Context API 96 + 97 + The module provides a React Context provider for managing preferences in the app: 98 + 99 + ```typescript 100 + import { 101 + BackgroundNotificationPreferencesProvider, 102 + useBackgroundNotificationPreferences, 103 + } from 'expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 104 + 105 + function App() { 106 + return ( 107 + <BackgroundNotificationPreferencesProvider> 108 + <YourApp /> 109 + </BackgroundNotificationPreferencesProvider> 110 + ) 111 + } 112 + 113 + function SettingsScreen() { 114 + const {preferences, setPref} = useBackgroundNotificationPreferences() 115 + 116 + return ( 117 + <Toggle 118 + value={preferences.playSoundChat} 119 + onValueChange={(value) => setPref('playSoundChat', value)} 120 + /> 121 + ) 122 + } 123 + ``` 124 + 125 + ## Android Notification Handling 126 + 127 + The Android implementation includes logic for processing notifications while the app is backgrounded: 128 + 129 + - **Chat messages**: Applies custom notification channels based on `playSoundChat` preference 130 + - Sound enabled: Uses `chat-messages` channel (or `dm.mp3` sound on older Android) 131 + - Sound disabled: Uses `chat-messages-muted` channel 132 + 133 + - **Other notification types**: On Android Oreo+ (API 26+), assigns notifications to channels based on reason: 134 + - Supported reasons: `like`, `repost`, `follow`, `mention`, `reply`, `quote`, `like-via-repost`, `repost-via-repost`, `subscribed-post` 135 + - Each reason maps to its corresponding notification channel 136 + 137 + When the app is foregrounded, the module defers to `expo-notifications` for notification handling. 138 + 139 + ## Configuration 140 + 141 + ### iOS 142 + 143 + Requires App Group entitlement configured in Xcode: 144 + - App Group ID: `group.app.bsky` 145 + 146 + ### Android 147 + 148 + Requires Firebase Cloud Messaging (FCM) integration: 149 + - Dependency: `com.google.firebase:firebase-messaging-ktx:24.0.0` 150 + - SharedPreferences name: `xyz.blueskyweb.app` 151 + 152 + ## Usage in the App 153 + 154 + The module is used to: 155 + 156 + 1. Store notification preferences that need to be accessed by notification service extensions 157 + 2. Track app foreground/background state on Android 158 + 3. Process and mutate notification payloads based on user preferences before display 159 + 4. Manage notification badge counts on iOS 160 + 5. Handle thread muting and other notification filtering logic 161 + 162 + By keeping preferences in shared storage, the notification service extension can make intelligent decisions about notification presentation without waking up the React Native runtime or making network requests.
+167
modules/expo-bluesky-gif-view/README.md
··· 1 + # expo-bluesky-gif-view 2 + 3 + An Expo module for displaying animated GIFs and WebP images with optimized performance and playback controls. 4 + 5 + ## Overview 6 + 7 + This module provides a custom view component for rendering animated GIFs with support for: 8 + 9 + - Autoplay control 10 + - Placeholder images while loading 11 + - Programmatic playback control (play/pause/toggle) 12 + - Image prefetching 13 + - Efficient memory management 14 + - Player state change events 15 + 16 + ## Platform Support 17 + 18 + - iOS (13.4+) 19 + - Android (API 21+) 20 + - Web 21 + 22 + ## Architecture 23 + 24 + The module uses native platform libraries for optimal GIF rendering performance: 25 + 26 + ### iOS Implementation 27 + 28 + - **Library**: SDWebImage with SDWebImageWebPCoder 29 + - **Key Files**: 30 + - `ios/GifView.swift` - Main view implementation using `SDAnimatedImageView` 31 + - `ios/ExpoBlueskyGifViewModule.swift` - Module definition and prop bindings 32 + - `ios/Util.swift` - Cache configuration utilities 33 + 34 + **Approach**: Uses `SDAnimatedImageView` for hardware-accelerated GIF rendering. Images are cached to disk only (not memory) to avoid performance issues with `SDAnimatedImage` when loaded from memory. The view automatically cancels pending requests when scrolled off-screen and resumes loading when visible. 35 + 36 + ### Android Implementation 37 + 38 + - **Library**: Glide 39 + - **Key Files**: 40 + - `android/src/main/java/expo/modules/blueskygifview/GifView.kt` - Main view implementation 41 + - `android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt` - Module definition 42 + - `android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt` - Custom ImageView with playback control 43 + 44 + **Approach**: Uses Glide's disk cache strategy for loading animated GIFs. Placeholders are loaded with `skipMemoryCache(true)` to avoid cache bloat. The custom `AppCompatImageViewExtended` detects when animations are loaded via `onDraw` and manages the `Animatable` drawable lifecycle. 45 + 46 + ### Web Implementation 47 + 48 + - **Library**: Native HTML5 `<video>` element 49 + - **Key File**: `src/GifView.web.tsx` 50 + 51 + **Approach**: Uses a looping, muted video element to display GIFs. This provides better performance than image-based approaches on the web. The implementation tracks load state to fire the `onPlayerStateChange` event only once (since `onCanPlay` fires on every loop). 52 + 53 + ## Usage 54 + 55 + ```tsx 56 + import {GifView} from 'expo-bluesky-gif-view' 57 + 58 + function MyComponent() { 59 + const gifRef = React.useRef<GifView>(null) 60 + 61 + return ( 62 + <GifView 63 + source="https://example.com/animated.gif" 64 + placeholderSource="https://example.com/thumbnail.jpg" 65 + autoplay={true} 66 + onPlayerStateChange={(event) => { 67 + console.log('Playing:', event.nativeEvent.isPlaying) 68 + console.log('Loaded:', event.nativeEvent.isLoaded) 69 + }} 70 + ref={gifRef} 71 + /> 72 + ) 73 + } 74 + ``` 75 + 76 + ## API 77 + 78 + ### Props 79 + 80 + - `source?: string` - URL of the animated GIF/WebP 81 + - `placeholderSource?: string` - URL of a static placeholder image to show while loading 82 + - `autoplay?: boolean` - Whether to start playing automatically (default: true) 83 + - `onPlayerStateChange?: (event: GifViewStateChangeEvent) => void` - Callback fired when playback state changes 84 + 85 + ### Methods 86 + 87 + All methods are async and return a Promise: 88 + 89 + ```tsx 90 + await gifRef.current?.playAsync() 91 + await gifRef.current?.pauseAsync() 92 + await gifRef.current?.toggleAsync() 93 + ``` 94 + 95 + ### Static Methods 96 + 97 + ```tsx 98 + // Prefetch GIFs into the cache (not supported on web) 99 + await GifView.prefetchAsync([ 100 + 'https://example.com/gif1.gif', 101 + 'https://example.com/gif2.gif' 102 + ]) 103 + ``` 104 + 105 + ## Configuration 106 + 107 + ### iOS Dependencies 108 + 109 + The module requires SDWebImage and SDWebImageWebPCoder: 110 + 111 + ```ruby 112 + # ios/ExpoBlueskyGifView.podspec 113 + s.dependency 'SDWebImage', '~> 5.21.0' 114 + s.dependency 'SDWebImageWebPCoder', '~> 0.14.6' 115 + ``` 116 + 117 + ### Android Dependencies 118 + 119 + The module uses Glide, kept in sync with expo-image version: 120 + 121 + ```gradle 122 + # android/build.gradle 123 + implementation 'com.github.bumptech.glide:glide:4.13.2' 124 + ``` 125 + 126 + ## Key Implementation Details 127 + 128 + ### Lifecycle Management 129 + 130 + - **iOS**: Cancels pending requests in `willMove(toWindow:)` when scrolled off-screen 131 + - **Android**: Pauses playback in `onDetachedFromWindow()`, resumes in `onAttachedToWindow()` 132 + - **Web**: Uses React lifecycle methods to manage video element state 133 + 134 + ### Cache Strategy 135 + 136 + - **iOS**: Disk-only caching to work around `SDAnimatedImage` memory issues 137 + - **Android**: DATA disk cache for main images, skips memory cache for placeholders 138 + - **Web**: Relies on browser cache 139 + 140 + ### Animation Control 141 + 142 + - **iOS**: `SDAnimatedImageView.autoPlayAnimatedImage` is explicitly set to false to prevent automatic animation on viewport entry 143 + - **Android**: Custom `AppCompatImageViewExtended` manages `Animatable` drawable state 144 + - **Web**: Uses HTMLMediaElement play/pause APIs 145 + 146 + ## Files Overview 147 + 148 + ``` 149 + expo-bluesky-gif-view/ 150 + ├── index.ts # Module entry point 151 + ├── expo-module.config.json # Expo module configuration 152 + ├── src/ 153 + │ ├── GifView.types.ts # TypeScript type definitions 154 + │ ├── GifView.tsx # Native implementation (iOS/Android) 155 + │ └── GifView.web.tsx # Web implementation 156 + ├── ios/ 157 + │ ├── ExpoBlueskyGifView.podspec # CocoaPods spec 158 + │ ├── ExpoBlueskyGifViewModule.swift # Module and prop definitions 159 + │ ├── GifView.swift # iOS view implementation 160 + │ └── Util.swift # Cache configuration 161 + └── android/ 162 + ├── build.gradle # Gradle build configuration 163 + └── src/main/java/expo/modules/blueskygifview/ 164 + ├── ExpoBlueskyGifViewModule.kt # Module and prop definitions 165 + ├── GifView.kt # Android view implementation 166 + └── AppCompatImageViewExtended.kt # Custom ImageView for playback 167 + ```
+231
modules/expo-bluesky-swiss-army/README.md
··· 1 + # expo-bluesky-swiss-army 2 + 3 + A collection of native utilities for the Bluesky Social app. This Expo module provides platform-specific functionality that is not available through standard React Native APIs. 4 + 5 + ## Overview 6 + 7 + This module consolidates several native features into a single Expo module: 8 + 9 + - **PlatformInfo**: Platform-specific accessibility and audio session management 10 + - **Referrer**: Tracking how users arrive at the app (web referrers, app referrers, Google Play install referrer) 11 + - **SharedPrefs**: Shared preferences storage using native platform APIs (UserDefaults on iOS, SharedPreferences on Android) 12 + - **VisibilityView**: A native view component that tracks which view is currently visible on screen 13 + 14 + ## Modules 15 + 16 + ### PlatformInfo 17 + 18 + Provides platform-specific information and audio session control. 19 + 20 + **Functions:** 21 + 22 + - `getIsReducedMotionEnabled(): boolean` - Returns whether the user has enabled reduced motion in system settings. Works on all platforms (iOS uses UIAccessibility, Android checks transition animation scale, Web checks CSS media query). 23 + 24 + - `setAudioActive(active: boolean): void` - iOS only. Controls whether the app's audio session is active. When deactivated with `false`, it notifies other apps to resume their audio playback. 25 + 26 + - `setAudioCategory(category: AudioCategory): void` - iOS only. Sets the AVAudioSession category. Use `AudioCategory.Playback` for video/music playback and `AudioCategory.Ambient` for audio that mixes with other apps. 27 + 28 + **Platform Support:** 29 + - iOS: Full support for all functions 30 + - Android: `getIsReducedMotionEnabled()` only 31 + - Web: `getIsReducedMotionEnabled()` only 32 + 33 + ### Referrer 34 + 35 + Tracks how users arrive at the app from external sources. 36 + 37 + **Functions:** 38 + 39 + - `getReferrerInfo(): ReferrerInfo | null` - Returns information about the source that launched the app. Returns `{referrer: string, hostname: string}` or `null`. 40 + - **iOS**: Reads from SharedPrefs (set by app extensions or deep link handlers) 41 + - **Android**: Extracts referrer from Intent extras or activity referrer 42 + - **Web**: Parses `document.referrer` (excludes bsky.app domain) 43 + 44 + - `getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo>` - Android only. Retrieves Google Play install referrer information including install timestamp and click timestamp. Uses the Google Play Install Referrer API. 45 + 46 + **Platform Support:** 47 + - iOS: `getReferrerInfo()` only (reads from SharedPrefs) 48 + - Android: Both functions 49 + - Web: `getReferrerInfo()` only 50 + 51 + ### SharedPrefs 52 + 53 + Native key-value storage that persists across app restarts. Uses iOS App Groups (`group.app.bsky`) for sharing data with extensions, and Android SharedPreferences. 54 + 55 + **Functions:** 56 + 57 + - `setValue(key: string, value: string | number | boolean | null | undefined): void` - Store a value 58 + - `removeValue(key: string): void` - Remove a value 59 + - `getString(key: string): string | undefined` - Get a string value 60 + - `getNumber(key: string): number | undefined` - Get a number value 61 + - `getBool(key: string): boolean | undefined` - Get a boolean value 62 + - `addToSet(key: string, value: string): void` - Add a value to a set 63 + - `removeFromSet(key: string, value: string): void` - Remove a value from a set 64 + - `setContains(key: string, value: string): boolean` - Check if a set contains a value 65 + 66 + **Default Values (Android only):** 67 + The Android implementation initializes certain keys with default values on first access: 68 + - `playSoundChat`: true 69 + - `playSoundFollow`: false 70 + - `playSoundLike`: false 71 + - `playSoundMention`: false 72 + - `playSoundQuote`: false 73 + - `playSoundReply`: false 74 + - `playSoundRepost`: false 75 + - `badgeCount`: 0 76 + 77 + **Platform Support:** 78 + - iOS: Full support (uses UserDefaults with App Group) 79 + - Android: Full support (uses SharedPreferences) 80 + - Web: Not implemented 81 + 82 + **Implementation Notes:** 83 + - iOS uses App Group suite `group.app.bsky` to share preferences with app extensions 84 + - Android stores preferences in `xyz.blueskyweb.app` 85 + - Both platforms work around a bug where `JavaScriptValue.isString()` can cause crashes, so there's a separate `setString` function internally 86 + 87 + ### VisibilityView 88 + 89 + A React Native view component that detects which view is currently "active" based on visibility and position on screen. Only one view can be active at a time across the entire app. 90 + 91 + **Component:** 92 + 93 + ```tsx 94 + <VisibilityView 95 + enabled={boolean} 96 + onChangeStatus={(isActive: boolean) => void} 97 + > 98 + {children} 99 + </VisibilityView> 100 + ``` 101 + 102 + **Props:** 103 + - `enabled: boolean` - Whether this view participates in visibility tracking 104 + - `onChangeStatus: (isActive: boolean) => void` - Callback fired when the view becomes active or inactive 105 + - `children: React.ReactNode` - Child components 106 + 107 + **Functions:** 108 + 109 + - `updateActiveViewAsync(): Promise<void>` - Manually trigger recalculation of the active view 110 + 111 + **How It Works:** 112 + 113 + The module maintains a global registry of all VisibilityView instances. When views are added/removed or when explicitly updated, it calculates which view is "most visible": 114 + 115 + 1. A view must be at least 50% visible on screen 116 + 2. If multiple views meet this threshold, the one closest to the top of the screen wins (specifically, the one with the lowest Y position, but must be at least 150px from the top) 117 + 3. Only one view can be active at a time - when a new view becomes active, the previous one is deactivated 118 + 119 + This is useful for features like video autoplay, where you want to know which video is currently the "primary" one the user is viewing. 120 + 121 + **Platform Support:** 122 + - iOS: Full support using UIView position tracking 123 + - Android: Full support using View position tracking 124 + - Web: Passthrough component (renders children without tracking) 125 + 126 + ## Architecture 127 + 128 + ### TypeScript Layer 129 + 130 + The module uses platform-specific file extensions to provide appropriate implementations: 131 + 132 + - `index.ts` - Throws NotImplementedError (base/fallback) 133 + - `index.native.ts` - Calls native modules via Expo Modules Core 134 + - `index.web.ts` - Web-specific implementations or stubs 135 + - `index.ios.ts` / `index.android.ts` - Platform-specific implementations when behavior differs 136 + 137 + ### Native Layer 138 + 139 + **iOS:** 140 + - Swift implementation using Expo Modules Core 141 + - Files organized by feature in subdirectories (PlatformInfo/, Referrer/, SharedPrefs/, Visibility/) 142 + - Uses standard iOS APIs: UIAccessibility, AVAudioSession, UserDefaults, UIView 143 + 144 + **Android:** 145 + - Kotlin implementation using Expo Modules Core 146 + - Package structure: `expo.modules.blueskyswissarmy.[feature]` 147 + - Uses standard Android APIs: Settings.Global, InstallReferrerClient, SharedPreferences, View 148 + 149 + ## Key Files 150 + 151 + ### TypeScript 152 + - `index.ts` - Main module exports 153 + - `src/NotImplemented.ts` - Error thrown when functionality is not available on current platform 154 + - `src/[Feature]/types.ts` - TypeScript type definitions for each feature 155 + - `src/[Feature]/index.*.ts` - Platform-specific implementations 156 + 157 + ### iOS 158 + - `ios/ExpoBlueskySwissArmy.podspec` - CocoaPods specification 159 + - `ios/[Feature]/Expo*Module.swift` - Expo module definitions 160 + - `ios/SharedPrefs/SharedPrefs.swift` - Shared preference manager (usable from other native code) 161 + - `ios/Visibility/VisibilityViewManager.swift` - Global view tracking manager 162 + 163 + ### Android 164 + - `android/build.gradle` - Gradle build configuration (includes installreferrer dependency) 165 + - `android/src/main/java/expo/modules/blueskyswissarmy/[feature]/Expo*Module.kt` - Expo module definitions 166 + - `android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt` - Shared preference manager 167 + - `android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityViewManager.kt` - Global view tracking manager 168 + 169 + ## Configuration 170 + 171 + ### Expo Module Config 172 + 173 + The module is registered in `expo-module.config.json` with all four sub-modules for both iOS and Android. 174 + 175 + ### iOS 176 + 177 + Requires iOS 13.4 or later. Uses the App Group `group.app.bsky` for SharedPrefs - ensure this is configured in your app's entitlements. 178 + 179 + ### Android 180 + 181 + - Minimum SDK: 21 182 + - Target SDK: 34 183 + - Requires `com.android.installreferrer:installreferrer:2.2` dependency for Google Play referrer tracking 184 + 185 + ## Usage Example 186 + 187 + ```typescript 188 + import { 189 + PlatformInfo, 190 + AudioCategory, 191 + Referrer, 192 + SharedPrefs, 193 + VisibilityView 194 + } from 'expo-bluesky-swiss-army' 195 + 196 + // Check for reduced motion 197 + const isReducedMotion = PlatformInfo.getIsReducedMotionEnabled() 198 + 199 + // Set audio category for video playback (iOS) 200 + PlatformInfo.setAudioCategory(AudioCategory.Playback) 201 + PlatformInfo.setAudioActive(true) 202 + 203 + // Check how user arrived at the app 204 + const referrer = Referrer.getReferrerInfo() 205 + if (referrer) { 206 + console.log('User came from:', referrer.hostname) 207 + } 208 + 209 + // Store a preference 210 + SharedPrefs.setValue('lastOpenedAt', Date.now()) 211 + SharedPrefs.setValue('hasSeenOnboarding', true) 212 + 213 + // Track visible view 214 + <VisibilityView 215 + enabled={true} 216 + onChangeStatus={(isActive) => { 217 + if (isActive) { 218 + // This view is now the primary visible view 219 + video.play() 220 + } else { 221 + video.pause() 222 + } 223 + }} 224 + > 225 + <VideoPlayer /> 226 + </VisibilityView> 227 + ``` 228 + 229 + ## Version 230 + 231 + Current version: 0.6.0
+113 -1
modules/expo-emoji-picker/README.md
··· 1 1 # expo-emoji-picker 2 2 3 - Based on [react-native-emoji-popup](https://github.com/okwasniewski/react-native-emoji-popup) and [expo-emoji-picker](https://github.com/alanjhughes/expo-emoji-picker) 3 + A native emoji picker module for React Native applications built with Expo. This module provides platform-specific emoji selection interfaces using native system components. 4 + 5 + Based on [react-native-emoji-popup](https://github.com/okwasniewski/react-native-emoji-popup) and [expo-emoji-picker](https://github.com/alanjhughes/expo-emoji-picker). 6 + 7 + ## What It Does 8 + 9 + The module exposes a React component that presents native emoji picker UI on iOS and Android. When a user selects an emoji, it fires a callback with the selected emoji string. 10 + 11 + ## Platform Support 12 + 13 + - **iOS**: Uses [MCEmojiPicker](https://github.com/izyumkin/MCEmojiPicker) presented as a modal picker 14 + - **Android**: Uses the system `androidx.emoji2.emojipicker.EmojiPickerView` component 15 + - **Web**: Not supported (native platforms only) 16 + 17 + ## How It Works 18 + 19 + ### Architecture 20 + 21 + The module follows Expo's module architecture with three layers: 22 + 23 + 1. **JavaScript/TypeScript Layer** (`src/`): React components and type definitions 24 + 2. **Native iOS Layer** (`ios/`): Swift implementation using MCEmojiPicker 25 + 3. **Native Android Layer** (`android/`): Kotlin implementation using AndroidX emoji picker 26 + 27 + ### iOS Implementation 28 + 29 + On iOS, the module creates an invisible tap target view. When tapped, it presents MCEmojiPicker as a modal view controller: 30 + 31 + - `EmojiPickerView.swift`: Custom view that handles tap gestures and presents the picker 32 + - `EmojiPickerModule.swift`: Module definition that registers the view with Expo 33 + - Uses MCEmojiPicker dependency for the native picker UI 34 + 35 + The picker is presented from the current React view controller and returns the selected emoji via an event dispatcher. 36 + 37 + ### Android Implementation 38 + 39 + On Android, the module embeds the AndroidX EmojiPickerView directly as a full-screen component: 40 + 41 + - `EmojiPickerModuleView.kt`: Wraps the system EmojiPickerView in an ExpoView 42 + - `EmojiPickerModule.kt`: Module definition that registers the view with Expo 43 + - Handles configuration changes (dark mode, orientation) by recreating the view 44 + 45 + The AndroidX emoji picker provides a grid-based interface with category tabs and search. 46 + 47 + ### Platform-Specific React Components 48 + 49 + The module uses platform-specific file extensions for different behaviors: 50 + 51 + - `EmojiPicker.tsx` (iOS): Renders an invisible tap target that accepts children 52 + - `EmojiPicker.android.tsx` (Android): Renders the full emoji picker view with flex: 1 layout 53 + 54 + Both components normalize the native event structure to provide a consistent `onEmojiSelected` callback. 55 + 56 + ## Key Files 57 + 58 + ### Configuration 59 + - `expo-module.config.json`: Defines the module name and native class mappings for iOS and Android 60 + 61 + ### TypeScript/React 62 + - `index.ts`: Public exports for the module 63 + - `src/EmojiPickerModule.ts`: Native module registration 64 + - `src/EmojiPickerModule.types.ts`: TypeScript type definitions 65 + - `src/EmojiPickerView.tsx`: Base native view component 66 + - `src/EmojiPicker.tsx`: iOS-specific implementation 67 + - `src/EmojiPicker.android.tsx`: Android-specific implementation 68 + 69 + ### iOS (Swift) 70 + - `ios/EmojiPickerModule.swift`: Module definition (11 lines) 71 + - `ios/EmojiPickerView.swift`: View implementation with tap handling and picker presentation 72 + - `ios/EmojiPickerModule.podspec`: CocoaPods specification with MCEmojiPicker dependency 73 + 74 + ### Android (Kotlin) 75 + - `android/src/main/java/expo/community/modules/emojipicker/EmojiPickerModule.kt`: Module definition 76 + - `android/src/main/java/expo/community/modules/emojipicker/EmojiPickerModuleView.kt`: View implementation 77 + - `android/build.gradle`: Gradle configuration with androidx.emoji2:emoji2-emojipicker dependency 78 + 79 + ## Usage 80 + 81 + ```tsx 82 + import { EmojiPicker } from 'expo-emoji-picker' 83 + 84 + function MyComponent() { 85 + const handleEmojiSelected = (emoji: string) => { 86 + console.log('Selected emoji:', emoji) 87 + } 88 + 89 + return ( 90 + <EmojiPicker onEmojiSelected={handleEmojiSelected}> 91 + {/* On iOS, children render as the tap target */} 92 + {/* On Android, children are ignored - picker is shown directly */} 93 + </EmojiPicker> 94 + ) 95 + } 96 + ``` 97 + 98 + ## Dependencies 99 + 100 + ### iOS 101 + - ExpoModulesCore 102 + - MCEmojiPicker (external CocoaPods dependency) 103 + - Minimum iOS version: 15.1 104 + 105 + ### Android 106 + - expo-modules-core 107 + - androidx.emoji2:emoji2-emojipicker:1.5.0 108 + - Minimum SDK: 21 109 + - Target SDK: 34 110 + 111 + ## Configuration 112 + 113 + No additional configuration is required. The module is automatically linked through Expo's autolinking system when the app is built. 114 + 115 + The module definition in `expo-module.config.json` specifies the native class names for each platform, which Expo uses to register the module at runtime.
+118 -5
modules/expo-receive-android-intents/README.md
··· 1 1 # Expo Receive Android Intents 2 2 3 - This module handles incoming intents on Android. Handled intents are `text/plain` and `image/*` (single or multiple). 4 - The module handles saving images to the app's filesystem for access within the app, limiting the selection of images 5 - to a max of four, and handling intent types. No JS code is required for this module, and it is no-op on non-android 6 - platforms. 3 + An Expo module that handles incoming Android intents for sharing text, images, and videos into the Bluesky app. 4 + 5 + ## What It Does 6 + 7 + This module intercepts Android share intents (when a user shares content from another app to Bluesky) and converts them into deep links that the app can handle. It supports: 8 + 9 + - **Text sharing** - Share plain text to compose a post 10 + - **Image sharing** - Share single or multiple images (up to 4) to attach to a post 11 + - **Video sharing** - Share a single video to attach to a post 12 + 13 + The module operates entirely in native Android code and requires no JavaScript API calls. It automatically registers itself with Expo's module system and handles intents when the app is launched or receives new intents. 14 + 15 + ## Platform Support 16 + 17 + - **Android**: Fully supported 18 + - **iOS**: No-op (iOS handles share intents differently) 19 + - **Web**: No-op 20 + 21 + ## How It Works 22 + 23 + ### Architecture 24 + 25 + The module uses Expo's module lifecycle hooks to intercept Android intents at two key moments: 26 + 27 + 1. **OnCreate** - When the app is first launched from an intent 28 + 2. **OnNewIntent** - When the app receives a new intent while already running 29 + 30 + ### Intent Processing Flow 31 + 32 + 1. **Intent Reception**: Android sends an `ACTION_SEND` or `ACTION_SEND_MULTIPLE` intent 33 + 2. **Type Detection**: Module determines content type (text, image, or video) 34 + 3. **Content Processing**: 35 + - **Text**: URL-encodes the text 36 + - **Images**: Saves to app cache, extracts dimensions (limited to 4 images max) 37 + - **Video**: Copies to app cache with extension detection, extracts dimensions 38 + 4. **Deep Link Generation**: Creates a `bluesky://intent/compose` URL with encoded parameters 39 + 5. **App Launch**: Starts a new activity with the deep link, which is handled by `useIntentHandler` 40 + 41 + ### Deep Link Format 42 + 43 + The module generates deep links in the following formats: 44 + 45 + ``` 46 + # Text only 47 + bluesky://intent/compose?text=<encoded-text> 48 + 49 + # Images (single or multiple) 50 + bluesky://intent/compose?imageUris=<uri1>|<width>|<height>,<uri2>|<width>|<height>&text=<encoded-text> 51 + 52 + # Video (single only) 53 + bluesky://intent/compose?videoUri=<uri>|<width>|<height>&text=<encoded-text> 54 + ``` 55 + 56 + All URIs use the `file://` scheme pointing to files in the app's cache directory. Dimensions are included to avoid expensive measurement operations in JavaScript. 57 + 58 + ### Security Considerations 59 + 60 + - Images and videos are copied to the app's private cache directory before being passed to the app 61 + - The JavaScript handler (`useIntentHandler.ts`) validates image URIs with a regex to prevent external URLs 62 + - Image URIs containing `http://` or `https://` are filtered out 63 + - Multiple image sharing is limited to 4 images maximum 64 + 65 + ## Key Files 66 + 67 + ### Module Configuration 68 + 69 + - **expo-module.config.json** - Declares the module and registers it with Expo (Android-only) 70 + 71 + ### Native Implementation 72 + 73 + - **ExpoReceiveAndroidIntentsModule.kt** - Main module class with intent handling logic 74 + - `handleIntent()` - Routes intents based on type 75 + - `handleTextIntent()` - Processes text sharing 76 + - `handleAttachmentIntent()` - Processes single image/video 77 + - `handleAttachmentsIntent()` - Processes multiple images 78 + - `getImageInfo()` - Saves images to cache and extracts dimensions 79 + - `getVideoInfo()` - Extracts video dimensions using MediaMetadataRetriever 80 + 81 + - **android/build.gradle** - Gradle build configuration 82 + - Version: 0.4.1 83 + - Requires: Kotlin, expo-modules-core 84 + - Compile SDK: 33, Min SDK: 21, Target SDK: 34 85 + 86 + - **android/src/main/AndroidManifest.xml** - Empty manifest (intent filters configured in main app) 87 + 88 + ### JavaScript Integration 89 + 90 + The deep links generated by this module are handled by: 91 + 92 + - **src/lib/hooks/useIntentHandler.ts** - `useComposeIntent()` parses the deep link parameters and opens the composer with pre-populated content 93 + 94 + ## Installation 95 + 96 + No manual installation is required. Gradle automatically includes this module during the Android build process. The module is auto-linked through Expo's module system. 97 + 98 + ## Configuration 99 + 100 + Intent filters must be configured in the main app's `AndroidManifest.xml` to declare which MIME types the app accepts. The module itself has an empty manifest. 101 + 102 + ## Implementation Notes 103 + 104 + ### Android Version Compatibility 7 105 8 - No installation is required. Gradle will automatically add this module on build. 106 + The module uses version-specific APIs for Android 13+ (API 33): 107 + - `getParcelableExtra()` with type parameter on Android 13+ 108 + - Legacy `getParcelableExtra()` on older versions 109 + 110 + ### File Handling 111 + 112 + - Temporary files are created using `File.createTempFile()` in the app's cache directory 113 + - Image files use `.jpeg` extension and are compressed at 100% quality 114 + - Video files preserve their original extension, defaulting to `.mp4` if none is detected 115 + 116 + ### Limitations 117 + 118 + - Video sharing only supports a single video 119 + - Multiple video sharing is not implemented 120 + - Images are always converted to JPEG format 121 + - Maximum of 4 images can be shared at once
+116
modules/expo-scroll-forwarder/README.md
··· 1 + # expo-scroll-forwarder 2 + 3 + An Expo native module that forwards scroll gestures from a UIView to a UIScrollView on iOS. This enables custom scroll behaviors by allowing a non-scrollable view to control a scrollable view's scroll position. 4 + 5 + ## What It Does 6 + 7 + This module solves a specific interaction problem: allowing a fixed header or overlay view to respond to scroll gestures and forward them to an underlying scroll view. The primary use case in the Bluesky app is the profile screen, where the profile header sits above a scrollable content area and can be dragged to scroll the content below it. 8 + 9 + Key behaviors: 10 + - Captures pan gestures on a wrapper view and translates them to scroll offsets on a target scroll view 11 + - Implements physics-based deceleration animations that match native scroll behavior 12 + - Supports pull-to-refresh interactions with haptic feedback 13 + - Prevents gesture conflicts with iOS swipe-back navigation by only activating on vertical pans 14 + - Provides rubber-band damping when scrolling past content bounds 15 + 16 + ## Architecture 17 + 18 + The module consists of three main parts: 19 + 20 + ### 1. Native iOS Implementation (Swift) 21 + 22 + **ExpoScrollForwarderView.swift** - The core native view component that: 23 + - Attaches a UIPanGestureRecognizer to intercept scroll gestures 24 + - Finds and references the target RCTScrollView using its React Native tag 25 + - Implements custom scroll physics including velocity-based decay animation 26 + - Manages gesture recognizer delegation to prevent conflicts with system gestures 27 + - Handles pull-to-refresh activation at -130pt scroll offset with haptic feedback 28 + 29 + **ExpoScrollForwarderModule.swift** - The Expo module definition that: 30 + - Registers the view component with Expo 31 + - Exposes the `scrollViewTag` prop to specify which scroll view to control 32 + 33 + ### 2. TypeScript Interface 34 + 35 + **ExpoScrollForwarderView.tsx** - Platform-specific implementations: 36 + - **iOS (.ios.tsx)**: Wraps the native view manager from expo-modules-core 37 + - **Default (.tsx)**: No-op wrapper that just renders children (for Android/Web compatibility) 38 + 39 + **ExpoScrollForwarder.types.ts** - TypeScript type definitions: 40 + - `scrollViewTag`: The React Native tag of the scroll view to control 41 + - `children`: The content to render (typically a header component) 42 + 43 + ### 3. Module Configuration 44 + 45 + **expo-module.config.json** - Declares iOS-only platform support 46 + 47 + **ExpoScrollForwarder.podspec** - CocoaPods specification for iOS dependency management 48 + 49 + ## Usage 50 + 51 + ```tsx 52 + import {ExpoScrollForwarderView} from 'expo-scroll-forwarder' 53 + 54 + function ProfileScreen() { 55 + const scrollViewTag = useRef(null) 56 + 57 + return ( 58 + <View> 59 + <ExpoScrollForwarderView scrollViewTag={scrollViewTag.current}> 60 + <ProfileHeader /> 61 + </ExpoScrollForwarderView> 62 + 63 + <ScrollView ref={scrollViewTag}> 64 + {/* Scrollable content */} 65 + </ScrollView> 66 + </View> 67 + ) 68 + } 69 + ``` 70 + 71 + The `scrollViewTag` prop must be the React Native tag (numeric identifier) of the target scroll view. The module uses this to locate the native UIScrollView instance. 72 + 73 + ## Platform Support 74 + 75 + - **iOS**: Full native implementation with custom scroll physics 76 + - **Android**: No-op wrapper (renders children without scroll forwarding) 77 + - **Web**: No-op wrapper (renders children without scroll forwarding) 78 + 79 + The module is designed to enhance iOS UX while gracefully degrading on other platforms. 80 + 81 + ## Key Implementation Details 82 + 83 + ### Gesture Recognition 84 + - Only activates when pan velocity is more vertical than horizontal (`abs(velocity.y) > abs(velocity.x)`) 85 + - Delegates to UIGestureRecognizerDelegate to prevent simultaneous recognition with navigation swipe-back 86 + - Adds tap/long-press recognizers to the scroll view to cancel ongoing animations 87 + 88 + ### Scroll Physics 89 + - Implements custom decay animation at 120fps using a Timer 90 + - Velocity decay factor: 0.9875 per frame 91 + - Velocity clamped to +/- 5000 points/second 92 + - Rubber-band damping: offsets below 0 are reduced by 55% 93 + - Animation stops when velocity drops below 5 points/second 94 + 95 + ### Pull-to-Refresh 96 + - Triggers at -130pt scroll offset 97 + - Provides haptic feedback (UIImpactFeedbackGenerator, light style) 98 + - Calls refresh control via `RCTRefreshControl.forwarderBeginRefreshing()` 99 + 100 + ### Scroll View Management 101 + - Dynamically finds scroll view using `AppContext.findView(withTag:ofType:)` 102 + - Properly cleans up gesture recognizers when switching between scroll views 103 + - Maintains references to both the scroll view and its refresh control 104 + 105 + ## Files Overview 106 + 107 + | File | Purpose | 108 + |------|---------| 109 + | `ios/ExpoScrollForwarderView.swift` | Native iOS view implementation with gesture handling and scroll physics | 110 + | `ios/ExpoScrollForwarderModule.swift` | Expo module registration and prop definitions | 111 + | `ios/ExpoScrollForwarder.podspec` | CocoaPods dependency specification | 112 + | `src/ExpoScrollForwarderView.ios.tsx` | TypeScript wrapper for iOS native view | 113 + | `src/ExpoScrollForwarderView.tsx` | Default no-op implementation for other platforms | 114 + | `src/ExpoScrollForwarder.types.ts` | TypeScript type definitions | 115 + | `index.ts` | Module entry point | 116 + | `expo-module.config.json` | Expo module configuration |