├── .nvmrc ├── .prettierrc ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values-night │ │ │ │ │ └── colors.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ └── splashscreen_image.png │ │ │ │ ├── drawable │ │ │ │ │ ├── splashscreen.xml │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ └── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── jni │ │ │ │ ├── MainApplicationModuleProvider.h │ │ │ │ ├── OnLoad.cpp │ │ │ │ ├── MainApplicationModuleProvider.cpp │ │ │ │ ├── MainComponentsRegistry.h │ │ │ │ ├── MainApplicationTurboModuleManagerDelegate.h │ │ │ │ ├── MainApplicationTurboModuleManagerDelegate.cpp │ │ │ │ ├── Android.mk │ │ │ │ └── MainComponentsRegistry.cpp │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── resonatecoop │ │ │ │ │ └── mobile │ │ │ │ │ ├── newarchitecture │ │ │ │ │ ├── components │ │ │ │ │ │ └── MainComponentsRegistry.java │ │ │ │ │ ├── modules │ │ │ │ │ │ └── MainApplicationTurboModuleManagerDelegate.java │ │ │ │ │ └── MainApplicationReactNativeHost.java │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ └── AndroidManifest.xml │ │ └── debug │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── com │ │ │ └── resonatecoop │ │ │ └── mobile │ │ │ └── ReactNativeFlipper.java │ ├── debug.keystore │ ├── proguard-rules.pro │ ├── build_defs.bzl │ ├── BUCK │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── gradle.properties ├── build.gradle ├── gradlew.bat └── gradlew ├── ios ├── Podfile.properties.json ├── mobile │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── SplashScreen.imageset │ │ │ ├── image.png │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── ItunesArtwork@2x.png │ │ │ ├── App-Icon-20x20@1x.png │ │ │ ├── App-Icon-20x20@2x.png │ │ │ ├── App-Icon-20x20@3x.png │ │ │ ├── App-Icon-29x29@1x.png │ │ │ ├── App-Icon-29x29@2x.png │ │ │ ├── App-Icon-29x29@3x.png │ │ │ ├── App-Icon-40x40@1x.png │ │ │ ├── App-Icon-40x40@2x.png │ │ │ ├── App-Icon-40x40@3x.png │ │ │ ├── App-Icon-60x60@2x.png │ │ │ ├── App-Icon-60x60@3x.png │ │ │ ├── App-Icon-76x76@1x.png │ │ │ ├── App-Icon-76x76@2x.png │ │ │ ├── App-Icon-83.5x83.5@2x.png │ │ │ └── Contents.json │ │ └── SplashScreenBackground.imageset │ │ │ ├── image.png │ │ │ └── Contents.json │ ├── noop-file.swift │ ├── AppDelegate.h │ ├── main.m │ ├── mobile.entitlements │ ├── Supporting │ │ └── Expo.plist │ ├── Info.plist │ ├── SplashScreen.storyboard │ └── AppDelegate.mm ├── mobile.xcworkspace │ └── contents.xcworkspacedata ├── .gitignore ├── .xcode.env ├── Podfile └── mobile.xcodeproj │ └── xcshareddata │ └── xcschemes │ └── mobile.xcscheme ├── .eslintrc.js ├── jest.setupFilesAfterEnv.ts ├── assets ├── icon.png ├── favicon.png ├── splash.png └── adaptive-icon.png ├── .husky ├── pre-commit └── commit-msg ├── commitlint.config.js ├── client ├── components │ ├── Player │ │ ├── types.ts │ │ └── index.tsx │ ├── Home │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── CarouselListCard.tsx │ │ ├── CarouselList.tsx │ │ └── index.tsx │ ├── Browse │ │ └── index.tsx │ ├── Library │ │ └── index.tsx │ ├── CustomStatusBar │ │ └── index.tsx │ ├── common │ │ └── ScreenView.tsx │ └── Search │ │ ├── search-results.tsx │ │ └── index.tsx ├── constants │ └── index.ts ├── utils │ └── index.ts ├── hooks │ ├── useDebouncedState.ts │ └── useKeyboardVisible.ts ├── api │ ├── error.ts │ ├── fetch-with-timeout.ts │ ├── search.ts │ ├── fetch-client.ts │ ├── trackgroups.ts │ └── artists.ts ├── test │ └── index.ts ├── navigation │ └── RootTabNavigator.tsx └── theme │ └── index.tsx ├── lint-staged.config.js ├── tsconfig.json ├── CONTRIBUTING.md ├── .expo-shared └── assets.json ├── metro.config.js ├── babel.config.js ├── .gitignore ├── index.js ├── .github ├── workflows │ ├── commitlint.yml │ ├── custom-lint.yml │ └── github-actions.yaml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── FUNDING.yml ├── eas.json ├── jest.config.ts ├── SECURITY.md ├── App.tsx ├── app.config.ts ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false 3 | } 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "jsc" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "universe/native", 3 | }; 4 | -------------------------------------------------------------------------------- /jest.setupFilesAfterEnv.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-native/extend-expect"; 2 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/assets/icon.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/assets/splash.png -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /client/components/Player/types.ts: -------------------------------------------------------------------------------- 1 | export enum LoopingType { 2 | ALL = 0, 3 | ONE = 1, 4 | } 5 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "**/*.ts?(x)": () => "tsc -p tsconfig.json --noEmit", 3 | }; 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [View Resonate's Community Guidelines](https://github.com/resonatecoop/stream/blob/master/CONTRIBUTING.md) 2 | -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "expo" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /client/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const BOTTOM_NAVIGATION_HEIGHT = 80; 2 | export const FONT_SIZE = 14; 3 | export const PLAYER_HEIGHT = 140; 4 | -------------------------------------------------------------------------------- /ios/mobile/noop-file.swift: -------------------------------------------------------------------------------- 1 | // 2 | // @generated 3 | // A blank Swift file must be created for native modules with Swift files to work correctly. 4 | // 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/SplashScreen.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/SplashScreen.imageset/image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/drawable-hdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/drawable-mdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@1x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@2x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-20x20@3x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@1x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@2x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-29x29@3x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@1x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@2x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-40x40@3x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@2x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-60x60@3x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@1x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-76x76@2x.png -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/SplashScreenBackground.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/SplashScreenBackground.imageset/image.png -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonatecoop/mobile/HEAD/ios/mobile/Images.xcassets/AppIcon.appiconset/App-Icon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splashscreen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require("expo/metro-config"); 3 | 4 | // eslint-disable-next-line no-undef 5 | module.exports = getDefaultConfig(__dirname); 6 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # Android/IntelliJ 2 | # 3 | build/ 4 | .idea 5 | .gradle 6 | local.properties 7 | *.iml 8 | *.hprof 9 | 10 | # BUCK 11 | buck-out/ 12 | \.buckd/ 13 | *.keystore 14 | !debug.keystore 15 | 16 | # Bundle artifacts 17 | *.jsbundle 18 | -------------------------------------------------------------------------------- /ios/mobile/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | #import 6 | 7 | @interface AppDelegate : EXAppDelegateWrapper 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | env: { 6 | production: { 7 | plugins: ["react-native-paper/babel"], 8 | }, 9 | }, 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /ios/mobile/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | .env 13 | yarn-error.log 14 | 15 | # OSX 16 | # 17 | .DS_Store 18 | 19 | # Bundle artifacts 20 | *.jsbundle 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff 3 | #FFFFFF 4 | #023c69 5 | #ffffff 6 | -------------------------------------------------------------------------------- /client/components/Home/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react-native"; 2 | 3 | import Home from "../index"; 4 | 5 | test("Home displays title", () => { 6 | render(); 7 | 8 | expect(screen.getByText("Home")).toBeVisible(); 9 | }); 10 | -------------------------------------------------------------------------------- /ios/mobile/mobile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | -------------------------------------------------------------------------------- /ios/mobile.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/components/Browse/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "react-native-paper"; 2 | import { SafeAreaView } from "react-native-safe-area-context"; 3 | 4 | const Browse = () => { 5 | return ( 6 | 7 | Browse 8 | 9 | ); 10 | }; 11 | 12 | export default Browse; 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/components/Library/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "react-native-paper"; 2 | import { SafeAreaView } from "react-native-safe-area-context"; 3 | 4 | const Library = () => { 5 | return ( 6 | 7 | Library 8 | 9 | ); 10 | }; 11 | 12 | export default Library; 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from "expo"; 2 | 3 | import App from "./App"; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | commitlint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - uses: wagoid/commitlint-github-action@v4 -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | mobile 3 | contain 4 | false 5 | automatic 6 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | project.xcworkspace 20 | .xcode.env.local 21 | 22 | # CocoaPods 23 | /Pods/ 24 | 25 | # Bundle artifacts 26 | *.jsbundle 27 | -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/SplashScreen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /android/app/src/main/jni/MainApplicationModuleProvider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | std::shared_ptr MainApplicationModuleProvider( 12 | const std::string moduleName, 13 | const JavaTurboModule::InitParams ¶ms); 14 | 15 | } // namespace react 16 | } // namespace facebook 17 | -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/SplashScreenBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /android/app/src/main/jni/OnLoad.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "MainApplicationTurboModuleManagerDelegate.h" 3 | #include "MainComponentsRegistry.h" 4 | 5 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { 6 | return facebook::jni::initialize(vm, [] { 7 | facebook::react::MainApplicationTurboModuleManagerDelegate:: 8 | registerNatives(); 9 | facebook::react::MainComponentsRegistry::registerNatives(); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /client/utils/index.ts: -------------------------------------------------------------------------------- 1 | function padWithZero(number: number) { 2 | const string = String(number); 3 | if (number < 10) { 4 | return "0" + string; 5 | } 6 | return string; 7 | } 8 | 9 | export function getMMSSFromMillis(milliseconds: number) { 10 | const totalSeconds = milliseconds / 1000; 11 | const seconds = Math.floor(totalSeconds % 60); 12 | const minutes = Math.floor(totalSeconds / 60); 13 | 14 | return padWithZero(minutes) + ":" + padWithZero(seconds); 15 | } 16 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 2.6.0" 4 | }, 5 | "build": { 6 | "development": { 7 | "developmentClient": true, 8 | "distribution": "internal", 9 | "android": { 10 | "buildType": "apk" 11 | }, 12 | "ios": { 13 | "simulator": true 14 | } 15 | }, 16 | "preview": { 17 | "distribution": "internal" 18 | }, 19 | "production": {} 20 | }, 21 | "submit": { 22 | "production": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | 3 | const config: Config.InitialOptions = { 4 | preset: "jest-expo", 5 | transformIgnorePatterns: [ 6 | "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)", 7 | ], 8 | setupFilesAfterEnv: ["/jest.setupFilesAfterEnv.ts"], 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /.github/workflows/custom-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint primarily with prettier 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | 10 | Prettier: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - uses: actions/setup-node@v2 16 | 17 | - name: Install our packages 18 | run: yarn install 19 | 20 | - name: Run Prettier --check 21 | run: yarn prettier --check "**/*.{js,jsx,ts,tsx}" 22 | 23 | - name: Typescript 24 | run: yarn tsc --noEmit 25 | -------------------------------------------------------------------------------- /client/hooks/useDebouncedState.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export function useDebouncedState(defaultValue: string, delay: number = 300) { 4 | const [value, setValue] = useState(defaultValue); 5 | const [debouncedValue, setDebouncedValue] = useState(""); 6 | 7 | useEffect(() => { 8 | const handler = setTimeout(() => { 9 | setDebouncedValue(value); 10 | }, delay); 11 | 12 | return () => { 13 | clearTimeout(handler); 14 | }; 15 | }, [value]); 16 | 17 | return { value, setValue, debouncedValue }; 18 | } 19 | -------------------------------------------------------------------------------- /ios/mobile/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | EXUpdatesSDKVersion 12 | 46.0.0 13 | EXUpdatesURL 14 | https://exp.host/@resonatecoop/mobile 15 | 16 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # react-native-reanimated 11 | -keep class com.swmansion.reanimated.** { *; } 12 | -keep class com.facebook.react.turbomodule.** { *; } 13 | 14 | # Add any project specific keep options here: 15 | -------------------------------------------------------------------------------- /.github/workflows/github-actions.yaml: -------------------------------------------------------------------------------- 1 | name: Auto Assign to Project(s) 2 | 3 | on: 4 | issues: 5 | types: [opened, labeled] 6 | pull_request_target: 7 | types: [opened, labeled] 8 | issue_comment: 9 | types: [created] 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | assign_one_project: 15 | runs-on: ubuntu-latest 16 | name: Assign to One Project 17 | steps: 18 | - name: Assign NEW issues and NEW pull requests to project 1 19 | uses: srggrs/assign-one-project-github-action@1.2.1 20 | if: github.event.action == 'opened' 21 | with: 22 | project: 'https://github.com/resonatecoop/mobile/projects/1' 23 | -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /client/api/error.ts: -------------------------------------------------------------------------------- 1 | export interface ReportErrorInfo { 2 | [k: string]: unknown; 3 | } 4 | 5 | export class ExtendedError extends Error { 6 | extraInfo: Record; 7 | constructor(message: string) { 8 | super(message); 9 | this.name = this.constructor.name; 10 | this.extraInfo = {}; 11 | } 12 | addExtraInfo(info: ReportErrorInfo) { 13 | Object.assign(this.extraInfo, info); 14 | } 15 | } 16 | 17 | export class FetchError extends ExtendedError { 18 | status: number; 19 | url: string; 20 | constructor(message: string, url: string, res?: Response) { 21 | super(message); 22 | this.status = res?.status ?? 500; 23 | this.url = url; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /client/hooks/useKeyboardVisible.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Keyboard } from "react-native"; 3 | 4 | export default function useKeyboardVisible() { 5 | const [keyboardVisible, setKeyboardVisible] = useState(false); 6 | 7 | useEffect(() => { 8 | const showSubscription = Keyboard.addListener("keyboardDidShow", () => { 9 | setKeyboardVisible(true); 10 | }); 11 | const hideSubscription = Keyboard.addListener("keyboardDidHide", () => { 12 | setKeyboardVisible(false); 13 | }); 14 | 15 | return () => { 16 | showSubscription.remove(); 17 | hideSubscription.remove(); 18 | }; 19 | }, []); 20 | 21 | return keyboardVisible; 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /client/components/CustomStatusBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar, StatusBarStyle, View } from "react-native"; 2 | import { useSafeAreaInsets } from "react-native-safe-area-context"; 3 | 4 | import { useThemeMode } from "../../theme"; 5 | 6 | export default function CustomStatusBar() { 7 | const insets = useSafeAreaInsets(); 8 | const { mode } = useThemeMode(); 9 | const isDark: boolean = mode === "dark"; 10 | const backgroundColor: string = isDark ? "#000" : "#fff"; 11 | const barStyle: StatusBarStyle = isDark ? "light-content" : "dark-content"; 12 | 13 | return ( 14 | 20 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: resonate # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainApplicationModuleProvider.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationModuleProvider.h" 2 | 3 | #include 4 | 5 | namespace facebook { 6 | namespace react { 7 | 8 | std::shared_ptr MainApplicationModuleProvider( 9 | const std::string moduleName, 10 | const JavaTurboModule::InitParams ¶ms) { 11 | // Here you can provide your own module provider for TurboModules coming from 12 | // either your application or from external libraries. The approach to follow 13 | // is similar to the following (for a library called `samplelibrary`: 14 | // 15 | // auto module = samplelibrary_ModuleProvider(moduleName, params); 16 | // if (module != nullptr) { 17 | // return module; 18 | // } 19 | // return rncore_ModuleProvider(moduleName, params); 20 | return rncore_ModuleProvider(moduleName, params); 21 | } 22 | 23 | } // namespace react 24 | } // namespace facebook 25 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 17 | -------------------------------------------------------------------------------- /client/components/common/ScreenView.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType, PropsWithChildren } from "react"; 2 | import { 3 | SafeAreaView, 4 | StyleProp, 5 | View, 6 | ViewStyle, 7 | StyleSheet, 8 | } from "react-native"; 9 | import { useTheme } from "react-native-paper"; 10 | 11 | import { PLAYER_HEIGHT } from "../../constants"; 12 | 13 | const ScreenView: ComponentType< 14 | PropsWithChildren<{ style?: StyleProp }> 15 | > = (props) => { 16 | const theme = useTheme(); 17 | return ( 18 | 19 | 26 | {props.children} 27 | 28 | 29 | ); 30 | }; 31 | 32 | const styles = StyleSheet.create({ 33 | content: { 34 | paddingBottom: PLAYER_HEIGHT, 35 | }, 36 | }); 37 | 38 | export default ScreenView; 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import * as React from "react"; 3 | import { 4 | SafeAreaProvider, 5 | initialWindowMetrics, 6 | } from "react-native-safe-area-context"; 7 | 8 | import CustomStatusBar from "./client/components/CustomStatusBar"; 9 | import Player from "./client/components/Player"; 10 | import RootTabNavigator from "./client/navigation/RootTabNavigator"; 11 | import { PaperNavigationProvider, ThemeModeProvider } from "./client/theme"; 12 | 13 | const queryClient = new QueryClient(); 14 | 15 | export default function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainComponentsRegistry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | class MainComponentsRegistry 12 | : public facebook::jni::HybridClass { 13 | public: 14 | // Adapt it to the package you used for your Java class. 15 | constexpr static auto kJavaDescriptor = 16 | "Lcom/resonatecoop/mobile/newarchitecture/components/MainComponentsRegistry;"; 17 | 18 | static void registerNatives(); 19 | 20 | MainComponentsRegistry(ComponentFactory *delegate); 21 | 22 | private: 23 | static std::shared_ptr 24 | sharedProviderRegistry(); 25 | 26 | static jni::local_ref initHybrid( 27 | jni::alias_ref, 28 | ComponentFactory *delegate); 29 | }; 30 | 31 | } // namespace react 32 | } // namespace facebook 33 | -------------------------------------------------------------------------------- /client/api/fetch-with-timeout.ts: -------------------------------------------------------------------------------- 1 | import { FetchError } from "./error"; 2 | 3 | export type FetchOptions = RequestInit & { 4 | timeout?: number; 5 | }; 6 | 7 | const DEFAULT_TIMEOUT = 15000; 8 | 9 | export default async function fetchWithTimeout( 10 | url: string, 11 | opts: FetchOptions 12 | ): ReturnType { 13 | const controller = new AbortController(); 14 | 15 | const headers = { 16 | ...opts.headers, 17 | }; 18 | 19 | const abortTimer = setTimeout(() => { 20 | controller.abort("request timed out"); 21 | }, opts.timeout || DEFAULT_TIMEOUT); 22 | 23 | return fetch(url, { 24 | ...opts, 25 | headers, 26 | signal: controller.signal, 27 | }) 28 | .then(async (res) => { 29 | if (!res.ok) { 30 | const payload = await res.json(); 31 | 32 | const err = new FetchError( 33 | `Request failed: ${url} ${res.status} ${payload.message}`, 34 | url, 35 | res 36 | ); 37 | 38 | return Promise.reject(err); 39 | } 40 | return res; 41 | }) 42 | .finally(() => { 43 | clearTimeout(abortTimer); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mobile' 2 | 3 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); 4 | useExpoModules() 5 | 6 | apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); 7 | applyNativeModulesSettingsGradle(settings) 8 | 9 | include ':app' 10 | includeBuild(new File(["node", "--print", "require.resolve('react-native-gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile()) 11 | 12 | if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") { 13 | include(":ReactAndroid") 14 | project(":ReactAndroid").projectDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../ReactAndroid"); 15 | include(":ReactAndroid:hermes-engine") 16 | project(":ReactAndroid:hermes-engine").projectDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../ReactAndroid/hermes-engine"); 17 | } 18 | -------------------------------------------------------------------------------- /client/test/index.ts: -------------------------------------------------------------------------------- 1 | class PlaylistItem { 2 | name: string; 3 | uri: string; 4 | isVideo: boolean; 5 | constructor(name: string, uri: string, isVideo: boolean) { 6 | this.name = name; 7 | this.uri = uri; 8 | this.isVideo = isVideo; 9 | } 10 | } 11 | 12 | export const PLAYLIST = [ 13 | new PlaylistItem( 14 | "Comfort Fit - “Sorry”", 15 | "https://s3.amazonaws.com/exp-us-standard/audio/playlist-example/Comfort_Fit_-_03_-_Sorry.mp3", 16 | false 17 | ), 18 | new PlaylistItem( 19 | "Big Buck Bunny", 20 | "http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4", 21 | true 22 | ), 23 | new PlaylistItem( 24 | "Mildred Bailey – “All Of Me”", 25 | "https://ia800304.us.archive.org/34/items/PaulWhitemanwithMildredBailey/PaulWhitemanwithMildredBailey-AllofMe.mp3", 26 | false 27 | ), 28 | new PlaylistItem( 29 | "Popeye - I don't scare", 30 | "https://ia800501.us.archive.org/11/items/popeye_i_dont_scare/popeye_i_dont_scare_512kb.mp4", 31 | true 32 | ), 33 | new PlaylistItem( 34 | "Podington Bear - “Rubber Robot”", 35 | "https://s3.amazonaws.com/exp-us-standard/audio/playlist-example/Podington_Bear_-_Rubber_Robot.mp3", 36 | false 37 | ), 38 | ]; 39 | -------------------------------------------------------------------------------- /client/components/Home/CarouselListCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet } from "react-native"; 3 | import { Card, Title } from "react-native-paper"; 4 | 5 | export interface CarouselListCardProps { 6 | image: string; 7 | title: string; 8 | } 9 | 10 | export default function CarouselListCard({ 11 | image, 12 | title, 13 | }: CarouselListCardProps) { 14 | return ( 15 | 16 | 22 | 23 | 24 | {title} 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | const CARD_WIDTH = 150; 32 | 33 | const styles = StyleSheet.create({ 34 | container: { 35 | width: CARD_WIDTH, 36 | }, 37 | image: { 38 | height: CARD_WIDTH, 39 | width: CARD_WIDTH, 40 | }, 41 | content: { 42 | paddingHorizontal: 4, 43 | width: "100%", 44 | paddingBottom: 0, 45 | }, 46 | title: { 47 | marginTop: 0, 48 | fontSize: 12, 49 | fontWeight: "700", 50 | marginVertical: 0, 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | namespace facebook { 8 | namespace react { 9 | 10 | class MainApplicationTurboModuleManagerDelegate 11 | : public jni::HybridClass< 12 | MainApplicationTurboModuleManagerDelegate, 13 | TurboModuleManagerDelegate> { 14 | public: 15 | // Adapt it to the package you used for your Java class. 16 | static constexpr auto kJavaDescriptor = 17 | "Lcom/resonatecoop/mobile/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;"; 18 | 19 | static jni::local_ref initHybrid(jni::alias_ref); 20 | 21 | static void registerNatives(); 22 | 23 | std::shared_ptr getTurboModule( 24 | const std::string name, 25 | const std::shared_ptr jsInvoker) override; 26 | std::shared_ptr getTurboModule( 27 | const std::string name, 28 | const JavaTurboModule::InitParams ¶ms) override; 29 | 30 | /** 31 | * Test-only method. Allows user to verify whether a TurboModule can be 32 | * created by instances of this class. 33 | */ 34 | bool canCreateTurboModule(std::string name); 35 | }; 36 | 37 | } // namespace react 38 | } // namespace facebook 39 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/resonatecoop/mobile/newarchitecture/components/MainComponentsRegistry.java: -------------------------------------------------------------------------------- 1 | package com.resonatecoop.mobile.newarchitecture.components; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.proguard.annotations.DoNotStrip; 5 | import com.facebook.react.fabric.ComponentFactory; 6 | import com.facebook.soloader.SoLoader; 7 | 8 | /** 9 | * Class responsible to load the custom Fabric Components. This class has native methods and needs a 10 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 11 | * folder for you). 12 | * 13 | *

