Add READMEs to modules (#10306)
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
# expo-bluesky-swiss-army
|
||||
|
||||
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.
|
||||
|
||||
## Overview
|
||||
|
||||
This module consolidates several native features into a single Expo module:
|
||||
|
||||
- **PlatformInfo**: Platform-specific accessibility and audio session management
|
||||
- **Referrer**: Tracking how users arrive at the app (web referrers, app referrers, Google Play install referrer)
|
||||
- **SharedPrefs**: Shared preferences storage using native platform APIs (UserDefaults on iOS, SharedPreferences on Android)
|
||||
- **VisibilityView**: A native view component that tracks which view is currently visible on screen
|
||||
|
||||
## Modules
|
||||
|
||||
### PlatformInfo
|
||||
|
||||
Provides platform-specific information and audio session control.
|
||||
|
||||
**Functions:**
|
||||
|
||||
- `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).
|
||||
|
||||
- `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.
|
||||
|
||||
- `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.
|
||||
|
||||
**Platform Support:**
|
||||
- iOS: Full support for all functions
|
||||
- Android: `getIsReducedMotionEnabled()` only
|
||||
- Web: `getIsReducedMotionEnabled()` only
|
||||
|
||||
### Referrer
|
||||
|
||||
Tracks how users arrive at the app from external sources.
|
||||
|
||||
**Functions:**
|
||||
|
||||
- `getReferrerInfo(): ReferrerInfo | null` - Returns information about the source that launched the app. Returns `{referrer: string, hostname: string}` or `null`.
|
||||
- **iOS**: Reads from SharedPrefs (set by app extensions or deep link handlers)
|
||||
- **Android**: Extracts referrer from Intent extras or activity referrer
|
||||
- **Web**: Parses `document.referrer` (excludes bsky.app domain)
|
||||
|
||||
- `getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo>` - Android only. Retrieves Google Play install referrer information including install timestamp and click timestamp. Uses the Google Play Install Referrer API.
|
||||
|
||||
**Platform Support:**
|
||||
- iOS: `getReferrerInfo()` only (reads from SharedPrefs)
|
||||
- Android: Both functions
|
||||
- Web: `getReferrerInfo()` only
|
||||
|
||||
### SharedPrefs
|
||||
|
||||
Native key-value storage that persists across app restarts. Uses iOS App Groups (`group.app.bsky`) for sharing data with extensions, and Android SharedPreferences.
|
||||
|
||||
**Functions:**
|
||||
|
||||
- `setValue(key: string, value: string | number | boolean | null | undefined): void` - Store a value
|
||||
- `removeValue(key: string): void` - Remove a value
|
||||
- `getString(key: string): string | undefined` - Get a string value
|
||||
- `getNumber(key: string): number | undefined` - Get a number value
|
||||
- `getBool(key: string): boolean | undefined` - Get a boolean value
|
||||
- `addToSet(key: string, value: string): void` - Add a value to a set
|
||||
- `removeFromSet(key: string, value: string): void` - Remove a value from a set
|
||||
- `setContains(key: string, value: string): boolean` - Check if a set contains a value
|
||||
|
||||
**Default Values (Android only):**
|
||||
The Android implementation initializes certain keys with default values on first access:
|
||||
- `playSoundChat`: true
|
||||
- `playSoundFollow`: false
|
||||
- `playSoundLike`: false
|
||||
- `playSoundMention`: false
|
||||
- `playSoundQuote`: false
|
||||
- `playSoundReply`: false
|
||||
- `playSoundRepost`: false
|
||||
- `badgeCount`: 0
|
||||
|
||||
**Platform Support:**
|
||||
- iOS: Full support (uses UserDefaults with App Group)
|
||||
- Android: Full support (uses SharedPreferences)
|
||||
- Web: Not implemented
|
||||
|
||||
**Implementation Notes:**
|
||||
- iOS uses App Group suite `group.app.bsky` to share preferences with app extensions
|
||||
- Android stores preferences in `xyz.blueskyweb.app`
|
||||
- Both platforms work around a bug where `JavaScriptValue.isString()` can cause crashes, so there's a separate `setString` function internally
|
||||
|
||||
### VisibilityView
|
||||
|
||||
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.
|
||||
|
||||
**Component:**
|
||||
|
||||
```tsx
|
||||
<VisibilityView
|
||||
enabled={boolean}
|
||||
onChangeStatus={(isActive: boolean) => void}
|
||||
>
|
||||
{children}
|
||||
</VisibilityView>
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `enabled: boolean` - Whether this view participates in visibility tracking
|
||||
- `onChangeStatus: (isActive: boolean) => void` - Callback fired when the view becomes active or inactive
|
||||
- `children: React.ReactNode` - Child components
|
||||
|
||||
**Functions:**
|
||||
|
||||
- `updateActiveViewAsync(): Promise<void>` - Manually trigger recalculation of the active view
|
||||
|
||||
**How It Works:**
|
||||
|
||||
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":
|
||||
|
||||
1. A view must be at least 50% visible on screen
|
||||
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)
|
||||
3. Only one view can be active at a time - when a new view becomes active, the previous one is deactivated
|
||||
|
||||
This is useful for features like video autoplay, where you want to know which video is currently the "primary" one the user is viewing.
|
||||
|
||||
**Platform Support:**
|
||||
- iOS: Full support using UIView position tracking
|
||||
- Android: Full support using View position tracking
|
||||
- Web: Passthrough component (renders children without tracking)
|
||||
|
||||
## Architecture
|
||||
|
||||
### TypeScript Layer
|
||||
|
||||
The module uses platform-specific file extensions to provide appropriate implementations:
|
||||
|
||||
- `index.ts` - Throws NotImplementedError (base/fallback)
|
||||
- `index.native.ts` - Calls native modules via Expo Modules Core
|
||||
- `index.web.ts` - Web-specific implementations or stubs
|
||||
- `index.ios.ts` / `index.android.ts` - Platform-specific implementations when behavior differs
|
||||
|
||||
### Native Layer
|
||||
|
||||
**iOS:**
|
||||
- Swift implementation using Expo Modules Core
|
||||
- Files organized by feature in subdirectories (PlatformInfo/, Referrer/, SharedPrefs/, Visibility/)
|
||||
- Uses standard iOS APIs: UIAccessibility, AVAudioSession, UserDefaults, UIView
|
||||
|
||||
**Android:**
|
||||
- Kotlin implementation using Expo Modules Core
|
||||
- Package structure: `expo.modules.blueskyswissarmy.[feature]`
|
||||
- Uses standard Android APIs: Settings.Global, InstallReferrerClient, SharedPreferences, View
|
||||
|
||||
## Key Files
|
||||
|
||||
### TypeScript
|
||||
- `index.ts` - Main module exports
|
||||
- `src/NotImplemented.ts` - Error thrown when functionality is not available on current platform
|
||||
- `src/[Feature]/types.ts` - TypeScript type definitions for each feature
|
||||
- `src/[Feature]/index.*.ts` - Platform-specific implementations
|
||||
|
||||
### iOS
|
||||
- `ios/ExpoBlueskySwissArmy.podspec` - CocoaPods specification
|
||||
- `ios/[Feature]/Expo*Module.swift` - Expo module definitions
|
||||
- `ios/SharedPrefs/SharedPrefs.swift` - Shared preference manager (usable from other native code)
|
||||
- `ios/Visibility/VisibilityViewManager.swift` - Global view tracking manager
|
||||
|
||||
### Android
|
||||
- `android/build.gradle` - Gradle build configuration (includes installreferrer dependency)
|
||||
- `android/src/main/java/expo/modules/blueskyswissarmy/[feature]/Expo*Module.kt` - Expo module definitions
|
||||
- `android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt` - Shared preference manager
|
||||
- `android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityViewManager.kt` - Global view tracking manager
|
||||
|
||||
## Configuration
|
||||
|
||||
### Expo Module Config
|
||||
|
||||
The module is registered in `expo-module.config.json` with all four sub-modules for both iOS and Android.
|
||||
|
||||
### iOS
|
||||
|
||||
Requires iOS 13.4 or later. Uses the App Group `group.app.bsky` for SharedPrefs - ensure this is configured in your app's entitlements.
|
||||
|
||||
### Android
|
||||
|
||||
- Minimum SDK: 21
|
||||
- Target SDK: 34
|
||||
- Requires `com.android.installreferrer:installreferrer:2.2` dependency for Google Play referrer tracking
|
||||
|
||||
## Usage Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
PlatformInfo,
|
||||
AudioCategory,
|
||||
Referrer,
|
||||
SharedPrefs,
|
||||
VisibilityView
|
||||
} from 'expo-bluesky-swiss-army'
|
||||
|
||||
// Check for reduced motion
|
||||
const isReducedMotion = PlatformInfo.getIsReducedMotionEnabled()
|
||||
|
||||
// Set audio category for video playback (iOS)
|
||||
PlatformInfo.setAudioCategory(AudioCategory.Playback)
|
||||
PlatformInfo.setAudioActive(true)
|
||||
|
||||
// Check how user arrived at the app
|
||||
const referrer = Referrer.getReferrerInfo()
|
||||
if (referrer) {
|
||||
console.log('User came from:', referrer.hostname)
|
||||
}
|
||||
|
||||
// Store a preference
|
||||
SharedPrefs.setValue('lastOpenedAt', Date.now())
|
||||
SharedPrefs.setValue('hasSeenOnboarding', true)
|
||||
|
||||
// Track visible view
|
||||
<VisibilityView
|
||||
enabled={true}
|
||||
onChangeStatus={(isActive) => {
|
||||
if (isActive) {
|
||||
// This view is now the primary visible view
|
||||
video.play()
|
||||
} else {
|
||||
video.pause()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<VideoPlayer />
|
||||
</VisibilityView>
|
||||
```
|
||||
|
||||
## Version
|
||||
|
||||
Current version: 0.6.0
|
||||
Reference in New Issue
Block a user