Please note that this class is used ONLY if you opt-in for the New Architecture (see the 14 | * `newArchEnabled` property). Is ignored otherwise. 15 | */ 16 | @DoNotStrip 17 | public class MainComponentsRegistry { 18 | static { 19 | SoLoader.loadLibrary("fabricjni"); 20 | } 21 | 22 | @DoNotStrip private final HybridData mHybridData; 23 | 24 | @DoNotStrip 25 | private native HybridData initHybrid(ComponentFactory componentFactory); 26 | 27 | @DoNotStrip 28 | private MainComponentsRegistry(ComponentFactory componentFactory) { 29 | mHybridData = initHybrid(componentFactory); 30 | } 31 | 32 | @DoNotStrip 33 | public static MainComponentsRegistry register(ComponentFactory componentFactory) { 34 | return new MainComponentsRegistry(componentFactory); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | import { ExpoConfig, ConfigContext } from "@expo/config"; 2 | import * as dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | export default function config({ config }: ConfigContext): ExpoConfig { 7 | return { 8 | ...config, 9 | name: "mobile", 10 | slug: "mobile", 11 | version: "1.0.0", 12 | orientation: "portrait", 13 | icon: "./assets/icon.png", 14 | userInterfaceStyle: "automatic", 15 | splash: { 16 | image: "./assets/splash.png", 17 | resizeMode: "contain", 18 | backgroundColor: "#ffffff", 19 | }, 20 | updates: { 21 | fallbackToCacheTimeout: 0, 22 | }, 23 | assetBundlePatterns: ["**/*"], 24 | ios: { 25 | supportsTablet: true, 26 | infoPlist: { 27 | UIBackgroundModes: ["audio"], 28 | }, 29 | bundleIdentifier: "com.resonatecoop.mobile", 30 | }, 31 | android: { 32 | adaptiveIcon: { 33 | foregroundImage: "./assets/adaptive-icon.png", 34 | backgroundColor: "#FFFFFF", 35 | }, 36 | package: "com.resonatecoop.mobile", 37 | }, 38 | web: { 39 | favicon: "./assets/favicon.png", 40 | }, 41 | plugins: ["expo-community-flipper"], 42 | extra: { 43 | eas: { 44 | projectId: "0d5b1fc5-4d05-4f8d-bfa7-9e0eddc180db", 45 | }, 46 | apiUrl: process.env.API_URL, 47 | }, 48 | owner: "resonatecoop", 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /client/components/Home/CarouselList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, FlatList, StyleSheet, ListRenderItem } from "react-native"; 3 | import { Title } from "react-native-paper"; 4 | 5 | interface CarouselListProps { 6 | title: string; 7 | itemGap?: number; 8 | keyExtractor: (item: TItem, index: number) => string; 9 | data: TItem[] | null | undefined; 10 | renderItem: ListRenderItem; 11 | } 12 | function CarouselList({ 13 | title, 14 | data, 15 | itemGap = 12, 16 | keyExtractor, 17 | renderItem, 18 | }: CarouselListProps) { 19 | return ( 20 | 21 | {title} 22 | ( 30 | 31 | )} 32 | /> 33 | 34 | ); 35 | } 36 | 37 | const styles = StyleSheet.create({ 38 | container: { 39 | paddingTop: 24, 40 | }, 41 | title: { 42 | marginBottom: 12, 43 | }, 44 | horizontalSpacing: { 45 | paddingHorizontal: 12, 46 | }, 47 | }); 48 | 49 | export default CarouselList; 50 | -------------------------------------------------------------------------------- /client/components/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ScrollView } from "react-native"; 3 | import { SafeAreaView } from "react-native-safe-area-context"; 4 | 5 | import { useLastUpdatedArtists } from "../../api/artists"; 6 | import { useFeaturedPlaylists } from "../../api/trackgroups"; 7 | import CarouselList from "./CarouselList"; 8 | import CarouselListCard from "./CarouselListCard"; 9 | 10 | const Home = () => { 11 | const { data: lastUpdatedArtists } = useLastUpdatedArtists(); 12 | const { data: featuredPlaylists } = useFeaturedPlaylists(); 13 | 14 | return ( 15 | 16 | 17 | item.id} 20 | data={featuredPlaylists?.data} 21 | renderItem={({ item }) => ( 22 | 26 | )} 27 | /> 28 | item.id.toString()} 31 | data={lastUpdatedArtists?.data} 32 | renderItem={({ item }) => ( 33 | 37 | )} 38 | /> 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default Home; 45 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.resonatecoop.mobile", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.resonatecoop.mobile", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /client/components/Search/search-results.tsx: -------------------------------------------------------------------------------- 1 | import { View, StyleSheet, Dimensions } from "react-native"; 2 | import { Text, List } from "react-native-paper"; 3 | 4 | import { PLAYER_HEIGHT } from "../../constants"; 5 | 6 | interface SearchResultItemProps { 7 | id: string; 8 | artist: string; 9 | title: string; 10 | kind: string; 11 | imageUrl?: string; 12 | imageWidth?: number; 13 | imageHeight?: number; 14 | } 15 | export function SearchResultTrackItem({ 16 | title, 17 | artist, 18 | imageUrl, 19 | imageWidth, 20 | imageHeight, 21 | }: SearchResultItemProps) { 22 | return ( 23 | ( 27 | 32 | )} 33 | /> 34 | ); 35 | } 36 | 37 | interface SearchResultsEmptyProps { 38 | searchQuery: string; 39 | } 40 | 41 | export function SearchResultsEmpty({ searchQuery }: SearchResultsEmptyProps) { 42 | return ( 43 | 44 | {`Couldn't find "${searchQuery}"`} 45 | 46 | Try searching again using different keywords 47 | 48 | 49 | ); 50 | } 51 | 52 | const styles = StyleSheet.create({ 53 | container: { 54 | height: Dimensions.get("window").height - PLAYER_HEIGHT, 55 | justifyContent: "center", 56 | alignItems: "center", 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp: -------------------------------------------------------------------------------- 1 | #include "MainApplicationTurboModuleManagerDelegate.h" 2 | #include "MainApplicationModuleProvider.h" 3 | 4 | namespace facebook { 5 | namespace react { 6 | 7 | jni::local_ref 8 | MainApplicationTurboModuleManagerDelegate::initHybrid( 9 | jni::alias_ref) { 10 | return makeCxxInstance(); 11 | } 12 | 13 | void MainApplicationTurboModuleManagerDelegate::registerNatives() { 14 | registerHybrid({ 15 | makeNativeMethod( 16 | "initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid), 17 | makeNativeMethod( 18 | "canCreateTurboModule", 19 | MainApplicationTurboModuleManagerDelegate::canCreateTurboModule), 20 | }); 21 | } 22 | 23 | std::shared_ptr 24 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 25 | const std::string name, 26 | const std::shared_ptr jsInvoker) { 27 | // Not implemented yet: provide pure-C++ NativeModules here. 28 | return nullptr; 29 | } 30 | 31 | std::shared_ptr 32 | MainApplicationTurboModuleManagerDelegate::getTurboModule( 33 | const std::string name, 34 | const JavaTurboModule::InitParams ¶ms) { 35 | return MainApplicationModuleProvider(name, params); 36 | } 37 | 38 | bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule( 39 | std::string name) { 40 | return getTurboModule(name, nullptr) != nullptr || 41 | getTurboModule(name, {.moduleName = name}) != nullptr; 42 | } 43 | 44 | } // namespace react 45 | } // namespace facebook 46 | -------------------------------------------------------------------------------- /android/app/src/main/jni/Android.mk: -------------------------------------------------------------------------------- 1 | THIS_DIR := $(call my-dir) 2 | 3 | include $(REACT_ANDROID_DIR)/Android-prebuilt.mk 4 | 5 | # If you wish to add a custom TurboModule or Fabric component in your app you 6 | # will have to include the following autogenerated makefile. 7 | # include $(GENERATED_SRC_DIR)/codegen/jni/Android.mk 8 | include $(CLEAR_VARS) 9 | 10 | LOCAL_PATH := $(THIS_DIR) 11 | 12 | # You can customize the name of your application .so file here. 13 | LOCAL_MODULE := mobile_appmodules 14 | 15 | LOCAL_C_INCLUDES := $(LOCAL_PATH) 16 | LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) 17 | LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) 18 | 19 | # If you wish to add a custom TurboModule or Fabric component in your app you 20 | # will have to uncomment those lines to include the generated source 21 | # files from the codegen (placed in $(GENERATED_SRC_DIR)/codegen/jni) 22 | # 23 | # LOCAL_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni 24 | # LOCAL_SRC_FILES += $(wildcard $(GENERATED_SRC_DIR)/codegen/jni/*.cpp) 25 | # LOCAL_EXPORT_C_INCLUDES += $(GENERATED_SRC_DIR)/codegen/jni 26 | 27 | # Here you should add any native library you wish to depend on. 28 | LOCAL_SHARED_LIBRARIES := \ 29 | libfabricjni \ 30 | libfbjni \ 31 | libfolly_runtime \ 32 | libglog \ 33 | libjsi \ 34 | libreact_codegen_rncore \ 35 | libreact_debug \ 36 | libreact_nativemodule_core \ 37 | libreact_render_componentregistry \ 38 | libreact_render_core \ 39 | libreact_render_debug \ 40 | libreact_render_graphics \ 41 | librrc_view \ 42 | libruntimeexecutor \ 43 | libturbomodulejsijni \ 44 | libyoga 45 | 46 | LOCAL_CFLAGS := -DLOG_TAG=\"ReactNative\" -fexceptions -frtti -std=c++17 -Wall 47 | 48 | include $(BUILD_SHARED_LIBRARY) 49 | -------------------------------------------------------------------------------- /client/navigation/RootTabNavigator.tsx: -------------------------------------------------------------------------------- 1 | import MaterialCommunityIcon from "@expo/vector-icons/MaterialCommunityIcons"; 2 | import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs"; 3 | 4 | import Browse from "../components/Browse"; 5 | import Home from "../components/Home"; 6 | import Library from "../components/Library"; 7 | import Search from "../components/Search"; 8 | import { BOTTOM_NAVIGATION_HEIGHT } from "../constants"; 9 | 10 | const Tab = createMaterialBottomTabNavigator(); 11 | 12 | export default function RootTabNavigator() { 13 | return ( 14 | 19 | ( 24 | 25 | ), 26 | }} 27 | /> 28 | ( 33 | 34 | ), 35 | }} 36 | /> 37 | ( 42 | 43 | ), 44 | }} 45 | /> 46 | ( 51 | 52 | ), 53 | }} 54 | /> 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /client/components/Search/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { View, FlatList } from "react-native"; 3 | import { ProgressBar, Searchbar } from "react-native-paper"; 4 | 5 | import { Image, useSearch } from "../../api/search"; 6 | import { useDebouncedState } from "../../hooks/useDebouncedState"; 7 | import ScreenView from "../common/ScreenView"; 8 | import { SearchResultTrackItem, SearchResultsEmpty } from "./search-results"; 9 | 10 | const Search = () => { 11 | const { 12 | value, 13 | setValue, 14 | debouncedValue: searchQuery, 15 | } = useDebouncedState(""); 16 | const { 17 | data: searchResult, 18 | isFetching, 19 | error, 20 | } = useSearch({ q: searchQuery }); 21 | 22 | const tracks = useMemo(() => { 23 | return searchResult?.data.filter((item) => item.kind === "track") ?? []; 24 | }, [searchResult]); 25 | 26 | const hasNoResults = error != null && tracks.length === 0; 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | 38 | ) : null 39 | } 40 | renderItem={({ item }) => { 41 | return ( 42 | 51 | ); 52 | }} 53 | /> 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default Search; 60 | -------------------------------------------------------------------------------- /client/api/search.ts: -------------------------------------------------------------------------------- 1 | import { useQuery, UseQueryOptions } from "@tanstack/react-query"; 2 | 3 | import { FetchError } from "./error"; 4 | import { FetchClient } from "./fetch-client"; 5 | 6 | const searchAPI = new FetchClient({ 7 | baseURL: "https://stream.resonate.coop/api/v3/search", 8 | }); 9 | 10 | const searchKeys = { 11 | all: ["search"] as const, 12 | list: (filters: SearchParams) => 13 | [...searchKeys.all, "list", { filters }] as const, 14 | }; 15 | 16 | export function useSearch(params: SearchParams, options?: UseQueryOptions) { 17 | return useQuery({ 18 | queryKey: searchKeys.list(params), 19 | queryFn: ({ queryKey: [_, __, { filters }] }) => search(filters), 20 | enabled: params.q !== "", 21 | retry: (errorCount, error) => { 22 | if (errorCount > 3) { 23 | return false; 24 | } 25 | if (error instanceof FetchError) { 26 | switch (error.status) { 27 | case 429: 28 | return true; 29 | default: 30 | return false; 31 | } 32 | } 33 | return false; 34 | }, 35 | }); 36 | } 37 | 38 | function search({ limit, page, q }: SearchParams): Promise { 39 | const filters: Record = { 40 | q: q.trim(), 41 | }; 42 | if (limit != null) { 43 | filters.limit = limit.toString(); 44 | } 45 | if (page != null) { 46 | filters.page = page.toString(); 47 | } 48 | return searchAPI.get("", filters); 49 | } 50 | 51 | interface SearchResponse { 52 | data: { 53 | tags: string[]; 54 | _id: string; 55 | track_id: number; 56 | display_artist: string; 57 | title: string; 58 | kind: string; 59 | score: number; 60 | cover: string; 61 | images: { 62 | small: Image; 63 | medium: Image; 64 | }; 65 | }[]; 66 | } 67 | 68 | export interface Image { 69 | width: number; 70 | height: number; 71 | url: string; 72 | } 73 | 74 | interface SearchParams { 75 | limit?: number; 76 | page?: number; 77 | q: string; 78 | } 79 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/resonatecoop/mobile/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java: -------------------------------------------------------------------------------- 1 | package com.resonatecoop.mobile.newarchitecture.modules; 2 | 3 | import com.facebook.jni.HybridData; 4 | import com.facebook.react.ReactPackage; 5 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.soloader.SoLoader; 8 | import java.util.List; 9 | 10 | /** 11 | * Class responsible to load the TurboModules. This class has native methods and needs a 12 | * corresponding C++ implementation/header file to work correctly (already placed inside the jni/ 13 | * folder for you). 14 | * 15 | *

Please note that this class is used ONLY if you opt-in for the New Architecture (see the 16 | * `newArchEnabled` property). Is ignored otherwise. 17 | */ 18 | public class MainApplicationTurboModuleManagerDelegate 19 | extends ReactPackageTurboModuleManagerDelegate { 20 | 21 | private static volatile boolean sIsSoLibraryLoaded; 22 | 23 | protected MainApplicationTurboModuleManagerDelegate( 24 | ReactApplicationContext reactApplicationContext, List packages) { 25 | super(reactApplicationContext, packages); 26 | } 27 | 28 | protected native HybridData initHybrid(); 29 | 30 | native boolean canCreateTurboModule(String moduleName); 31 | 32 | public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder { 33 | protected MainApplicationTurboModuleManagerDelegate build( 34 | ReactApplicationContext context, List packages) { 35 | return new MainApplicationTurboModuleManagerDelegate(context, packages); 36 | } 37 | } 38 | 39 | @Override 40 | protected synchronized void maybeLoadOtherSoLibraries() { 41 | if (!sIsSoLibraryLoaded) { 42 | // If you change the name of your application .so file in the Android.mk file, 43 | // make sure you update the name here as well. 44 | SoLoader.loadLibrary("mobile_appmodules"); 45 | sIsSoLibraryLoaded = true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /android/app/src/main/jni/MainComponentsRegistry.cpp: -------------------------------------------------------------------------------- 1 | #include "MainComponentsRegistry.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {} 12 | 13 | std::shared_ptr 14 | MainComponentsRegistry::sharedProviderRegistry() { 15 | auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry(); 16 | 17 | // Custom Fabric Components go here. You can register custom 18 | // components coming from your App or from 3rd party libraries here. 19 | // 20 | // providerRegistry->add(concreteComponentDescriptorProvider< 21 | // AocViewerComponentDescriptor>()); 22 | return providerRegistry; 23 | } 24 | 25 | jni::local_ref 26 | MainComponentsRegistry::initHybrid( 27 | jni::alias_ref, 28 | ComponentFactory *delegate) { 29 | auto instance = makeCxxInstance(delegate); 30 | 31 | auto buildRegistryFunction = 32 | [](EventDispatcher::Weak const &eventDispatcher, 33 | ContextContainer::Shared const &contextContainer) 34 | -> ComponentDescriptorRegistry::Shared { 35 | auto registry = MainComponentsRegistry::sharedProviderRegistry() 36 | ->createComponentDescriptorRegistry( 37 | {eventDispatcher, contextContainer}); 38 | 39 | auto mutableRegistry = 40 | std::const_pointer_cast(registry); 41 | 42 | mutableRegistry->setFallbackComponentDescriptor( 43 | std::make_shared( 44 | ComponentDescriptorParameters{ 45 | eventDispatcher, contextContainer, nullptr})); 46 | 47 | return registry; 48 | }; 49 | 50 | delegate->buildRegistryFunction = buildRegistryFunction; 51 | return instance; 52 | } 53 | 54 | void MainComponentsRegistry::registerNatives() { 55 | registerHybrid({ 56 | makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid), 57 | }); 58 | } 59 | 60 | } // namespace react 61 | } // namespace facebook 62 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Automatically convert third-party libraries to use AndroidX 26 | android.enableJetifier=true 27 | 28 | # Version of flipper SDK to use with React Native 29 | 30 | # Use this property to specify which architecture you want to build. 31 | # You can also override it from the CLI using 32 | # ./gradlew -PreactNativeArchitectures=x86_64 33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 34 | 35 | # Use this property to enable support to the new architecture. 36 | # This will allow you to use TurboModules and the Fabric render in 37 | # your application. You should enable this flag either if you want 38 | # to write custom TurboModules/Fabric components OR use libraries that 39 | # are providing them. 40 | newArchEnabled=false 41 | 42 | # The hosted JavaScript engine 43 | # Supported values: expo.jsEngine = "hermes" | "jsc" 44 | expo.jsEngine=jsc 45 | 46 | # Enable GIF support in React Native images (~200 B increase) 47 | expo.gif.enabled=true 48 | # Enable webp support in React Native images (~85 KB increase) 49 | expo.webp.enabled=true 50 | # Enable animated webp support (~3.4 MB increase) 51 | # Disabled by default because iOS doesn't support animated webp 52 | expo.webp.animated=false 53 | 54 | FLIPPER_VERSION=0.125.0 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resonate-mobile", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "expo start --dev-client", 6 | "android": "expo run:android", 7 | "expo:prebuild": "expo prebuild", 8 | "eslint:check": "eslint . --ext .jsx,.js,.ts,.tsx", 9 | "prettier:check": "prettier --check '**/*.{js,ts,jsx,tsx}'", 10 | "eslint:fix": "eslint --fix . --ext .jsx,.js,.ts,.tsx", 11 | "prettier:fix": "prettier --write '**/*.{js,ts,jsx,tsx}'", 12 | "lint": "npm run eslint:check && npm run prettier:check", 13 | "lint:fix": "npm run eslint:fix && npm run prettier:fix", 14 | "ios": "expo run:ios", 15 | "web": "expo start --web", 16 | "eject": "expo eject", 17 | "prepare": "husky install", 18 | "test:watch": "jest --watch --coverage=false --changedSince=origin/main", 19 | "test": "jest" 20 | }, 21 | "dependencies": { 22 | "@react-native-community/slider": "4.2.3", 23 | "@react-navigation/material-bottom-tabs": "^6.2.4", 24 | "@react-navigation/native": "^6.0.13", 25 | "@tanstack/react-query": "^4.13.5", 26 | "dotenv": "^16.0.3", 27 | "expo": "~46.0.17", 28 | "expo-av": "~12.0.4", 29 | "expo-community-flipper": "^46.0.2", 30 | "expo-dev-client": "~1.3.1", 31 | "expo-splash-screen": "~0.16.2", 32 | "expo-status-bar": "~1.4.0", 33 | "expo-system-ui": "~1.3.0", 34 | "jest": "^26.6.3", 35 | "jest-expo": "^47.0.1", 36 | "lodash": "^4.17.21", 37 | "react": "18.0.0", 38 | "react-dom": "18.0.0", 39 | "react-native": "0.69.6", 40 | "react-native-flipper": "^0.173.0", 41 | "react-native-paper": "5.0.0-rc.10", 42 | "react-native-safe-area-context": "4.3.1", 43 | "react-native-screens": "~3.15.0", 44 | "react-native-web": "~0.18.7" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.18.6", 48 | "@commitlint/cli": "^17.2.0", 49 | "@commitlint/config-conventional": "^17.2.0", 50 | "@expo/cli": "^0.4.8", 51 | "@jest/types": "^29.3.1", 52 | "@testing-library/jest-native": "^5.2.0", 53 | "@testing-library/react-native": "^11.4.0", 54 | "@types/jest": "^29.2.2", 55 | "@types/lodash": "^4.14.184", 56 | "@types/react-dom": "^18.0.8", 57 | "@types/react-native": "^0.70.6", 58 | "eslint": "^8.26.0", 59 | "eslint-config-universe": "^11.1.0", 60 | "husky": ">=7", 61 | "lint-staged": ">=10", 62 | "prettier": "^2.7.1", 63 | "ts-node": "^10.9.1", 64 | "typescript": "^4.6.3" 65 | }, 66 | "private": true, 67 | "lint-staged": { 68 | "*.js": "eslint --cache --fix", 69 | "*.{js,css,md}": "prettier --write" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") 2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") 3 | require File.join(File.dirname(`node --print "require.resolve('@react-native-community/cli-platform-ios/package.json')"`), "native_modules") 4 | 5 | require 'json' 6 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} 7 | 8 | platform :ios, podfile_properties['ios.deploymentTarget'] || '12.4' 9 | install! 'cocoapods', 10 | :deterministic_uuids => false 11 | 12 | target 'mobile' do 13 | use_expo_modules! 14 | config = use_native_modules! 15 | 16 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] 17 | 18 | # Flags change depending on the env values. 19 | flags = get_default_flags() 20 | 21 | use_react_native!( 22 | :path => config[:reactNativePath], 23 | # @generated begin expo-community-flipper-isprod - expo prebuild (DO NOT MODIFY) sync-828c22a1a38236bf5b7c203393f474bc68356b34 24 | # ENV value added to support Hermes 25 | :production => ENV["PRODUCTION"] == "1" ? true : false, 26 | # @generated end expo-community-flipper-isprod 27 | :hermes_enabled => flags[:hermes_enabled] || podfile_properties['expo.jsEngine'] == 'hermes', 28 | :fabric_enabled => flags[:fabric_enabled], 29 | # @generated begin expo-community-flipper-urn - expo prebuild (DO NOT MODIFY) sync-2c72a3e830aecfd65d973c105092ad560f1397e6 30 | # Flipper arguments generated from app.json 31 | :flipper_configuration => ENV['FLIPPER_DISABLE'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled, 32 | # @generated end expo-community-flipper-urn 33 | # An absolute path to your application root. 34 | :app_path => "#{Dir.pwd}/.." 35 | ) 36 | 37 | # Uncomment to opt-in to using Flipper 38 | # Note that if you have use_frameworks! enabled, Flipper will not work 39 | # 40 | # if !ENV['CI'] 41 | # use_flipper!() 42 | # end 43 | 44 | post_install do |installer| 45 | react_native_post_install(installer) 46 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 47 | 48 | # This is necessary for Xcode 14, because it signs resource bundles by default 49 | # when building for devices. 50 | installer.target_installation_results.pod_target_installation_results 51 | .each do |pod_name, target_installation_result| 52 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target| 53 | resource_bundle_target.build_configurations.each do |config| 54 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 55 | end 56 | end 57 | end 58 | end 59 | 60 | post_integrate do |installer| 61 | begin 62 | expo_patch_react_imports!(installer) 63 | rescue => e 64 | Pod::UI.warn e 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | import org.apache.tools.ant.taskdefs.condition.Os 2 | 3 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 4 | buildscript { 5 | ext { 6 | buildToolsVersion = findProperty('android.buildToolsVersion') ?: '31.0.0' 7 | minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21') 8 | compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '31') 9 | targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '31') 10 | if (findProperty('android.kotlinVersion')) { 11 | kotlinVersion = findProperty('android.kotlinVersion') 12 | } 13 | frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0' 14 | 15 | if (System.properties['os.arch'] == 'aarch64') { 16 | // For M1 Users we need to use the NDK 24 which added support for aarch64 17 | ndkVersion = '24.0.8215888' 18 | } else { 19 | // Otherwise we default to the side-by-side NDK version from AGP. 20 | ndkVersion = '21.4.7075529' 21 | } 22 | } 23 | repositories { 24 | google() 25 | mavenCentral() 26 | } 27 | dependencies { 28 | classpath('com.android.tools.build:gradle:7.1.1') 29 | classpath('com.facebook.react:react-native-gradle-plugin') 30 | classpath('de.undercouch:gradle-download-task:5.0.1') 31 | // NOTE: Do not place your application dependencies here; they belong 32 | // in the individual module build.gradle files 33 | } 34 | } 35 | 36 | def REACT_NATIVE_VERSION = new File(['node', '--print',"JSON.parse(require('fs').readFileSync(require.resolve('react-native/package.json'), 'utf-8')).version"].execute(null, rootDir).text.trim()) 37 | 38 | allprojects { 39 | configurations.all { 40 | resolutionStrategy { 41 | force "com.facebook.react:react-native:" + REACT_NATIVE_VERSION 42 | } 43 | } 44 | 45 | repositories { 46 | mavenLocal() 47 | maven { 48 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 49 | url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android')) 50 | } 51 | maven { 52 | // Android JSC is installed from npm 53 | url(new File(['node', '--print', "require.resolve('jsc-android/package.json')"].execute(null, rootDir).text.trim(), '../dist')) 54 | } 55 | 56 | google() 57 | mavenCentral { 58 | // We don't want to fetch react-native from Maven Central as there are 59 | // older versions over there. 60 | content { 61 | excludeGroup 'com.facebook.react' 62 | } 63 | } 64 | maven { url 'https://www.jitpack.io' } 65 | } 66 | } 67 | 68 | configurations.all { 69 | resolutionStrategy { 70 | force 'com.facebook.react:react-native:0.69.6' 71 | } 72 | } -------------------------------------------------------------------------------- /ios/mobile/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | mobile 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleURLSchemes 27 | 28 | com.resonatecoop.mobile 29 | 30 | 31 | 32 | CFBundleURLSchemes 33 | 34 | exp+mobile 35 | 36 | 37 | 38 | CFBundleVersion 39 | 1 40 | LSRequiresIPhoneOS 41 | 42 | NSAppTransportSecurity 43 | 44 | NSAllowsArbitraryLoads 45 | 46 | NSExceptionDomains 47 | 48 | localhost 49 | 50 | NSExceptionAllowsInsecureHTTPLoads 51 | 52 | 53 | 54 | 55 | NSMicrophoneUsageDescription 56 | Allow $(PRODUCT_NAME) to access your microphone 57 | UIBackgroundModes 58 | 59 | audio 60 | 61 | UILaunchStoryboardName 62 | SplashScreen 63 | UIRequiredDeviceCapabilities 64 | 65 | armv7 66 | 67 | UIRequiresFullScreen 68 | 69 | UIStatusBarStyle 70 | UIStatusBarStyleDefault 71 | UISupportedInterfaceOrientations 72 | 73 | UIInterfaceOrientationPortrait 74 | UIInterfaceOrientationPortraitUpsideDown 75 | 76 | UISupportedInterfaceOrientations~ipad 77 | 78 | UIInterfaceOrientationPortrait 79 | UIInterfaceOrientationPortraitUpsideDown 80 | UIInterfaceOrientationLandscapeLeft 81 | UIInterfaceOrientationLandscapeRight 82 | 83 | UIUserInterfaceStyle 84 | Automatic 85 | UIViewControllerBasedStatusBarAppearance 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/mobile/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "iphone", 5 | "size": "20x20", 6 | "scale": "2x", 7 | "filename": "App-Icon-20x20@2x.png" 8 | }, 9 | { 10 | "idiom": "iphone", 11 | "size": "20x20", 12 | "scale": "3x", 13 | "filename": "App-Icon-20x20@3x.png" 14 | }, 15 | { 16 | "idiom": "iphone", 17 | "size": "29x29", 18 | "scale": "1x", 19 | "filename": "App-Icon-29x29@1x.png" 20 | }, 21 | { 22 | "idiom": "iphone", 23 | "size": "29x29", 24 | "scale": "2x", 25 | "filename": "App-Icon-29x29@2x.png" 26 | }, 27 | { 28 | "idiom": "iphone", 29 | "size": "29x29", 30 | "scale": "3x", 31 | "filename": "App-Icon-29x29@3x.png" 32 | }, 33 | { 34 | "idiom": "iphone", 35 | "size": "40x40", 36 | "scale": "2x", 37 | "filename": "App-Icon-40x40@2x.png" 38 | }, 39 | { 40 | "idiom": "iphone", 41 | "size": "40x40", 42 | "scale": "3x", 43 | "filename": "App-Icon-40x40@3x.png" 44 | }, 45 | { 46 | "idiom": "iphone", 47 | "size": "60x60", 48 | "scale": "2x", 49 | "filename": "App-Icon-60x60@2x.png" 50 | }, 51 | { 52 | "idiom": "iphone", 53 | "size": "60x60", 54 | "scale": "3x", 55 | "filename": "App-Icon-60x60@3x.png" 56 | }, 57 | { 58 | "idiom": "ipad", 59 | "size": "20x20", 60 | "scale": "1x", 61 | "filename": "App-Icon-20x20@1x.png" 62 | }, 63 | { 64 | "idiom": "ipad", 65 | "size": "20x20", 66 | "scale": "2x", 67 | "filename": "App-Icon-20x20@2x.png" 68 | }, 69 | { 70 | "idiom": "ipad", 71 | "size": "29x29", 72 | "scale": "1x", 73 | "filename": "App-Icon-29x29@1x.png" 74 | }, 75 | { 76 | "idiom": "ipad", 77 | "size": "29x29", 78 | "scale": "2x", 79 | "filename": "App-Icon-29x29@2x.png" 80 | }, 81 | { 82 | "idiom": "ipad", 83 | "size": "40x40", 84 | "scale": "1x", 85 | "filename": "App-Icon-40x40@1x.png" 86 | }, 87 | { 88 | "idiom": "ipad", 89 | "size": "40x40", 90 | "scale": "2x", 91 | "filename": "App-Icon-40x40@2x.png" 92 | }, 93 | { 94 | "idiom": "ipad", 95 | "size": "76x76", 96 | "scale": "1x", 97 | "filename": "App-Icon-76x76@1x.png" 98 | }, 99 | { 100 | "idiom": "ipad", 101 | "size": "76x76", 102 | "scale": "2x", 103 | "filename": "App-Icon-76x76@2x.png" 104 | }, 105 | { 106 | "idiom": "ipad", 107 | "size": "83.5x83.5", 108 | "scale": "2x", 109 | "filename": "App-Icon-83.5x83.5@2x.png" 110 | }, 111 | { 112 | "idiom": "ios-marketing", 113 | "size": "1024x1024", 114 | "scale": "1x", 115 | "filename": "ItunesArtwork@2x.png" 116 | } 117 | ], 118 | "info": { 119 | "version": 1, 120 | "author": "expo" 121 | } 122 | } -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/resonatecoop/mobile/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.resonatecoop.mobile; 2 | 3 | import android.os.Build; 4 | import android.os.Bundle; 5 | 6 | import com.facebook.react.ReactActivity; 7 | import com.facebook.react.ReactActivityDelegate; 8 | import com.facebook.react.ReactRootView; 9 | 10 | import expo.modules.ReactActivityDelegateWrapper; 11 | 12 | public class MainActivity extends ReactActivity { 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | // Set the theme to AppTheme BEFORE onCreate to support 16 | // coloring the background, status bar, and navigation bar. 17 | // This is required for expo-splash-screen. 18 | setTheme(R.style.AppTheme); 19 | super.onCreate(null); 20 | } 21 | 22 | /** 23 | * Returns the name of the main component registered from JavaScript. 24 | * This is used to schedule rendering of the component. 25 | */ 26 | @Override 27 | protected String getMainComponentName() { 28 | return "main"; 29 | } 30 | 31 | /** 32 | * Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and 33 | * you can specify the renderer you wish to use - the new renderer (Fabric) or the old renderer 34 | * (Paper). 35 | */ 36 | @Override 37 | protected ReactActivityDelegate createReactActivityDelegate() { 38 | return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, 39 | new MainActivityDelegate(this, getMainComponentName()) 40 | ); 41 | } 42 | 43 | /** 44 | * Align the back button behavior with Android S 45 | * where moving root activities to background instead of finishing activities. 46 | * @see onBackPressed 47 | */ 48 | @Override 49 | public void invokeDefaultOnBackPressed() { 50 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { 51 | if (!moveTaskToBack(false)) { 52 | // For non-root activities, use the default implementation to finish them. 53 | super.invokeDefaultOnBackPressed(); 54 | } 55 | return; 56 | } 57 | 58 | // Use the default back button implementation on Android S 59 | // because it's doing more than {@link Activity#moveTaskToBack} in fact. 60 | super.invokeDefaultOnBackPressed(); 61 | } 62 | 63 | public static class MainActivityDelegate extends ReactActivityDelegate { 64 | public MainActivityDelegate(ReactActivity activity, String mainComponentName) { 65 | super(activity, mainComponentName); 66 | } 67 | 68 | @Override 69 | protected ReactRootView createRootView() { 70 | ReactRootView reactRootView = new ReactRootView(getContext()); 71 | // If you opted-in for the New Architecture, we enable the Fabric Renderer. 72 | reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED); 73 | return reactRootView; 74 | } 75 | 76 | @Override 77 | protected boolean isConcurrentRootEnabled() { 78 | // If you opted-in for the New Architecture, we enable Concurrent Root (i.e. React 18). 79 | // More on this on https://reactjs.org/blog/2022/03/29/react-v18.html 80 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /android/app/src/debug/java/com/resonatecoop/mobile/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.resonatecoop.mobile; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.react.ReactFlipperPlugin; 21 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 22 | import com.facebook.react.ReactInstanceManager; 23 | import com.facebook.react.bridge.ReactContext; 24 | import com.facebook.react.modules.network.NetworkingModule; 25 | import okhttp3.OkHttpClient; 26 | 27 | public class ReactNativeFlipper { 28 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 29 | if (FlipperUtils.shouldEnableFlipper(context)) { 30 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 31 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 32 | client.addPlugin(new ReactFlipperPlugin()); 33 | client.addPlugin(new DatabasesFlipperPlugin(context)); 34 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 35 | client.addPlugin(CrashReporterPlugin.getInstance()); 36 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 37 | NetworkingModule.setCustomClientBuilder( 38 | new NetworkingModule.CustomClientBuilder() { 39 | @Override 40 | public void apply(OkHttpClient.Builder builder) { 41 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 42 | } 43 | }); 44 | client.addPlugin(networkFlipperPlugin); 45 | client.start(); 46 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 47 | // Hence we run if after all native modules have been initialized 48 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 49 | if (reactContext == null) { 50 | reactInstanceManager.addReactInstanceEventListener( 51 | new ReactInstanceManager.ReactInstanceEventListener() { 52 | @Override 53 | public void onReactContextInitialized(ReactContext reactContext) { 54 | reactInstanceManager.removeReactInstanceEventListener(this); 55 | reactContext.runOnNativeModulesQueueThread( 56 | new Runnable() { 57 | @Override 58 | public void run() { 59 | client.addPlugin(new FrescoFlipperPlugin()); 60 | } 61 | }); 62 | } 63 | }); 64 | } else { 65 | client.addPlugin(new FrescoFlipperPlugin()); 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /ios/mobile.xcodeproj/xcshareddata/xcschemes/mobile.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/resonatecoop/mobile/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.resonatecoop.mobile; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import androidx.annotation.NonNull; 7 | 8 | import com.facebook.react.PackageList; 9 | import com.facebook.react.ReactApplication; 10 | import com.facebook.react.ReactInstanceManager; 11 | import com.facebook.react.ReactNativeHost; 12 | import com.facebook.react.ReactPackage; 13 | import com.facebook.react.config.ReactFeatureFlags; 14 | import com.facebook.soloader.SoLoader; 15 | import com.resonatecoop.mobile.newarchitecture.MainApplicationReactNativeHost; 16 | 17 | import expo.modules.ApplicationLifecycleDispatcher; 18 | import expo.modules.ReactNativeHostWrapper; 19 | 20 | import java.lang.reflect.InvocationTargetException; 21 | import java.util.List; 22 | 23 | public class MainApplication extends Application implements ReactApplication { 24 | private final ReactNativeHost mReactNativeHost = new ReactNativeHostWrapper( 25 | this, 26 | new ReactNativeHost(this) { 27 | @Override 28 | public boolean getUseDeveloperSupport() { 29 | return BuildConfig.DEBUG; 30 | } 31 | 32 | @Override 33 | protected List getPackages() { 34 | @SuppressWarnings("UnnecessaryLocalVariable") 35 | List packages = new PackageList(this).getPackages(); 36 | // Packages that cannot be autolinked yet can be added manually here, for example: 37 | // packages.add(new MyReactNativePackage()); 38 | return packages; 39 | } 40 | 41 | @Override 42 | protected String getJSMainModuleName() { 43 | return "index"; 44 | } 45 | }); 46 | 47 | private final ReactNativeHost mNewArchitectureNativeHost = 48 | new ReactNativeHostWrapper(this, new MainApplicationReactNativeHost(this)); 49 | 50 | @Override 51 | public ReactNativeHost getReactNativeHost() { 52 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 53 | return mNewArchitectureNativeHost; 54 | } else { 55 | return mReactNativeHost; 56 | } 57 | } 58 | 59 | @Override 60 | public void onCreate() { 61 | super.onCreate(); 62 | // If you opted-in for the New Architecture, we enable the TurboModule system 63 | ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 64 | SoLoader.init(this, /* native exopackage */ false); 65 | 66 | initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 67 | ApplicationLifecycleDispatcher.onApplicationCreate(this); 68 | } 69 | 70 | @Override 71 | public void onConfigurationChanged(@NonNull Configuration newConfig) { 72 | super.onConfigurationChanged(newConfig); 73 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig); 74 | } 75 | 76 | /** 77 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like 78 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 79 | * 80 | * @param context 81 | * @param reactInstanceManager 82 | */ 83 | private static void initializeFlipper( 84 | Context context, ReactInstanceManager reactInstanceManager) { 85 | if (BuildConfig.DEBUG) { 86 | try { 87 | /* 88 | We use reflection here to pick up the class that initializes Flipper, 89 | since Flipper library is not available in release mode 90 | */ 91 | Class aClass = Class.forName("com.resonatecoop.mobile.ReactNativeFlipper"); 92 | aClass 93 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class) 94 | .invoke(null, context, reactInstanceManager); 95 | } catch (ClassNotFoundException e) { 96 | e.printStackTrace(); 97 | } catch (NoSuchMethodException e) { 98 | e.printStackTrace(); 99 | } catch (IllegalAccessException e) { 100 | e.printStackTrace(); 101 | } catch (InvocationTargetException e) { 102 | e.printStackTrace(); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /client/api/fetch-client.ts: -------------------------------------------------------------------------------- 1 | import { FetchError } from "./error"; 2 | import fetchWithTimeout, { FetchOptions } from "./fetch-with-timeout"; 3 | 4 | interface ServiceClientOptions { 5 | baseURL?: string; 6 | headers?: Record; 7 | timeout?: number; 8 | } 9 | 10 | export class FetchClient { 11 | options: ServiceClientOptions; 12 | constructor(options: ServiceClientOptions) { 13 | this.options = options; 14 | } 15 | 16 | mergeOptions(options?: ServiceClientOptions): ServiceClientOptions { 17 | return { 18 | ...this.options, 19 | ...options, 20 | headers: this.mergeHeaders(options?.headers), 21 | }; 22 | } 23 | 24 | mergeHeaders(newHeaders: Record = {}) { 25 | const headers = { ...this.options.headers }; 26 | for (const key of Object.keys(newHeaders)) { 27 | const value = newHeaders[key]; 28 | if (value === undefined) { 29 | delete headers[key]; 30 | } else { 31 | headers[key] = value; 32 | } 33 | } 34 | return headers; 35 | } 36 | 37 | setOptions(options?: ServiceClientOptions) { 38 | this.options = this.mergeOptions(options); 39 | } 40 | 41 | extend(options: ServiceClientOptions) { 42 | return new FetchClient(this.mergeOptions(options)); 43 | } 44 | 45 | getURL(endpoint: string, prefix: string) { 46 | const result = new URL(endpoint, prefix); 47 | return result.toString(); 48 | } 49 | 50 | request( 51 | _method: string, 52 | endpoint: string, 53 | body?: BodyInit, 54 | options?: ServiceClientOptions 55 | ) { 56 | const { baseURL, headers, timeout = 0 } = this.mergeOptions(options); 57 | const url = this.getURL(endpoint, baseURL || ""); 58 | const method = _method.toUpperCase(); 59 | const opts: FetchOptions = { 60 | method, 61 | headers, 62 | timeout, 63 | }; 64 | 65 | // fetch will throw if given body for these 66 | if (method !== "GET" && method !== "HEAD") { 67 | opts.body = body; 68 | } 69 | 70 | return fetchWithTimeout(url, opts); 71 | } 72 | 73 | json( 74 | method: string, 75 | endpoint: string, 76 | payload: U | undefined, 77 | options: Partial = { headers: {} } 78 | ) { 79 | if (!options.headers) { 80 | options.headers = {}; 81 | } 82 | 83 | options.headers["Content-Type"] = "application/json"; 84 | options.headers["Accept"] = "application/json"; 85 | 86 | return this.request( 87 | method, 88 | endpoint, 89 | JSON.stringify(payload), 90 | options 91 | ).then((res) => (res.status === 204 ? (null as T) : parseJSON(res))); 92 | } 93 | 94 | put( 95 | endpoint: string, 96 | payload?: U, 97 | options?: ServiceClientOptions 98 | ) { 99 | return this.json("PUT", endpoint, payload, options); 100 | } 101 | 102 | post( 103 | endpoint: string, 104 | payload?: U, 105 | options?: ServiceClientOptions 106 | ) { 107 | return this.json("POST", endpoint, payload, options); 108 | } 109 | 110 | patch( 111 | endpoint: string, 112 | payload?: U, 113 | options?: ServiceClientOptions 114 | ) { 115 | return this.json("PATCH", endpoint, payload, options); 116 | } 117 | 118 | get>( 119 | endpoint: string, 120 | params?: U, 121 | options?: ServiceClientOptions 122 | ) { 123 | if (params) { 124 | endpoint += "?" + new URLSearchParams(params).toString(); 125 | } 126 | return this.json("GET", endpoint, {}, options); 127 | } 128 | 129 | submit(endpoint: string, body: BodyInit, options?: ServiceClientOptions) { 130 | return this.request("POST", endpoint, body, { 131 | ...options, 132 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 133 | }).then(parseJSON); 134 | } 135 | } 136 | 137 | function parseJSON(res: Response) { 138 | return res.json().catch(() => { 139 | return Promise.reject( 140 | new FetchError("Failed to parse JSON payload", res.url, res) 141 | ); 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 🛠 **Status: Active Development | Experimental** 2 | > 3 | > This project is currently broken and under very active development. 4 | 5 | ## 🎵 Resonate Mobile 6 | 7 | A mobile app for playing music on [Resonate](https://stream.resonate.coop/), an open source music streaming co-op. 8 | 9 | This is a newer iteration, built completely in React Native and TypeScript, which aims to expand upon [`stream-app`](https://github.com/peterklingelhofer/stream-app), the initial [`react-native-webview`](https://github.com/react-native-webview/react-native-webview) implementation. Note that the [`stream-app`](https://github.com/peterklingelhofer/stream-app) is still being used on Google Play Store and Apple Stores, so please submit any issues for problems encountered [there](https://github.com/resonatecoop/stream-app/issues). 10 | 11 | 12 | ## 🎶 Description 13 | 14 | Resonate is an open-source music streaming service run by a cooperative of artists and software developers. 15 | 16 | If you want to know what we're building or want to get more involved, head over to the Platform category on our [forum](https://community.resonate.is/c/platform/l/latest?board=default) or read the [Developer Guide](https://community.resonate.is/docs?topic=2262) in our [Resonate Handbook](https://community.resonate.is/docs). 17 | 18 | View the [project board](https://github.com/resonatecoop/mobile/projects/1) where work is tracked for this repository. If you're looking for a good first task, feel encouraged to take on an un-assigned [`help wanted` or `good first task` issues](https://github.com/resonatecoop/mobile/issues). 19 | 20 | Are you building something using the Resonate [API](#api) and would like to request a change? Resonate welcomes #proposals in the [Co-Operation section of the forum](https://community.resonate.is/c/66). 21 | 22 | 23 | ## 🗂 Tech Stack 24 | 25 | - React Native 26 | - TypeScript 27 | 28 | 29 | ## 🔧 Installation & Start 30 | 31 | Clone this repository, install dependencies, and start expo. 32 | 33 | ```sh 34 | git clone https://github.com/resonatecoop/mobile.git 35 | cd mobile 36 | yarn 37 | expo start 38 | ``` 39 | 40 | You can also use device-specific commands to run Expo on your preferred device: 41 | ```sh 42 | yarn run android 43 | yarn run ios 44 | yarn run web 45 | ``` 46 | 47 | ## 🧪 Testing 48 | Expect your code contributions to be tested: we use [commitlint](https://commitlint.js.org) to lint commit messages, and [prettier](https://prettier.io) to lint code. 49 | 50 | To test the `dark` theme on Android Studio, use the following command. Please try to ensure your changes work with both `light` and `dark` themes: 51 | 52 | ```sh 53 | adb shell "cmd uimode night yes" 54 | ``` 55 | 56 | ## 📚 Contributing 57 | 58 | Contributing to others’ projects is an avenue to learn new software development skills and experience new technologies. The pull request is how your personal contributions will be added to the project. The following is an overview of the Git project management workflow: 59 | 60 | Search project for contribution instructions and follow them if present. 61 | Fork project repo from your personal Github account. 62 | Copy the fork and clone repo onto your local machine. 63 | Add the original repository (the you forked) as a remote called upstream. 64 | If you created your fork a while ago be sure to pull upstream changes into your local repository. 65 | Create a new branch to work on! Branch from develop if it exists, else from master. 66 | Implement/fix your feature. 67 | Follow the code style of the project, including indentation. 68 | If the project has included tests use them. 69 | Add additional tests or convert existing tests as necessary. 70 | Add or convert project documentation as needed. 71 | Push your working branch to your forked repo on Github. 72 | Make a pull request from your forked repo to the origin master or development branch if present. 73 | Once your pull request is merged, pull down upstream master to your local repo and delete any additional branch(es) you may have created. 74 | Commit messages should be written in present tense describing what the committed code does and not what you changed in the code. 75 | 76 | 77 | ## 📖 References 78 | 79 | - [React Native](https://reactnative.dev/) 80 | - [TypeScript](https://typescriptlang.org/') 81 | 82 | 83 | ## 📑 License 84 | 85 | `mobile` is licensed under the 86 | [GNU General Public License v3.0](https://github.com/peterklingelhofer/stream-app/blob/master/LICENSE) 87 | 88 | Permissions of this strong copyleft license are conditioned on making available complete source code of licensed works and modifications, which include larger works using a licensed work, under the same license. Copyright and license notices must be preserved. Contributors provide an express grant of patent rights. 89 | -------------------------------------------------------------------------------- /ios/mobile/SplashScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/resonatecoop/mobile/newarchitecture/MainApplicationReactNativeHost.java: -------------------------------------------------------------------------------- 1 | package com.resonatecoop.mobile.newarchitecture; 2 | 3 | import android.app.Application; 4 | import androidx.annotation.NonNull; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactInstanceManager; 7 | import com.facebook.react.ReactNativeHost; 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.react.ReactPackageTurboModuleManagerDelegate; 10 | import com.facebook.react.bridge.JSIModulePackage; 11 | import com.facebook.react.bridge.JSIModuleProvider; 12 | import com.facebook.react.bridge.JSIModuleSpec; 13 | import com.facebook.react.bridge.JSIModuleType; 14 | import com.facebook.react.bridge.JavaScriptContextHolder; 15 | import com.facebook.react.bridge.ReactApplicationContext; 16 | import com.facebook.react.bridge.UIManager; 17 | import com.facebook.react.fabric.ComponentFactory; 18 | import com.facebook.react.fabric.CoreComponentsRegistry; 19 | import com.facebook.react.fabric.EmptyReactNativeConfig; 20 | import com.facebook.react.fabric.FabricJSIModuleProvider; 21 | import com.facebook.react.fabric.ReactNativeConfig; 22 | import com.facebook.react.uimanager.ViewManagerRegistry; 23 | import com.resonatecoop.mobile.BuildConfig; 24 | import com.resonatecoop.mobile.newarchitecture.components.MainComponentsRegistry; 25 | import com.resonatecoop.mobile.newarchitecture.modules.MainApplicationTurboModuleManagerDelegate; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | /** 30 | * A {@link ReactNativeHost} that helps you load everything needed for the New Architecture, both 31 | * TurboModule delegates and the Fabric Renderer. 32 | * 33 | *

Please note that this class is used ONLY if you opt-in for the New Architecture (see the 34 | * `newArchEnabled` property). Is ignored otherwise. 35 | */ 36 | public class MainApplicationReactNativeHost extends ReactNativeHost { 37 | public MainApplicationReactNativeHost(Application application) { 38 | super(application); 39 | } 40 | 41 | @Override 42 | public boolean getUseDeveloperSupport() { 43 | return BuildConfig.DEBUG; 44 | } 45 | 46 | @Override 47 | protected List getPackages() { 48 | List packages = new PackageList(this).getPackages(); 49 | // Packages that cannot be autolinked yet can be added manually here, for example: 50 | // packages.add(new MyReactNativePackage()); 51 | // TurboModules must also be loaded here providing a valid TurboReactPackage implementation: 52 | // packages.add(new TurboReactPackage() { ... }); 53 | // If you have custom Fabric Components, their ViewManagers should also be loaded here 54 | // inside a ReactPackage. 55 | return packages; 56 | } 57 | 58 | @Override 59 | protected String getJSMainModuleName() { 60 | return "index"; 61 | } 62 | 63 | @NonNull 64 | @Override 65 | protected ReactPackageTurboModuleManagerDelegate.Builder 66 | getReactPackageTurboModuleManagerDelegateBuilder() { 67 | // Here we provide the ReactPackageTurboModuleManagerDelegate Builder. This is necessary 68 | // for the new architecture and to use TurboModules correctly. 69 | return new MainApplicationTurboModuleManagerDelegate.Builder(); 70 | } 71 | 72 | @Override 73 | protected JSIModulePackage getJSIModulePackage() { 74 | return new JSIModulePackage() { 75 | @Override 76 | public List getJSIModules( 77 | final ReactApplicationContext reactApplicationContext, 78 | final JavaScriptContextHolder jsContext) { 79 | final List specs = new ArrayList<>(); 80 | 81 | // Here we provide a new JSIModuleSpec that will be responsible of providing the 82 | // custom Fabric Components. 83 | specs.add( 84 | new JSIModuleSpec() { 85 | @Override 86 | public JSIModuleType getJSIModuleType() { 87 | return JSIModuleType.UIManager; 88 | } 89 | 90 | @Override 91 | public JSIModuleProvider getJSIModuleProvider() { 92 | final ComponentFactory componentFactory = new ComponentFactory(); 93 | CoreComponentsRegistry.register(componentFactory); 94 | 95 | // Here we register a Components Registry. 96 | // The one that is generated with the template contains no components 97 | // and just provides you the one from React Native core. 98 | MainComponentsRegistry.register(componentFactory); 99 | 100 | final ReactInstanceManager reactInstanceManager = getReactInstanceManager(); 101 | 102 | ViewManagerRegistry viewManagerRegistry = 103 | new ViewManagerRegistry( 104 | reactInstanceManager.getOrCreateViewManagers(reactApplicationContext)); 105 | 106 | return new FabricJSIModuleProvider( 107 | reactApplicationContext, 108 | componentFactory, 109 | ReactNativeConfig.DEFAULT_CONFIG, 110 | viewManagerRegistry); 111 | } 112 | }); 113 | return specs; 114 | } 115 | }; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | r7477cdv4@relay.firefox.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /client/theme/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DarkTheme as NavigationDarkTheme, 3 | DefaultTheme as NavigationLightTheme, 4 | NavigationContainer, 5 | } from "@react-navigation/native"; 6 | import merge from "lodash/merge"; 7 | import { 8 | createContext, 9 | PropsWithChildren, 10 | useContext, 11 | useEffect, 12 | useMemo, 13 | useState, 14 | } from "react"; 15 | import { Appearance } from "react-native"; 16 | import { 17 | Provider as PaperProvider, 18 | adaptNavigationTheme, 19 | MD3DarkTheme, 20 | MD3LightTheme, 21 | } from "react-native-paper"; 22 | 23 | export type ThemeMode = "light" | "dark"; 24 | 25 | const { LightTheme, DarkTheme } = adaptNavigationTheme({ 26 | light: NavigationLightTheme, 27 | dark: NavigationDarkTheme, 28 | }); 29 | 30 | const lightTheme = merge({}, LightTheme, MD3LightTheme, { 31 | colors: { 32 | primary: "rgb(0, 101, 140)", 33 | onPrimary: "rgb(255, 255, 255)", 34 | primaryContainer: "rgb(197, 231, 255)", 35 | onPrimaryContainer: "rgb(0, 30, 45)", 36 | secondary: "rgb(78, 97, 109)", 37 | onSecondary: "rgb(255, 255, 255)", 38 | secondaryContainer: "rgb(210, 229, 244)", 39 | onSecondaryContainer: "rgb(10, 30, 40)", 40 | tertiary: "rgb(97, 89, 124)", 41 | onTertiary: "rgb(255, 255, 255)", 42 | tertiaryContainer: "rgb(231, 222, 255)", 43 | onTertiaryContainer: "rgb(29, 23, 53)", 44 | error: "rgb(186, 26, 26)", 45 | onError: "rgb(255, 255, 255)", 46 | errorContainer: "rgb(255, 218, 214)", 47 | onErrorContainer: "rgb(65, 0, 2)", 48 | background: "rgb(251, 252, 255)", 49 | onBackground: "rgb(25, 28, 30)", 50 | surface: "rgb(251, 252, 255)", 51 | onSurface: "rgb(25, 28, 30)", 52 | surfaceVariant: "rgb(221, 227, 234)", 53 | onSurfaceVariant: "rgb(65, 72, 77)", 54 | outline: "rgb(113, 120, 126)", 55 | outlineVariant: "rgb(193, 199, 206)", 56 | shadow: "rgb(0, 0, 0)", 57 | scrim: "rgb(0, 0, 0)", 58 | inverseSurface: "rgb(46, 49, 51)", 59 | inverseOnSurface: "rgb(240, 241, 243)", 60 | inversePrimary: "rgb(127, 208, 255)", 61 | elevation: { 62 | level0: "transparent", 63 | level1: "rgb(238, 244, 249)", 64 | level2: "rgb(231, 240, 246)", 65 | level3: "rgb(223, 235, 242)", 66 | level4: "rgb(221, 234, 241)", 67 | level5: "rgb(216, 231, 239)", 68 | }, 69 | surfaceDisabled: "rgba(25, 28, 30, 0.12)", 70 | onSurfaceDisabled: "rgba(25, 28, 30, 0.38)", 71 | backdrop: "rgba(42, 49, 54, 0.4)", 72 | }, 73 | }); 74 | const darkTheme = merge({}, DarkTheme, MD3DarkTheme, { 75 | colors: { 76 | colors: { 77 | primary: "rgb(127, 208, 255)", 78 | onPrimary: "rgb(0, 52, 74)", 79 | primaryContainer: "rgb(0, 76, 106)", 80 | onPrimaryContainer: "rgb(197, 231, 255)", 81 | secondary: "rgb(182, 201, 216)", 82 | onSecondary: "rgb(32, 51, 62)", 83 | secondaryContainer: "rgb(55, 73, 85)", 84 | onSecondaryContainer: "rgb(210, 229, 244)", 85 | tertiary: "rgb(203, 193, 233)", 86 | onTertiary: "rgb(51, 44, 76)", 87 | tertiaryContainer: "rgb(73, 66, 99)", 88 | onTertiaryContainer: "rgb(231, 222, 255)", 89 | error: "rgb(255, 180, 171)", 90 | onError: "rgb(105, 0, 5)", 91 | errorContainer: "rgb(147, 0, 10)", 92 | onErrorContainer: "rgb(255, 180, 171)", 93 | background: "rgb(25, 28, 30)", 94 | onBackground: "rgb(225, 226, 229)", 95 | surface: "rgb(25, 28, 30)", 96 | onSurface: "rgb(225, 226, 229)", 97 | surfaceVariant: "rgb(65, 72, 77)", 98 | onSurfaceVariant: "rgb(193, 199, 206)", 99 | outline: "rgb(139, 146, 151)", 100 | outlineVariant: "rgb(65, 72, 77)", 101 | shadow: "rgb(0, 0, 0)", 102 | scrim: "rgb(0, 0, 0)", 103 | inverseSurface: "rgb(225, 226, 229)", 104 | inverseOnSurface: "rgb(46, 49, 51)", 105 | inversePrimary: "rgb(0, 101, 140)", 106 | elevation: { 107 | level0: "transparent", 108 | level1: "rgb(30, 37, 41)", 109 | level2: "rgb(33, 42, 48)", 110 | level3: "rgb(36, 48, 55)", 111 | level4: "rgb(37, 50, 57)", 112 | level5: "rgb(39, 53, 62)", 113 | }, 114 | surfaceDisabled: "rgba(225, 226, 229, 0.12)", 115 | onSurfaceDisabled: "rgba(225, 226, 229, 0.38)", 116 | backdrop: "rgba(42, 49, 54, 0.4)", 117 | }, 118 | }, 119 | }); 120 | 121 | export const themes = { 122 | light: lightTheme, 123 | dark: darkTheme, 124 | }; 125 | 126 | const ThemeModeContext = createContext<{ 127 | setMode: (mode: ThemeMode) => void; 128 | mode: ThemeMode; 129 | }>({ 130 | setMode: (mode: ThemeMode) => {}, 131 | mode: "dark", 132 | }); 133 | 134 | export function ThemeModeProvider({ children }: PropsWithChildren) { 135 | const [mode, setMode] = useState( 136 | () => Appearance.getColorScheme() ?? "dark" 137 | ); 138 | const settings = useMemo(() => ({ mode, setMode }), [mode]); 139 | 140 | useEffect(() => { 141 | const sub = Appearance.addChangeListener((appearance) => { 142 | if (appearance.colorScheme) { 143 | setMode(appearance.colorScheme); 144 | } 145 | }); 146 | 147 | return () => { 148 | sub.remove(); 149 | }; 150 | }, []); 151 | 152 | return ( 153 | 154 | {children} 155 | 156 | ); 157 | } 158 | 159 | export function useThemeMode() { 160 | return useContext(ThemeModeContext); 161 | } 162 | 163 | export function PaperNavigationProvider({ 164 | children, 165 | }: PropsWithChildren) { 166 | const { mode } = useThemeMode(); 167 | const theme = themes[mode]; 168 | 169 | return ( 170 | 171 | {children} 172 | 173 | ); 174 | } 175 | -------------------------------------------------------------------------------- /ios/mobile/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | #import 7 | #import 8 | 9 | #import 10 | 11 | #if RCT_NEW_ARCH_ENABLED 12 | #import 13 | #import 14 | #import 15 | #import 16 | #import 17 | #import 18 | 19 | #import 20 | 21 | static NSString *const kRNConcurrentRoot = @"concurrentRoot"; 22 | 23 | @interface AppDelegate () { 24 | RCTTurboModuleManager *_turboModuleManager; 25 | RCTSurfacePresenterBridgeAdapter *_bridgeAdapter; 26 | std::shared_ptr _reactNativeConfig; 27 | facebook::react::ContextContainer::Shared _contextContainer; 28 | } 29 | @end 30 | #endif 31 | 32 | @implementation AppDelegate 33 | 34 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 35 | { 36 | RCTAppSetupPrepareApp(application); 37 | 38 | RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions]; 39 | 40 | #if RCT_NEW_ARCH_ENABLED 41 | _contextContainer = std::make_shared(); 42 | _reactNativeConfig = std::make_shared(); 43 | _contextContainer->insert("ReactNativeConfig", _reactNativeConfig); 44 | _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer]; 45 | bridge.surfacePresenter = _bridgeAdapter.surfacePresenter; 46 | #endif 47 | 48 | NSDictionary *initProps = [self prepareInitialProps]; 49 | UIView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:initProps]; 50 | 51 | rootView.backgroundColor = [UIColor whiteColor]; 52 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 53 | UIViewController *rootViewController = [self.reactDelegate createRootViewController]; 54 | rootViewController.view = rootView; 55 | self.window.rootViewController = rootViewController; 56 | [self.window makeKeyAndVisible]; 57 | 58 | [super application:application didFinishLaunchingWithOptions:launchOptions]; 59 | 60 | return YES; 61 | } 62 | 63 | - (NSArray> *)extraModulesForBridge:(RCTBridge *)bridge 64 | { 65 | // If you'd like to export some custom RCTBridgeModules, add them here! 66 | return @[]; 67 | } 68 | 69 | /// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. 70 | /// 71 | /// @see: https://reactjs.org/blog/2022/03/29/react-v18.html 72 | /// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). 73 | /// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`. 74 | - (BOOL)concurrentRootEnabled 75 | { 76 | // Switch this bool to turn on and off the concurrent root 77 | return true; 78 | } 79 | 80 | - (NSDictionary *)prepareInitialProps 81 | { 82 | NSMutableDictionary *initProps = [NSMutableDictionary new]; 83 | #if RCT_NEW_ARCH_ENABLED 84 | initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]); 85 | #endif 86 | return initProps; 87 | } 88 | 89 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 90 | { 91 | #if DEBUG 92 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 93 | #else 94 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 95 | #endif 96 | } 97 | 98 | // Linking API 99 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { 100 | return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; 101 | } 102 | 103 | // Universal Links 104 | - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { 105 | BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 106 | return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; 107 | } 108 | 109 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 110 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken 111 | { 112 | return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 113 | } 114 | 115 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 116 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error 117 | { 118 | return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; 119 | } 120 | 121 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 122 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler 123 | { 124 | return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; 125 | } 126 | 127 | #if RCT_NEW_ARCH_ENABLED 128 | 129 | #pragma mark - RCTCxxBridgeDelegate 130 | 131 | - (std::unique_ptr)jsExecutorFactoryForBridge:(RCTBridge *)bridge 132 | { 133 | _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge 134 | delegate:self 135 | jsInvoker:bridge.jsCallInvoker]; 136 | return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager); 137 | } 138 | 139 | #pragma mark RCTTurboModuleManagerDelegate 140 | 141 | - (Class)getModuleClassFromName:(const char *)name 142 | { 143 | return RCTCoreModulesClassProvider(name); 144 | } 145 | 146 | - (std::shared_ptr)getTurboModule:(const std::string &)name 147 | jsInvoker:(std::shared_ptr)jsInvoker 148 | { 149 | return nullptr; 150 | } 151 | 152 | - (std::shared_ptr)getTurboModule:(const std::string &)name 153 | initParams: 154 | (const facebook::react::ObjCTurboModule::InitParams &)params 155 | { 156 | return nullptr; 157 | } 158 | 159 | - (id)getModuleInstanceFromClass:(Class)moduleClass 160 | { 161 | return RCTAppSetupDefaultModuleFromClass(moduleClass); 162 | } 163 | 164 | #endif 165 | 166 | @end 167 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /client/api/trackgroups.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | const trackgroupKeys = { 4 | all: ["trackgroups"] as const, 5 | featuredPlaylists: ({ limit, page, order }: GetFeaturedPlaylistsParams) => [ 6 | ...trackgroupKeys.all, 7 | "list", 8 | { featured: true, type: "playlist", limit, order, page }, 9 | ], 10 | }; 11 | 12 | export function useFeaturedPlaylists(params: GetFeaturedPlaylistsParams = {}) { 13 | return useQuery(trackgroupKeys.featuredPlaylists(params), () => 14 | getFeaturedPlaylists(params) 15 | ); 16 | } 17 | 18 | function getFeaturedPlaylists({ 19 | limit, 20 | page, 21 | }: GetFeaturedPlaylistsParams = {}): Promise { 22 | return Promise.resolve({ 23 | data: [ 24 | { 25 | about: "", 26 | cover: 27 | "https://static.resonate.is/track-artwork/600x600/a596a32d-aa0a-4d7d-baf0-02a18f92c114", 28 | creator_id: 12788, 29 | display_artist: null, 30 | id: "4d646ccd-7e07-411b-b6f5-25640db46c7c", 31 | slug: "staff-picks-nov20", 32 | tags: [], 33 | title: "Staff Picks #Nov20", 34 | type: "playlist", 35 | cover_metadata: null, 36 | user: { 37 | id: 12788, 38 | nicename: "upload", 39 | }, 40 | uri: "https://stream.resonate.coop/v3/trackgroups/4d646ccd-7e07-411b-b6f5-25640db46c7c", 41 | images: { 42 | small: { 43 | width: 120, 44 | height: 120, 45 | url: "https://static.resonate.is/track-artwork/120x120/a596a32d-aa0a-4d7d-baf0-02a18f92c114", 46 | }, 47 | medium: { 48 | width: 600, 49 | height: 600, 50 | url: "https://static.resonate.is/track-artwork/600x600/a596a32d-aa0a-4d7d-baf0-02a18f92c114", 51 | }, 52 | large: { 53 | width: 1500, 54 | height: 1500, 55 | url: "https://static.resonate.is/track-artwork/1500x1500/a596a32d-aa0a-4d7d-baf0-02a18f92c114", 56 | }, 57 | }, 58 | }, 59 | { 60 | about: null, 61 | cover: 62 | "https://dash.resonate.coop/images/9fdcace0-8646-44bb-b06c-89be6dfe62b8-x600.jpg", 63 | creator_id: 17929, 64 | display_artist: null, 65 | id: "154f1656-681e-4d88-aeb5-c9be126d9e03", 66 | slug: "birdsite", 67 | tags: [], 68 | title: "birdsite", 69 | type: "playlist", 70 | cover_metadata: { 71 | id: "9fdcace0-8646-44bb-b06c-89be6dfe62b8", 72 | owner_id: 12335, 73 | }, 74 | user: { 75 | id: 17929, 76 | nicename: "boopboop", 77 | }, 78 | uri: "https://stream.resonate.coop/v3/trackgroups/154f1656-681e-4d88-aeb5-c9be126d9e03", 79 | images: { 80 | small: { 81 | width: 120, 82 | height: 120, 83 | url: "https://dash.resonate.coop/images/9fdcace0-8646-44bb-b06c-89be6dfe62b8-x120.jpg", 84 | }, 85 | medium: { 86 | width: 600, 87 | height: 600, 88 | url: "https://dash.resonate.coop/images/9fdcace0-8646-44bb-b06c-89be6dfe62b8-x600.jpg", 89 | }, 90 | large: { 91 | width: 1500, 92 | height: 1500, 93 | url: "https://dash.resonate.coop/images/9fdcace0-8646-44bb-b06c-89be6dfe62b8-x1500.jpg", 94 | }, 95 | }, 96 | }, 97 | { 98 | about: null, 99 | cover: 100 | "https://dash.resonate.coop/images/949244ea-9e47-4d04-a8eb-9b4ca74906ec-x600.jpg", 101 | creator_id: 17778, 102 | display_artist: null, 103 | id: "e3b78e08-18a4-4213-af58-7ae1769c4c40", 104 | slug: "feral-five", 105 | tags: [], 106 | title: "Feral Five", 107 | type: "playlist", 108 | cover_metadata: { 109 | id: "949244ea-9e47-4d04-a8eb-9b4ca74906ec", 110 | owner_id: 12778, 111 | }, 112 | user: { 113 | id: 17778, 114 | nicename: "rajames512", 115 | }, 116 | uri: "https://stream.resonate.coop/v3/trackgroups/e3b78e08-18a4-4213-af58-7ae1769c4c40", 117 | images: { 118 | small: { 119 | width: 120, 120 | height: 120, 121 | url: "https://dash.resonate.coop/images/949244ea-9e47-4d04-a8eb-9b4ca74906ec-x120.jpg", 122 | }, 123 | medium: { 124 | width: 600, 125 | height: 600, 126 | url: "https://dash.resonate.coop/images/949244ea-9e47-4d04-a8eb-9b4ca74906ec-x600.jpg", 127 | }, 128 | large: { 129 | width: 1500, 130 | height: 1500, 131 | url: "https://dash.resonate.coop/images/949244ea-9e47-4d04-a8eb-9b4ca74906ec-x1500.jpg", 132 | }, 133 | }, 134 | }, 135 | { 136 | about: null, 137 | cover: 138 | "https://dash.resonate.coop/images/e751d673-67a9-44fc-a6c6-c124bc76ab85-x600.jpg", 139 | creator_id: 11719, 140 | display_artist: null, 141 | id: "771b8b37-788e-4917-821c-8e428919a4dd", 142 | slug: "wow-fuschia", 143 | tags: [], 144 | title: "Wow Fuschia", 145 | type: "playlist", 146 | cover_metadata: { 147 | id: "e751d673-67a9-44fc-a6c6-c124bc76ab85", 148 | owner_id: 12778, 149 | }, 150 | user: { 151 | id: 11719, 152 | nicename: "kat-five", 153 | }, 154 | uri: "https://stream.resonate.coop/v3/trackgroups/771b8b37-788e-4917-821c-8e428919a4dd", 155 | images: { 156 | small: { 157 | width: 120, 158 | height: 120, 159 | url: "https://dash.resonate.coop/images/e751d673-67a9-44fc-a6c6-c124bc76ab85-x120.jpg", 160 | }, 161 | medium: { 162 | width: 600, 163 | height: 600, 164 | url: "https://dash.resonate.coop/images/e751d673-67a9-44fc-a6c6-c124bc76ab85-x600.jpg", 165 | }, 166 | large: { 167 | width: 1500, 168 | height: 1500, 169 | url: "https://dash.resonate.coop/images/e751d673-67a9-44fc-a6c6-c124bc76ab85-x1500.jpg", 170 | }, 171 | }, 172 | }, 173 | { 174 | about: null, 175 | cover: 176 | "https://dash.resonate.coop/images/09f0c2df-cbe9-49e7-8df5-089659293208-x600.jpg", 177 | creator_id: 20911, 178 | display_artist: null, 179 | id: "7495dacc-8208-4c19-bdbb-12f207076bfd", 180 | slug: "how-do-you-delete-a-playlist", 181 | tags: [], 182 | title: "HOW DO YOU DELETE A PLAYLIST", 183 | type: "playlist", 184 | cover_metadata: { 185 | id: "09f0c2df-cbe9-49e7-8df5-089659293208", 186 | owner_id: 19764, 187 | }, 188 | user: { 189 | id: 20911, 190 | nicename: "mobsil", 191 | }, 192 | uri: "https://stream.resonate.coop/v3/trackgroups/7495dacc-8208-4c19-bdbb-12f207076bfd", 193 | images: { 194 | small: { 195 | width: 120, 196 | height: 120, 197 | url: "https://dash.resonate.coop/images/09f0c2df-cbe9-49e7-8df5-089659293208-x120.jpg", 198 | }, 199 | medium: { 200 | width: 600, 201 | height: 600, 202 | url: "https://dash.resonate.coop/images/09f0c2df-cbe9-49e7-8df5-089659293208-x600.jpg", 203 | }, 204 | large: { 205 | width: 1500, 206 | height: 1500, 207 | url: "https://dash.resonate.coop/images/09f0c2df-cbe9-49e7-8df5-089659293208-x1500.jpg", 208 | }, 209 | }, 210 | }, 211 | { 212 | about: null, 213 | cover: 214 | "https://dash.resonate.coop/images/3fc5dadb-4c87-47ed-be17-53d8974b36f9-x600.jpg", 215 | creator_id: 18562, 216 | display_artist: null, 217 | id: "912f8d9d-7012-48e9-8769-7f69d1fe70f4", 218 | slug: "2022-06-19-resonate-on-camp-radio", 219 | tags: [], 220 | title: "2022-06-19 Resonate on CAMP Radio", 221 | type: "playlist", 222 | cover_metadata: { 223 | id: "3fc5dadb-4c87-47ed-be17-53d8974b36f9", 224 | owner_id: 19724, 225 | }, 226 | user: { 227 | id: 18562, 228 | nicename: "caprenter", 229 | }, 230 | uri: "https://stream.resonate.coop/v3/trackgroups/912f8d9d-7012-48e9-8769-7f69d1fe70f4", 231 | images: { 232 | small: { 233 | width: 120, 234 | height: 120, 235 | url: "https://dash.resonate.coop/images/3fc5dadb-4c87-47ed-be17-53d8974b36f9-x120.jpg", 236 | }, 237 | medium: { 238 | width: 600, 239 | height: 600, 240 | url: "https://dash.resonate.coop/images/3fc5dadb-4c87-47ed-be17-53d8974b36f9-x600.jpg", 241 | }, 242 | large: { 243 | width: 1500, 244 | height: 1500, 245 | url: "https://dash.resonate.coop/images/3fc5dadb-4c87-47ed-be17-53d8974b36f9-x1500.jpg", 246 | }, 247 | }, 248 | }, 249 | ], 250 | count: 66, 251 | numberOfPages: 11, 252 | status: "ok", 253 | }); 254 | } 255 | 256 | interface GetTrackgroupsResponse { 257 | data: { 258 | about: string | null; 259 | cover: string; 260 | creator_id: number; 261 | display_artist: any; 262 | id: string; 263 | slug: string; 264 | tags: string[]; 265 | title: string; 266 | type: string; 267 | cover_metadata: any; 268 | user: { 269 | id: number; 270 | nicename: string; 271 | }; 272 | uri: string; 273 | images: { 274 | small: Image; 275 | medium: Image; 276 | large: Image; 277 | }; 278 | }[]; 279 | count: number; 280 | numberOfPages: number; 281 | } 282 | 283 | interface Image { 284 | width: number; 285 | height: number; 286 | url: string; 287 | } 288 | 289 | interface GetFeaturedPlaylistsParams { 290 | limit?: number; 291 | page?: number; 292 | order?: "random" | "oldest" | "newest"; 293 | } 294 | -------------------------------------------------------------------------------- /client/components/Player/index.tsx: -------------------------------------------------------------------------------- 1 | import Slider from "@react-native-community/slider"; 2 | import { 3 | Audio, 4 | AVPlaybackStatus, 5 | InterruptionModeAndroid, 6 | InterruptionModeIOS, 7 | } from "expo-av"; 8 | import isNil from "lodash/isNil"; 9 | import { useState, useEffect, useRef, useLayoutEffect } from "react"; 10 | import { View, StyleSheet, Animated } from "react-native"; 11 | import { useTheme, Text, Appbar, Surface } from "react-native-paper"; 12 | 13 | import { 14 | BOTTOM_NAVIGATION_HEIGHT, 15 | FONT_SIZE, 16 | PLAYER_HEIGHT, 17 | } from "../../constants"; 18 | import useKeyboardVisible from "../../hooks/useKeyboardVisible"; 19 | import { PLAYLIST } from "../../test"; 20 | import { getMMSSFromMillis } from "../../utils"; 21 | import { LoopingType } from "./types"; 22 | 23 | const DISABLED_OPACITY = 0.5; 24 | const LOADING_STRING = "... loading ..."; 25 | const BUFFERING_STRING = "...buffering..."; 26 | 27 | export default function Player() { 28 | const [index, setIndex] = useState(0); 29 | const [isBuffering, setIsBuffering] = useState(false); 30 | const [isLoading, setIsLoading] = useState(false); 31 | const [isPlaying, setIsPlaying] = useState(false); 32 | const [isSeeking, setIsSeeking] = useState(false); 33 | const [loopingType, setLoopingType] = useState(LoopingType.ALL); 34 | const [muted, setMuted] = useState(false); 35 | const [playbackInstance, setPlaybackInstance] = useState( 36 | null 37 | ); 38 | const [playbackInstanceDuration, setPlaybackInstanceDuration] = useState< 39 | number | null 40 | >(null); 41 | const [playbackInstanceName, setPlaybackInstanceName] = 42 | useState(LOADING_STRING); 43 | const [playbackInstancePosition, setPlaybackInstancePosition] = useState< 44 | number | null 45 | >(null); 46 | const [rate, setRate] = useState(1.0); 47 | const [shouldCorrectPitch, setShouldCorrectPitch] = useState(false); 48 | const [shouldPlay, setShouldPlay] = useState(false); 49 | const [shouldPlayAtEndOfSeek, setShouldPlayAtEndOfSeek] = 50 | useState(false); 51 | const [volume, setVolume] = useState(1.0); 52 | const theme = useTheme(); 53 | const keyboardVisible = useKeyboardVisible(); 54 | const shiftAnim = useRef(new Animated.Value(1)).current; 55 | 56 | useLayoutEffect(() => { 57 | Animated.spring(shiftAnim, { 58 | toValue: keyboardVisible ? 0 : 1, 59 | useNativeDriver: true, 60 | }).start(); 61 | }, [keyboardVisible]); 62 | 63 | useEffect(() => { 64 | Audio.setAudioModeAsync({ 65 | allowsRecordingIOS: false, 66 | staysActiveInBackground: false, 67 | interruptionModeIOS: InterruptionModeIOS.DoNotMix, 68 | playsInSilentModeIOS: true, 69 | shouldDuckAndroid: true, 70 | interruptionModeAndroid: InterruptionModeAndroid.DoNotMix, 71 | playThroughEarpieceAndroid: false, 72 | }); 73 | if (playbackInstance == null) { 74 | _loadNewPlaybackInstance(false); 75 | } 76 | _advanceIndex(true); 77 | }, []); 78 | 79 | async function _loadNewPlaybackInstance(playing: boolean) { 80 | if (playbackInstance != null) { 81 | await playbackInstance.unloadAsync(); 82 | setPlaybackInstance(null); 83 | } 84 | 85 | const source = { uri: PLAYLIST[index].uri }; 86 | const initialStatus = { 87 | shouldPlay: playing, 88 | rate, 89 | shouldCorrectPitch, 90 | volume, 91 | isMuted: muted, 92 | isLooping: loopingType === LoopingType.ONE, 93 | }; 94 | 95 | const { sound } = await Audio.Sound.createAsync( 96 | source, 97 | initialStatus, 98 | _onPlaybackStatusUpdate 99 | ); 100 | setPlaybackInstance(sound); 101 | 102 | _updateScreenForLoading(false); 103 | } 104 | 105 | function _updateScreenForLoading(isLoading: boolean) { 106 | if (isLoading) { 107 | setIsPlaying(false); 108 | setPlaybackInstanceName(LOADING_STRING); 109 | setPlaybackInstanceDuration(null); 110 | setPlaybackInstancePosition(null); 111 | setIsLoading(true); 112 | } else { 113 | setPlaybackInstanceName(PLAYLIST[index].name); 114 | setIsLoading(false); 115 | } 116 | } 117 | 118 | function _onPlaybackStatusUpdate(status: AVPlaybackStatus): void { 119 | if (status.isLoaded) { 120 | setPlaybackInstancePosition(status.positionMillis); 121 | setPlaybackInstanceDuration(status.durationMillis ?? null); 122 | setShouldPlay(status.shouldPlay); 123 | setIsPlaying(status.isPlaying); 124 | setIsBuffering(status.isBuffering); 125 | setRate(status.rate); 126 | setMuted(status.isMuted); 127 | setVolume(status.volume); 128 | setLoopingType(status.isLooping ? LoopingType.ONE : LoopingType.ALL); 129 | setShouldCorrectPitch(status.shouldCorrectPitch); 130 | 131 | if (status.didJustFinish && !status.isLooping) { 132 | _advanceIndex(true); 133 | _updatePlaybackInstanceForIndex(true); 134 | } 135 | } else { 136 | if (status.error) { 137 | console.log(`FATAL PLAYER ERROR: ${status.error}`); 138 | } 139 | } 140 | } 141 | 142 | function _advanceIndex(forward: boolean) { 143 | setIndex((index + (forward ? 1 : PLAYLIST.length - 1)) % PLAYLIST.length); 144 | } 145 | 146 | async function _updatePlaybackInstanceForIndex(playing: boolean) { 147 | _updateScreenForLoading(true); 148 | _loadNewPlaybackInstance(playing); 149 | } 150 | 151 | function _onPlayPausePressed() { 152 | if (playbackInstance != null) { 153 | if (isPlaying) { 154 | playbackInstance.pauseAsync(); 155 | } else { 156 | playbackInstance.playAsync(); 157 | } 158 | } 159 | } 160 | 161 | function _onStopPressed() { 162 | if (playbackInstance != null) { 163 | playbackInstance.stopAsync(); 164 | } 165 | } 166 | 167 | function _onForwardPressed() { 168 | if (playbackInstance != null) { 169 | _advanceIndex(true); 170 | _updatePlaybackInstanceForIndex(shouldPlay || isPlaying); 171 | } 172 | } 173 | 174 | function _onBackPressed() { 175 | if (playbackInstance != null) { 176 | _advanceIndex(false); 177 | _updatePlaybackInstanceForIndex(shouldPlay || isPlaying); 178 | } 179 | } 180 | 181 | function _onLoopPressed() { 182 | if (playbackInstance != null) { 183 | playbackInstance.setIsLoopingAsync(loopingType !== LoopingType.ONE); 184 | } 185 | } 186 | 187 | function _onSeekSliderValueChange() { 188 | if (playbackInstance != null && !isSeeking) { 189 | setIsSeeking(true); 190 | setShouldPlayAtEndOfSeek(shouldPlay); 191 | playbackInstance.pauseAsync(); 192 | } 193 | } 194 | 195 | async function _onSeekSliderSlidingComplete(value: number) { 196 | if (!isNil(playbackInstance) && !isNil(playbackInstanceDuration)) { 197 | setIsSeeking(false); 198 | const seekPosition = value * playbackInstanceDuration; 199 | if (shouldPlayAtEndOfSeek) { 200 | playbackInstance.playFromPositionAsync(seekPosition); 201 | } else { 202 | playbackInstance.setPositionAsync(seekPosition); 203 | } 204 | } 205 | } 206 | 207 | function _getSeekSliderPosition() { 208 | if ( 209 | playbackInstance != null && 210 | playbackInstancePosition != null && 211 | playbackInstanceDuration != null 212 | ) { 213 | return playbackInstancePosition / playbackInstanceDuration; 214 | } 215 | return 0; 216 | } 217 | 218 | function _getTimestamp() { 219 | if ( 220 | playbackInstance != null && 221 | playbackInstancePosition != null && 222 | playbackInstanceDuration != null 223 | ) { 224 | return `${getMMSSFromMillis( 225 | playbackInstancePosition 226 | )} / ${getMMSSFromMillis(playbackInstanceDuration)}`; 227 | } 228 | return ""; 229 | } 230 | 231 | return ( 232 | 244 | 253 | 261 | 262 | {isBuffering || isLoading ? ( 263 | 264 | {isBuffering ? BUFFERING_STRING : LOADING_STRING} 265 | 266 | ) : ( 267 | {playbackInstanceName} 268 | )} 269 | 270 | 279 | 280 | 281 | 286 | 291 | 296 | 301 | 308 | 312 | 313 | 314 | 315 | ); 316 | } 317 | 318 | export const styles = StyleSheet.create({ 319 | contentContainer: { 320 | position: "absolute", 321 | width: "100%", 322 | height: PLAYER_HEIGHT, 323 | }, 324 | playbackContainer: { 325 | width: "100%", 326 | flex: 1, 327 | }, 328 | trackInfoRow: { 329 | flex: 1, 330 | alignItems: "center", 331 | marginTop: 22, 332 | }, 333 | text: { 334 | fontSize: FONT_SIZE, 335 | }, 336 | timestamp: { 337 | textAlign: "right", 338 | marginRight: 16, 339 | }, 340 | }); 341 | -------------------------------------------------------------------------------- /client/api/artists.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | 3 | const artistKeys = { 4 | all: ["artists"] as const, 5 | lastUpdated: ({ limit, page }: GetLastUpdatedArtistsParams = {}) => [ 6 | ...artistKeys.all, 7 | "lastUpdated", 8 | { limit, page }, 9 | ], 10 | }; 11 | 12 | export function useLastUpdatedArtists( 13 | params: GetLastUpdatedArtistsParams = {} 14 | ) { 15 | return useQuery(artistKeys.lastUpdated(params), () => 16 | getLastUpdatedArtists(params) 17 | ); 18 | } 19 | 20 | function getLastUpdatedArtists({ 21 | limit, 22 | page, 23 | }: GetLastUpdatedArtistsParams = {}): Promise { 24 | return Promise.resolve({ 25 | data: [ 26 | { 27 | name: "Jaded Forum", 28 | id: 21687, 29 | images: { 30 | "cover_photo-l": 31 | "https://resonate.is/wp-content/uploads/ultimatemember/21687/cover_photo-1500.jpeg", 32 | "cover_photo-s": 33 | "https://resonate.is/wp-content/uploads/ultimatemember/21687/cover_photo-500.jpeg", 34 | "cover_photo-m": 35 | "https://resonate.is/wp-content/uploads/ultimatemember/21687/cover_photo-600.jpeg", 36 | cover_photo: 37 | "https://resonate.is/wp-content/uploads/ultimatemember/21687/cover_photo.jpeg", 38 | 39 | "profile_photo-xxl": 40 | "https://resonate.is/wp-content/uploads/ultimatemember/21687/profile_photo-1200x1200.png", 41 | "profile_photo-m": 42 | "https://resonate.is/wp-content/uploads/ultimatemember/21687/profile_photo-300x300.png", 43 | "profile_photo-l": 44 | "https://resonate.is/wp-content/uploads/ultimatemember/21687/profile_photo-400x400.png", 45 | "profile_photo-xs": 46 | "https://resonate.is/wp-content/uploads/ultimatemember/21687/profile_photo-40x40.png", 47 | "profile_photo-xl": 48 | "https://resonate.is/wp-content/uploads/ultimatemember/21687/profile_photo-800x800.png", 49 | "profile_photo-sm": 50 | "https://resonate.is/wp-content/uploads/ultimatemember/21687/profile_photo-80x80.png", 51 | }, 52 | }, 53 | { 54 | name: "BREADMACHINE", 55 | id: 21108, 56 | images: { 57 | "profile_photo-m": 58 | "https://resonate.is/wp-content/uploads/ultimatemember/21108/profile_photo-300x300.jpg", 59 | "profile_photo-xs": 60 | "https://resonate.is/wp-content/uploads/ultimatemember/21108/profile_photo-40x40.jpg", 61 | "profile_photo-sm": 62 | "https://resonate.is/wp-content/uploads/ultimatemember/21108/profile_photo-80x80.jpg", 63 | profile_photo: 64 | "https://resonate.is/wp-content/uploads/ultimatemember/21108/profile_photo.jpg", 65 | }, 66 | }, 67 | { 68 | name: "Alex Griffiths", 69 | id: 18010, 70 | images: { 71 | "profile_photo-m": 72 | "https://resonate.is/wp-content/uploads/ultimatemember/18010/profile_photo-300x300.jpg", 73 | "profile_photo-l": 74 | "https://resonate.is/wp-content/uploads/ultimatemember/18010/profile_photo-400x400.jpg", 75 | "profile_photo-xs": 76 | "https://resonate.is/wp-content/uploads/ultimatemember/18010/profile_photo-40x40.jpg", 77 | "profile_photo-sm": 78 | "https://resonate.is/wp-content/uploads/ultimatemember/18010/profile_photo-80x80.jpg", 79 | profile_photo: 80 | "https://resonate.is/wp-content/uploads/ultimatemember/18010/profile_photo.jpg", 81 | }, 82 | }, 83 | { 84 | name: "w_lf", 85 | id: 22154, 86 | images: { 87 | "cover_photo-l": 88 | "https://resonate.is/wp-content/uploads/ultimatemember/22154/cover_photo-1500.jpg", 89 | "cover_photo-s": 90 | "https://resonate.is/wp-content/uploads/ultimatemember/22154/cover_photo-500.jpg", 91 | "cover_photo-m": 92 | "https://resonate.is/wp-content/uploads/ultimatemember/22154/cover_photo-600.jpg", 93 | cover_photo: 94 | "https://resonate.is/wp-content/uploads/ultimatemember/22154/cover_photo.jpg", 95 | "profile_photo-m": 96 | "https://resonate.is/wp-content/uploads/ultimatemember/22154/profile_photo-300x300.jpg", 97 | "profile_photo-l": 98 | "https://resonate.is/wp-content/uploads/ultimatemember/22154/profile_photo-400x400.jpg", 99 | "profile_photo-xs": 100 | "https://resonate.is/wp-content/uploads/ultimatemember/22154/profile_photo-40x40.jpg", 101 | "profile_photo-xl": 102 | "https://resonate.is/wp-content/uploads/ultimatemember/22154/profile_photo-800x800.jpg", 103 | "profile_photo-sm": 104 | "https://resonate.is/wp-content/uploads/ultimatemember/22154/profile_photo-80x80.jpg", 105 | profile_photo: 106 | "https://resonate.is/wp-content/uploads/ultimatemember/22154/profile_photo.jpg", 107 | }, 108 | }, 109 | { 110 | name: "Bios Contrast", 111 | id: 19763, 112 | images: { 113 | "cover_photo-l": 114 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/cover_photo-1500.jpg", 115 | "cover_photo-s": 116 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/cover_photo-500.jpg", 117 | "cover_photo-m": 118 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/cover_photo-600.jpg", 119 | cover_photo: 120 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/cover_photo.jpg", 121 | "profile_photo-xxl": 122 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/profile_photo-1200x1200.jpg", 123 | "profile_photo-m": 124 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/profile_photo-300x300.jpg", 125 | "profile_photo-l": 126 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/profile_photo-400x400.jpg", 127 | "profile_photo-xs": 128 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/profile_photo-40x40.jpg", 129 | "profile_photo-xl": 130 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/profile_photo-800x800.jpg", 131 | "profile_photo-sm": 132 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/profile_photo-80x80.jpg", 133 | profile_photo: 134 | "https://resonate.is/wp-content/uploads/ultimatemember/19763/profile_photo.jpg", 135 | }, 136 | }, 137 | { 138 | name: "‡Starving Poet§", 139 | id: 8091, 140 | images: { 141 | "cover_photo-l": 142 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/cover_photo-1500.jpg", 143 | "cover_photo-s": 144 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/cover_photo-500.jpg", 145 | "cover_photo-m": 146 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/cover_photo-600.jpg", 147 | cover_photo: 148 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/cover_photo.jpg", 149 | "profile_photo-xxl": 150 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/profile_photo-1200x1200.jpg", 151 | "profile_photo-m": 152 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/profile_photo-300x300.jpg", 153 | "profile_photo-l": 154 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/profile_photo-400x400.jpg", 155 | "profile_photo-xs": 156 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/profile_photo-40x40.jpg", 157 | "profile_photo-xl": 158 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/profile_photo-800x800.jpg", 159 | "profile_photo-sm": 160 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/profile_photo-80x80.jpg", 161 | profile_photo: 162 | "https://resonate.is/wp-content/uploads/ultimatemember/8091/profile_photo.jpg", 163 | }, 164 | }, 165 | { 166 | name: "Jason Murray", 167 | id: 12122, 168 | images: { 169 | "cover_photo-l": 170 | "https://resonate.is/wp-content/uploads/ultimatemember/12122/cover_photo-1500x469.jpg", 171 | "cover_photo-s": 172 | "https://resonate.is/wp-content/uploads/ultimatemember/12122/cover_photo-500x156.jpg", 173 | cover_photo: 174 | "https://resonate.is/wp-content/uploads/ultimatemember/12122/cover_photo.jpg", 175 | "profile_photo-m": 176 | "https://resonate.is/wp-content/uploads/ultimatemember/12122/profile_photo-300x300.jpg", 177 | "profile_photo-l": 178 | "https://resonate.is/wp-content/uploads/ultimatemember/12122/profile_photo-400x400.jpg", 179 | "profile_photo-xs": 180 | "https://resonate.is/wp-content/uploads/ultimatemember/12122/profile_photo-40x40.jpg", 181 | "profile_photo-sm": 182 | "https://resonate.is/wp-content/uploads/ultimatemember/12122/profile_photo-80x80.jpg", 183 | profile_photo: 184 | "https://resonate.is/wp-content/uploads/ultimatemember/12122/profile_photo.jpg", 185 | }, 186 | }, 187 | { 188 | name: "paun", 189 | id: 21630, 190 | images: { 191 | "cover_photo-s": 192 | "https://resonate.is/wp-content/uploads/ultimatemember/21630/cover_photo-500.jpeg", 193 | "cover_photo-m": 194 | "https://resonate.is/wp-content/uploads/ultimatemember/21630/cover_photo-600.jpeg", 195 | cover_photo: 196 | "https://resonate.is/wp-content/uploads/ultimatemember/21630/cover_photo.jpeg", 197 | "profile_photo-m": 198 | "https://resonate.is/wp-content/uploads/ultimatemember/21630/profile_photo-300x300.jpeg", 199 | "profile_photo-l": 200 | "https://resonate.is/wp-content/uploads/ultimatemember/21630/profile_photo-400x400.jpeg", 201 | "profile_photo-xs": 202 | "https://resonate.is/wp-content/uploads/ultimatemember/21630/profile_photo-40x40.jpeg", 203 | "profile_photo-xl": 204 | "https://resonate.is/wp-content/uploads/ultimatemember/21630/profile_photo-800x800.jpeg", 205 | "profile_photo-sm": 206 | "https://resonate.is/wp-content/uploads/ultimatemember/21630/profile_photo-80x80.jpeg", 207 | profile_photo: 208 | "https://resonate.is/wp-content/uploads/ultimatemember/21630/profile_photo.jpeg", 209 | }, 210 | }, 211 | { 212 | name: "Jooiing", 213 | id: 11905, 214 | images: { 215 | "cover_photo-l": 216 | "https://resonate.is/wp-content/uploads/ultimatemember/11905/cover_photo-1500x469.jpg", 217 | "cover_photo-s": 218 | "https://resonate.is/wp-content/uploads/ultimatemember/11905/cover_photo-500x156.jpg", 219 | cover_photo: 220 | "https://resonate.is/wp-content/uploads/ultimatemember/11905/cover_photo.jpg", 221 | "profile_photo-m": 222 | "https://resonate.is/wp-content/uploads/ultimatemember/11905/profile_photo-300x300.jpg", 223 | "profile_photo-l": 224 | "https://resonate.is/wp-content/uploads/ultimatemember/11905/profile_photo-400x400.jpg", 225 | "profile_photo-xs": 226 | "https://resonate.is/wp-content/uploads/ultimatemember/11905/profile_photo-40x40.jpg", 227 | "profile_photo-sm": 228 | "https://resonate.is/wp-content/uploads/ultimatemember/11905/profile_photo-80x80.jpg", 229 | profile_photo: 230 | "https://resonate.is/wp-content/uploads/ultimatemember/11905/profile_photo.jpg", 231 | }, 232 | }, 233 | ], 234 | numberOfPages: null, 235 | }); 236 | } 237 | 238 | interface GetLastUpdatedArtistsResponse { 239 | data: { 240 | name: string; 241 | id: number; 242 | images: Image; 243 | }[]; 244 | count?: number; 245 | numberOfPages: number | null; 246 | } 247 | 248 | interface Image { 249 | [imageKey: string]: string | undefined; 250 | } 251 | 252 | interface GetLastUpdatedArtistsParams { 253 | limit?: number; 254 | page?: number; 255 | } 256 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | import org.apache.tools.ant.taskdefs.condition.Os 5 | 6 | /** 7 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 8 | * and bundleReleaseJsAndAssets). 9 | * These basically call `react-native bundle` with the correct arguments during the Android build 10 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 11 | * bundle directly from the development server. Below you can see all the possible configurations 12 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 13 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 14 | * 15 | * project.ext.react = [ 16 | * // the name of the generated asset file containing your JS bundle 17 | * bundleAssetName: "index.android.bundle", 18 | * 19 | * // the entry file for bundle generation. If none specified and 20 | * // "index.android.js" exists, it will be used. Otherwise "index.js" is 21 | * // default. Can be overridden with ENTRY_FILE environment variable. 22 | * entryFile: "index.android.js", 23 | * 24 | * // https://reactnative.dev/docs/performance#enable-the-ram-format 25 | * bundleCommand: "ram-bundle", 26 | * 27 | * // whether to bundle JS and assets in debug mode 28 | * bundleInDebug: false, 29 | * 30 | * // whether to bundle JS and assets in release mode 31 | * bundleInRelease: true, 32 | * 33 | * // whether to bundle JS and assets in another build variant (if configured). 34 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 35 | * // The configuration property can be in the following formats 36 | * // 'bundleIn${productFlavor}${buildType}' 37 | * // 'bundleIn${buildType}' 38 | * // bundleInFreeDebug: true, 39 | * // bundleInPaidRelease: true, 40 | * // bundleInBeta: true, 41 | * 42 | * // whether to disable dev mode in custom build variants (by default only disabled in release) 43 | * // for example: to disable dev mode in the staging build type (if configured) 44 | * devDisabledInStaging: true, 45 | * // The configuration property can be in the following formats 46 | * // 'devDisabledIn${productFlavor}${buildType}' 47 | * // 'devDisabledIn${buildType}' 48 | * 49 | * // the root of your project, i.e. where "package.json" lives 50 | * root: "../../", 51 | * 52 | * // where to put the JS bundle asset in debug mode 53 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 54 | * 55 | * // where to put the JS bundle asset in release mode 56 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 57 | * 58 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 59 | * // require('./image.png')), in debug mode 60 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 61 | * 62 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 63 | * // require('./image.png')), in release mode 64 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 65 | * 66 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 67 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 68 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 69 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 70 | * // for example, you might want to remove it from here. 71 | * inputExcludes: ["android/**", "ios/**"], 72 | * 73 | * // override which node gets called and with what additional arguments 74 | * nodeExecutableAndArgs: ["node"], 75 | * 76 | * // supply additional arguments to the packager 77 | * extraPackagerArgs: [] 78 | * ] 79 | */ 80 | 81 | def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() 82 | 83 | def reactNativeRoot = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() 84 | 85 | project.ext.react = [ 86 | entryFile: ["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android"].execute(null, rootDir).text.trim(), 87 | enableHermes: (findProperty('expo.jsEngine') ?: "jsc") == "hermes", 88 | hermesCommand: new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc", 89 | cliPath: "${reactNativeRoot}/cli.js", 90 | composeSourceMapsPath: "${reactNativeRoot}/scripts/compose-source-maps.js", 91 | ] 92 | 93 | apply from: new File(reactNativeRoot, "react.gradle") 94 | 95 | /** 96 | * Set this to true to create two separate APKs instead of one: 97 | * - An APK that only works on ARM devices 98 | * - An APK that only works on x86 devices 99 | * The advantage is the size of the APK is reduced by about 4MB. 100 | * Upload all the APKs to the Play Store and people will download 101 | * the correct one based on the CPU architecture of their device. 102 | */ 103 | def enableSeparateBuildPerCPUArchitecture = false 104 | 105 | /** 106 | * Run Proguard to shrink the Java bytecode in release builds. 107 | */ 108 | def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean() 109 | 110 | /** 111 | * The preferred build flavor of JavaScriptCore. 112 | * 113 | * For example, to use the international variant, you can use: 114 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 115 | * 116 | * The international variant includes ICU i18n library and necessary data 117 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 118 | * give correct results when using with locales other than en-US. Note that 119 | * this variant is about 6MiB larger per architecture than default. 120 | */ 121 | def jscFlavor = 'org.webkit:android-jsc:+' 122 | 123 | /** 124 | * Whether to enable the Hermes VM. 125 | * 126 | * This should be set on project.ext.react and that value will be read here. If it is not set 127 | * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode 128 | * and the benefits of using Hermes will therefore be sharply reduced. 129 | */ 130 | def enableHermes = project.ext.react.get("enableHermes", false); 131 | 132 | /** 133 | * Architectures to build native code for. 134 | */ 135 | def reactNativeArchitectures() { 136 | def value = project.getProperties().get("reactNativeArchitectures") 137 | return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] 138 | } 139 | 140 | android { 141 | ndkVersion rootProject.ext.ndkVersion 142 | 143 | compileSdkVersion rootProject.ext.compileSdkVersion 144 | 145 | defaultConfig { 146 | applicationId 'com.resonatecoop.mobile' 147 | minSdkVersion rootProject.ext.minSdkVersion 148 | targetSdkVersion rootProject.ext.targetSdkVersion 149 | versionCode 1 150 | versionName "1.0.0" 151 | buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() 152 | 153 | if (isNewArchitectureEnabled()) { 154 | // We configure the NDK build only if you decide to opt-in for the New Architecture. 155 | externalNativeBuild { 156 | ndkBuild { 157 | arguments "APP_PLATFORM=android-21", 158 | "APP_STL=c++_shared", 159 | "NDK_TOOLCHAIN_VERSION=clang", 160 | "GENERATED_SRC_DIR=$buildDir/generated/source", 161 | "PROJECT_BUILD_DIR=$buildDir", 162 | "REACT_ANDROID_DIR=${reactNativeRoot}/ReactAndroid", 163 | "REACT_ANDROID_BUILD_DIR=${reactNativeRoot}/ReactAndroid/build", 164 | "NODE_MODULES_DIR=$rootDir/../node_modules" 165 | cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1" 166 | cppFlags "-std=c++17" 167 | // Make sure this target name is the same you specify inside the 168 | // src/main/jni/Android.mk file for the `LOCAL_MODULE` variable. 169 | targets "mobile_appmodules" 170 | 171 | // Fix for windows limit on number of character in file paths and in command lines 172 | if (Os.isFamily(Os.FAMILY_WINDOWS)) { 173 | arguments "NDK_APP_SHORT_COMMANDS=true" 174 | } 175 | } 176 | } 177 | if (!enableSeparateBuildPerCPUArchitecture) { 178 | ndk { 179 | abiFilters (*reactNativeArchitectures()) 180 | } 181 | } 182 | } 183 | } 184 | 185 | if (isNewArchitectureEnabled()) { 186 | // We configure the NDK build only if you decide to opt-in for the New Architecture. 187 | externalNativeBuild { 188 | ndkBuild { 189 | path "$projectDir/src/main/jni/Android.mk" 190 | } 191 | } 192 | def reactAndroidProjectDir = project(':ReactAndroid').projectDir 193 | def packageReactNdkDebugLibs = tasks.register("packageReactNdkDebugLibs", Copy) { 194 | dependsOn(":ReactAndroid:packageReactNdkDebugLibsForBuck") 195 | from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib") 196 | into("$buildDir/react-ndk/exported") 197 | } 198 | def packageReactNdkReleaseLibs = tasks.register("packageReactNdkReleaseLibs", Copy) { 199 | dependsOn(":ReactAndroid:packageReactNdkReleaseLibsForBuck") 200 | from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib") 201 | into("$buildDir/react-ndk/exported") 202 | } 203 | afterEvaluate { 204 | // If you wish to add a custom TurboModule or component locally, 205 | // you should uncomment this line. 206 | // preBuild.dependsOn("generateCodegenArtifactsFromSchema") 207 | preDebugBuild.dependsOn(packageReactNdkDebugLibs) 208 | preReleaseBuild.dependsOn(packageReactNdkReleaseLibs) 209 | 210 | // Due to a bug inside AGP, we have to explicitly set a dependency 211 | // between configureNdkBuild* tasks and the preBuild tasks. 212 | // This can be removed once this is solved: https://issuetracker.google.com/issues/207403732 213 | configureNdkBuildRelease.dependsOn(preReleaseBuild) 214 | configureNdkBuildDebug.dependsOn(preDebugBuild) 215 | reactNativeArchitectures().each { architecture -> 216 | tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure { 217 | dependsOn("preDebugBuild") 218 | } 219 | tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure { 220 | dependsOn("preReleaseBuild") 221 | } 222 | } 223 | } 224 | } 225 | 226 | splits { 227 | abi { 228 | reset() 229 | enable enableSeparateBuildPerCPUArchitecture 230 | universalApk false // If true, also generate a universal APK 231 | include (*reactNativeArchitectures()) 232 | } 233 | } 234 | signingConfigs { 235 | debug { 236 | storeFile file('debug.keystore') 237 | storePassword 'android' 238 | keyAlias 'androiddebugkey' 239 | keyPassword 'android' 240 | } 241 | } 242 | buildTypes { 243 | debug { 244 | signingConfig signingConfigs.debug 245 | } 246 | release { 247 | // Caution! In production, you need to generate your own keystore file. 248 | // see https://reactnative.dev/docs/signed-apk-android. 249 | signingConfig signingConfigs.debug 250 | minifyEnabled enableProguardInReleaseBuilds 251 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 252 | } 253 | } 254 | 255 | // applicationVariants are e.g. debug, release 256 | applicationVariants.all { variant -> 257 | variant.outputs.each { output -> 258 | // For each separate APK per architecture, set a unique version code as described here: 259 | // https://developer.android.com/studio/build/configure-apk-splits.html 260 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] 261 | def abi = output.getFilter(OutputFile.ABI) 262 | if (abi != null) { // null for the universal-debug, universal-release variants 263 | output.versionCodeOverride = 264 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 265 | } 266 | 267 | } 268 | } 269 | } 270 | 271 | // Apply static values from `gradle.properties` to the `android.packagingOptions` 272 | // Accepts values in comma delimited lists, example: 273 | // android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini 274 | ["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop -> 275 | // Split option: 'foo,bar' -> ['foo', 'bar'] 276 | def options = (findProperty("android.packagingOptions.$prop") ?: "").split(","); 277 | // Trim all elements in place. 278 | for (i in 0.. 0) { 283 | println "android.packagingOptions.$prop += $options ($options.length)" 284 | // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**' 285 | options.each { 286 | android.packagingOptions[prop] += it 287 | } 288 | } 289 | } 290 | 291 | dependencies { 292 | implementation fileTree(dir: "libs", include: ["*.jar"]) 293 | 294 | //noinspection GradleDynamicVersion 295 | implementation "com.facebook.react:react-native:+" // From node_modules 296 | 297 | def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; 298 | def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; 299 | def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; 300 | def frescoVersion = rootProject.ext.frescoVersion 301 | 302 | // If your app supports Android versions before Ice Cream Sandwich (API level 14) 303 | if (isGifEnabled || isWebpEnabled) { 304 | implementation "com.facebook.fresco:fresco:${frescoVersion}" 305 | implementation "com.facebook.fresco:imagepipeline-okhttp3:${frescoVersion}" 306 | } 307 | 308 | if (isGifEnabled) { 309 | // For animated gif support 310 | implementation "com.facebook.fresco:animated-gif:${frescoVersion}" 311 | } 312 | 313 | if (isWebpEnabled) { 314 | // For webp support 315 | implementation "com.facebook.fresco:webpsupport:${frescoVersion}" 316 | if (isWebpAnimatedEnabled) { 317 | // Animated webp support 318 | implementation "com.facebook.fresco:animated-webp:${frescoVersion}" 319 | } 320 | } 321 | 322 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" 323 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { 324 | exclude group:'com.facebook.fbjni' 325 | } 326 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { 327 | exclude group:'com.facebook.flipper' 328 | exclude group:'com.squareup.okhttp3', module:'okhttp' 329 | } 330 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") { 331 | exclude group:'com.facebook.flipper' 332 | } 333 | 334 | if (enableHermes) { 335 | //noinspection GradleDynamicVersion 336 | implementation("com.facebook.react:hermes-engine:+") { // From node_modules 337 | exclude group:'com.facebook.fbjni' 338 | } 339 | } else { 340 | implementation jscFlavor 341 | } 342 | } 343 | 344 | if (isNewArchitectureEnabled()) { 345 | // If new architecture is enabled, we let you build RN from source 346 | // Otherwise we fallback to a prebuilt .aar bundled in the NPM package. 347 | // This will be applied to all the imported transtitive dependency. 348 | configurations.all { 349 | resolutionStrategy.dependencySubstitution { 350 | substitute(module("com.facebook.react:react-native")) 351 | .using(project(":ReactAndroid")) 352 | .because("On New Architecture we're building React Native from source") 353 | substitute(module("com.facebook.react:hermes-engine")) 354 | .using(project(":ReactAndroid:hermes-engine")) 355 | .because("On New Architecture we're building Hermes from source") 356 | } 357 | } 358 | } 359 | 360 | // Run this once to be able to run the application with BUCK 361 | // puts all compile dependencies into folder libs for BUCK to use 362 | task copyDownloadableDepsToLibs(type: Copy) { 363 | from configurations.implementation 364 | into 'libs' 365 | } 366 | 367 | apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); 368 | applyNativeModulesAppBuildGradle(project) 369 | 370 | def isNewArchitectureEnabled() { 371 | // To opt-in for the New Architecture, you can either: 372 | // - Set `newArchEnabled` to true inside the `gradle.properties` file 373 | // - Invoke gradle with `-newArchEnabled=true` 374 | // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true` 375 | return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" 376 | } 377 | --------------------------------------------------------------------------------