├── .eslintignore
├── .fingerprintignore
├── assets
├── icon.png
├── favicon.png
├── splash.png
├── icons
│ ├── gift.png
│ └── icon-desert.png
├── images
│ ├── desert.png
│ ├── hotel.png
│ ├── react-blue.png
│ ├── react-green.png
│ ├── react-logo.png
│ ├── react-orange.png
│ ├── react-dark-blue.png
│ ├── reactlogo-cyan.png
│ ├── reactlogo-white.png
│ ├── sponsor-abbott.png
│ ├── sponsor-amazon.png
│ ├── sponsor-amazon.webp
│ ├── x.svg
│ ├── linkedin.svg
│ ├── sponsor-vercel.svg
│ ├── sponsor-sentry.svg
│ ├── sponsor-mui.svg
│ ├── sponsor-expo.svg
│ ├── not-found.svg
│ ├── meta-logo.svg
│ ├── react-logo.svg
│ ├── sponsor-abbott.svg
│ ├── callstack-logo.svg
│ ├── sponsor-remix-spotify.svg
│ └── sponsor-redwood.svg
├── bootsplash
│ ├── logo.png
│ ├── brand.png
│ ├── logo@2x.png
│ ├── logo@3x.png
│ ├── logo@4x.png
│ ├── brand@2x.png
│ ├── brand@3x.png
│ ├── brand@4x.png
│ ├── logo@1,5x.png
│ ├── brand@1,5x.png
│ ├── android
│ │ ├── drawable-hdpi
│ │ │ ├── bootsplash_logo.png
│ │ │ └── bootsplash_brand.png
│ │ ├── drawable-mdpi
│ │ │ ├── bootsplash_logo.png
│ │ │ └── bootsplash_brand.png
│ │ ├── drawable-xhdpi
│ │ │ ├── bootsplash_brand.png
│ │ │ └── bootsplash_logo.png
│ │ ├── drawable-xxhdpi
│ │ │ ├── bootsplash_logo.png
│ │ │ └── bootsplash_brand.png
│ │ └── drawable-xxxhdpi
│ │ │ ├── bootsplash_brand.png
│ │ │ └── bootsplash_logo.png
│ ├── ios
│ │ ├── Images.xcassets
│ │ │ ├── BootSplashLogo-adxe67.imageset
│ │ │ │ ├── logo-adxe67.png
│ │ │ │ ├── logo-adxe67@2x.png
│ │ │ │ ├── logo-adxe67@3x.png
│ │ │ │ └── Contents.json
│ │ │ └── BootSplashBrand-adxe67.imageset
│ │ │ │ ├── brand-adxe67.png
│ │ │ │ ├── brand-adxe67@2x.png
│ │ │ │ ├── brand-adxe67@3x.png
│ │ │ │ └── Contents.json
│ │ ├── Colors.xcassets
│ │ │ └── BootSplashBackground-adxe67.colorset
│ │ │ │ └── Contents.json
│ │ └── BootSplash.storyboard
│ └── manifest.json
├── icon-android-foreground.png
└── fonts
│ ├── FreightSansProBlack-Italic.ttf
│ ├── FreightSansProBlack-Regular.ttf
│ ├── FreightSansProBold-Italic.ttf
│ ├── FreightSansProBold-Regular.ttf
│ ├── FreightSansProBook-Italic.ttf
│ ├── FreightSansProBook-Regular.ttf
│ ├── FreightSansProLight-Italic.ttf
│ ├── FreightSansProLight-Regular.ttf
│ ├── FreightSansProMedium-Italic.ttf
│ ├── FreightSansProMedium-Regular.ttf
│ ├── FreightSansProSemibold-Italic.ttf
│ └── FreightSansProSemibold-Regular.ttf
├── consts.ts
├── declarations.d.ts
├── lib
└── react-compiler-runtime
│ ├── README.md
│ ├── package.json
│ └── index.js
├── tsconfig.json
├── .eslintrc.js
├── babel.config.js
├── utils
├── useAppStateEffect.ts
├── openWebBrowserAsync.ts
├── registerForPushNotificationsAsync.ts
├── useQuickActionCallback.ts
├── sessions.ts
├── formatDate.ts
├── sessions.test.ts
└── useScrollToTopWithOffset.ts
├── .gitignore
├── scripts
└── syncApi.js
├── .github
└── workflows
│ └── pr-preview.yml
├── types.ts
├── components
├── DiscordInfo.tsx
├── PoweredByExpo.tsx
├── LiveStreamInfo.tsx
├── BackButton.tsx
├── PressableArea.tsx
├── Tags.tsx
├── InfoSection.tsx
├── Heading.tsx
├── IconButton.tsx
├── BuildDetails.tsx
├── ActivityCard.tsx
├── Button.tsx
├── NotFound.tsx
├── TabBarButton.tsx
├── ChangeAppIcon.tsx
├── SpeakerCard.tsx
├── OfflineBanner.tsx
├── TimeZoneSwitch.tsx
├── Themed.tsx
├── VenueInfo.tsx
├── OrganizersInfo.tsx
├── SearchInput.tsx
├── SpeakerImage.tsx
├── Bookmark.tsx
├── MiniTalkCard.tsx
├── AnimatedBootSplash.tsx
├── ReactConfHeader.tsx
├── ExpoImageDemo.tsx
├── TalkCard.tsx
└── SponsorsInfo.tsx
├── patches
├── expo-modules-core+1.12.10.patch
├── expo-quick-actions+2.0.0.patch
└── react-native-bootsplash+6.0.0-beta.6.patch
├── eas.json
├── store
├── bookmarkStore.ts
└── reactConfStore.ts
├── app
├── (tabs)
│ ├── speakers
│ │ ├── _layout.tsx
│ │ └── index.tsx
│ ├── info.tsx
│ ├── bookmarks.tsx
│ └── _layout.tsx
├── secretModal.tsx
├── speaker
│ └── [speakerId].tsx
└── _layout.tsx
├── theme.ts
├── package.json
├── README.md
└── app.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | ios
2 | android
3 | lib
--------------------------------------------------------------------------------
/.fingerprintignore:
--------------------------------------------------------------------------------
1 | node_modules/sharp/**/*
2 |
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/icon.png
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/favicon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/splash.png
--------------------------------------------------------------------------------
/assets/icons/gift.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/icons/gift.png
--------------------------------------------------------------------------------
/assets/images/desert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/desert.png
--------------------------------------------------------------------------------
/assets/images/hotel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/hotel.png
--------------------------------------------------------------------------------
/assets/bootsplash/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/logo.png
--------------------------------------------------------------------------------
/assets/bootsplash/brand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/brand.png
--------------------------------------------------------------------------------
/assets/bootsplash/logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/logo@2x.png
--------------------------------------------------------------------------------
/assets/bootsplash/logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/logo@3x.png
--------------------------------------------------------------------------------
/assets/bootsplash/logo@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/logo@4x.png
--------------------------------------------------------------------------------
/assets/icons/icon-desert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/icons/icon-desert.png
--------------------------------------------------------------------------------
/assets/images/react-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/react-blue.png
--------------------------------------------------------------------------------
/assets/images/react-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/react-green.png
--------------------------------------------------------------------------------
/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/react-logo.png
--------------------------------------------------------------------------------
/assets/bootsplash/brand@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/brand@2x.png
--------------------------------------------------------------------------------
/assets/bootsplash/brand@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/brand@3x.png
--------------------------------------------------------------------------------
/assets/bootsplash/brand@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/brand@4x.png
--------------------------------------------------------------------------------
/assets/bootsplash/logo@1,5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/logo@1,5x.png
--------------------------------------------------------------------------------
/assets/images/react-orange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/react-orange.png
--------------------------------------------------------------------------------
/assets/bootsplash/brand@1,5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/brand@1,5x.png
--------------------------------------------------------------------------------
/assets/icon-android-foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/icon-android-foreground.png
--------------------------------------------------------------------------------
/assets/images/react-dark-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/react-dark-blue.png
--------------------------------------------------------------------------------
/assets/images/reactlogo-cyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/reactlogo-cyan.png
--------------------------------------------------------------------------------
/assets/images/reactlogo-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/reactlogo-white.png
--------------------------------------------------------------------------------
/assets/images/sponsor-abbott.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/sponsor-abbott.png
--------------------------------------------------------------------------------
/assets/images/sponsor-amazon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/sponsor-amazon.png
--------------------------------------------------------------------------------
/assets/images/sponsor-amazon.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/images/sponsor-amazon.webp
--------------------------------------------------------------------------------
/consts.ts:
--------------------------------------------------------------------------------
1 | export const COLLAPSED_HEADER = 55;
2 | export const EXPANDED_HEADER = 176;
3 | export const ROW_HEIGHT = 55;
4 |
--------------------------------------------------------------------------------
/declarations.d.ts:
--------------------------------------------------------------------------------
1 | declare module "react-native-dynamic-app-icon" {
2 | function setAppIcon(iconIndex: stirng): void;
3 | }
4 |
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProBlack-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProBlack-Italic.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProBlack-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProBlack-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProBold-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProBold-Italic.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProBold-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProBold-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProBook-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProBook-Italic.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProBook-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProBook-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProLight-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProLight-Italic.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProLight-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProLight-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProMedium-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProMedium-Italic.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProMedium-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProMedium-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProSemibold-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProSemibold-Italic.ttf
--------------------------------------------------------------------------------
/assets/fonts/FreightSansProSemibold-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/fonts/FreightSansProSemibold-Regular.ttf
--------------------------------------------------------------------------------
/assets/bootsplash/android/drawable-hdpi/bootsplash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/android/drawable-hdpi/bootsplash_logo.png
--------------------------------------------------------------------------------
/assets/bootsplash/android/drawable-mdpi/bootsplash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/android/drawable-mdpi/bootsplash_logo.png
--------------------------------------------------------------------------------
/assets/bootsplash/android/drawable-hdpi/bootsplash_brand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/android/drawable-hdpi/bootsplash_brand.png
--------------------------------------------------------------------------------
/assets/bootsplash/android/drawable-mdpi/bootsplash_brand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/android/drawable-mdpi/bootsplash_brand.png
--------------------------------------------------------------------------------
/assets/bootsplash/android/drawable-xhdpi/bootsplash_brand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/android/drawable-xhdpi/bootsplash_brand.png
--------------------------------------------------------------------------------
/assets/bootsplash/android/drawable-xhdpi/bootsplash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/android/drawable-xhdpi/bootsplash_logo.png
--------------------------------------------------------------------------------
/assets/bootsplash/android/drawable-xxhdpi/bootsplash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/android/drawable-xxhdpi/bootsplash_logo.png
--------------------------------------------------------------------------------
/lib/react-compiler-runtime/README.md:
--------------------------------------------------------------------------------
1 | # Warning: do not use this!
2 |
3 | It is not meant for usage outside of this React Conf app, and it should not be depended on in your app.
--------------------------------------------------------------------------------
/assets/bootsplash/android/drawable-xxhdpi/bootsplash_brand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/android/drawable-xxhdpi/bootsplash_brand.png
--------------------------------------------------------------------------------
/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_brand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_brand.png
--------------------------------------------------------------------------------
/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/android/drawable-xxxhdpi/bootsplash_logo.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": [
7 | "./*"
8 | ]
9 | }
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // https://docs.expo.dev/guides/using-eslint/
2 | module.exports = {
3 | extends: ["expo", "prettier"],
4 | plugins: ["prettier"],
5 | rules: {
6 | "prettier/prettier": "error",
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-adxe67.imageset/logo-adxe67.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-adxe67.imageset/logo-adxe67.png
--------------------------------------------------------------------------------
/assets/bootsplash/ios/Images.xcassets/BootSplashBrand-adxe67.imageset/brand-adxe67.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/ios/Images.xcassets/BootSplashBrand-adxe67.imageset/brand-adxe67.png
--------------------------------------------------------------------------------
/assets/bootsplash/ios/Images.xcassets/BootSplashBrand-adxe67.imageset/brand-adxe67@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/ios/Images.xcassets/BootSplashBrand-adxe67.imageset/brand-adxe67@2x.png
--------------------------------------------------------------------------------
/assets/bootsplash/ios/Images.xcassets/BootSplashBrand-adxe67.imageset/brand-adxe67@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/ios/Images.xcassets/BootSplashBrand-adxe67.imageset/brand-adxe67@3x.png
--------------------------------------------------------------------------------
/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-adxe67.imageset/logo-adxe67@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-adxe67.imageset/logo-adxe67@2x.png
--------------------------------------------------------------------------------
/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-adxe67.imageset/logo-adxe67@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sync/react-conf-app/main/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-adxe67.imageset/logo-adxe67@3x.png
--------------------------------------------------------------------------------
/assets/bootsplash/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "background": "#091725",
3 | "logo": {
4 | "width": 192,
5 | "height": 192
6 | },
7 | "brand": {
8 | "bottom": 76.5,
9 | "width": 150,
10 | "height": 47
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/lib/react-compiler-runtime/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-forget-runtime",
3 | "version": "0.0.1",
4 | "description": "Runtime for React Forget",
5 | "license": "MIT",
6 | "main": "index.js",
7 | "dependencies": {
8 | "react": "^18.2.0"
9 | }
10 | }
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ["babel-preset-expo"],
5 | plugins: [
6 | [
7 | "babel-plugin-react-compiler",
8 | {
9 | runtimeModule: "react-compiler-runtime",
10 | },
11 | ],
12 | ],
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/assets/images/x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/bootsplash/ios/Colors.xcassets/BootSplashBackground-adxe67.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors": [
3 | {
4 | "idiom": "universal",
5 | "color": {
6 | "color-space": "srgb",
7 | "components": {
8 | "blue": "0.145098039215686",
9 | "green": "0.0901960784313725",
10 | "red": "0.0352941176470588",
11 | "alpha": "1.000"
12 | }
13 | }
14 | }
15 | ],
16 | "info": {
17 | "author": "xcode",
18 | "version": 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/assets/bootsplash/ios/Images.xcassets/BootSplashLogo-adxe67.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "idiom": "universal",
5 | "filename": "logo-adxe67.png",
6 | "scale": "1x"
7 | },
8 | {
9 | "idiom": "universal",
10 | "filename": "logo-adxe67@2x.png",
11 | "scale": "2x"
12 | },
13 | {
14 | "idiom": "universal",
15 | "filename": "logo-adxe67@3x.png",
16 | "scale": "3x"
17 | }
18 | ],
19 | "info": {
20 | "author": "xcode",
21 | "version": 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/assets/bootsplash/ios/Images.xcassets/BootSplashBrand-adxe67.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images": [
3 | {
4 | "idiom": "universal",
5 | "filename": "brand-adxe67.png",
6 | "scale": "1x"
7 | },
8 | {
9 | "idiom": "universal",
10 | "filename": "brand-adxe67@2x.png",
11 | "scale": "2x"
12 | },
13 | {
14 | "idiom": "universal",
15 | "filename": "brand-adxe67@3x.png",
16 | "scale": "3x"
17 | }
18 | ],
19 | "info": {
20 | "author": "xcode",
21 | "version": 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/utils/useAppStateEffect.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { AppState, AppStateStatus } from "react-native";
3 |
4 | export function useAppStateEffect(callback: (state: AppStateStatus) => void) {
5 | useEffect(() => {
6 | function onChange(newState: AppStateStatus) {
7 | callback(newState);
8 | }
9 |
10 | const subscription = AppState.addEventListener("change", onChange);
11 |
12 | // Fire initial state
13 | onChange(AppState.currentState);
14 |
15 | return () => {
16 | subscription.remove();
17 | };
18 | }, [callback]);
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | # ignoring as we're using CNG
38 | /ios
39 | /android
40 |
--------------------------------------------------------------------------------
/scripts/syncApi.js:
--------------------------------------------------------------------------------
1 | const fs = require("node:fs");
2 |
3 | async function fetchAndSync(url, filePath) {
4 | const result = await fetch(url);
5 | const data = await result.json();
6 |
7 | fs.writeFile(filePath, JSON.stringify(data, null, " "), (err) => {
8 | if (err) {
9 | console.info(`❌ error updating ${filePath}`);
10 | console.error(err);
11 | } else {
12 | console.info(`✅ ${filePath} updated`);
13 | }
14 | });
15 | }
16 |
17 | (async () => {
18 | await fetchAndSync(
19 | "https://sessionize.com/api/v2/ctta9bhe/view/All",
20 | "./data/allSessions.json",
21 | );
22 | })();
23 |
--------------------------------------------------------------------------------
/lib/react-compiler-runtime/index.js:
--------------------------------------------------------------------------------
1 | // lib/react-compiler-runtime.js
2 | const $empty = Symbol.for("react.memo_cache_sentinel");
3 | /**
4 | * DANGER: this hook is NEVER meant to be called directly!
5 | *
6 | * Note that this is a temporary userspace implementation of this function
7 | * from React 19. It is not as efficient and may invalidate more frequently
8 | * than the official API. Please upgrade to React 19 as soon as you can.
9 | **/
10 | export function c(size: number) {
11 | return React.useState(() => {
12 | const $ = new Array(size);
13 | for (let ii = 0; ii < size; ii++) {
14 | $[ii] = $empty;
15 | }
16 | // @ts-ignore
17 | $[$empty] = true;
18 | return $;
19 | })[0];
20 | }
21 |
--------------------------------------------------------------------------------
/utils/openWebBrowserAsync.ts:
--------------------------------------------------------------------------------
1 | import * as WebBrowser from "expo-web-browser";
2 | import { Appearance } from "react-native";
3 |
4 | import { theme } from "@/theme";
5 |
6 | export default function openBrowserAsync(url: string) {
7 | const colorScheme = Appearance.getColorScheme();
8 |
9 | WebBrowser.openBrowserAsync(url, {
10 | enableBarCollapsing: true,
11 | ...(colorScheme === "dark"
12 | ? {
13 | // Optional: we could match this to theme
14 | toolbarColor: theme.colorDarkestBlue,
15 | controlsColor: "#fff",
16 | }
17 | : {
18 | // Optional: we could match this to theme
19 | controlsColor: theme.colorReactDarkBlue,
20 | }),
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/pr-preview.yml:
--------------------------------------------------------------------------------
1 | on: [pull_request]
2 | jobs:
3 | preview:
4 | runs-on: ubuntu-latest
5 | steps:
6 | - name: 🏗 Setup repo
7 | uses: actions/checkout@v3
8 |
9 | - name: 🏗 Setup Node
10 | uses: actions/setup-node@v3
11 | with:
12 | node-version: 18.x
13 | cache: yarn
14 |
15 | - name: 🏗 Setup EAS
16 | uses: expo/expo-github-action@v8
17 | with:
18 | eas-version: latest
19 | token: ${{ secrets.EXPO_TOKEN }}
20 |
21 | - name: 📦 Install dependencies
22 | run: yarn install
23 |
24 | - name: 🚀 Create preview
25 | uses: expo/expo-github-action/preview@v8
26 | with:
27 | command: eas update --auto
28 |
--------------------------------------------------------------------------------
/assets/images/linkedin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/types.ts:
--------------------------------------------------------------------------------
1 | import { allSessions } from "@/utils/testData/allSessions";
2 |
3 | export type Session = {
4 | id: string;
5 | title: string;
6 | description: string | null;
7 | startsAt: string;
8 | endsAt: string;
9 | speakers: Speaker[];
10 | room: string;
11 | isServiceSession: boolean;
12 | };
13 |
14 | export type Speaker = {
15 | id: string;
16 | firstName: string;
17 | lastName: string;
18 | bio: string | null;
19 | tagLine: string | null;
20 | profilePicture: string | null;
21 | links: { title: string; url: string; linkType: string }[];
22 | sessions: number[];
23 | fullName: string;
24 | categoryItems: number[];
25 | };
26 |
27 | export type ApiAllSessions = typeof allSessions;
28 |
29 | export type ApiSpeaker = (typeof allSessions)["speakers"][number];
30 |
--------------------------------------------------------------------------------
/components/DiscordInfo.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "./Button";
2 | import { InfoSection } from "./InfoSection";
3 | import { ThemedText } from "./Themed";
4 | import * as Linking from "expo-linking";
5 |
6 | import { theme } from "@/theme";
7 |
8 | export function DiscordInfo() {
9 | const handlePress = () => {
10 | Linking.openURL("https://discord.gg/reactconf");
11 | };
12 |
13 | return (
14 |
15 |
16 | Chat with other folks at the conference on the React Conf 2024 Discord
17 | server. Coordinate around ridesharing and external activities.
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/components/PoweredByExpo.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet } from "react-native";
2 |
3 | import { ThemedText } from "./Themed";
4 | import { theme } from "@/theme";
5 | import { openBrowserAsync } from "expo-web-browser";
6 | import { TouchableOpacity } from "react-native-gesture-handler";
7 |
8 | export function PoweredByExpo() {
9 | return (
10 |
11 | openBrowserAsync("https://expo.dev")}
14 | >
15 | Powered by Expo
16 |
17 |
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | justifyContent: "center",
24 | alignItems: "center",
25 | paddingBottom: theme.space12 * 2,
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/utils/registerForPushNotificationsAsync.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from "react-native";
2 | import * as Device from "expo-device";
3 | import * as Notifications from "expo-notifications";
4 |
5 | export async function registerForPushNotificationsAsync() {
6 | if (Platform.OS === "android") {
7 | await Notifications.setNotificationChannelAsync("default", {
8 | name: "default",
9 | importance: Notifications.AndroidImportance.DEFAULT,
10 | vibrationPattern: [0, 250, 250, 250],
11 | showBadge: false,
12 | });
13 | }
14 |
15 | if (Device.isDevice) {
16 | const { status: existingStatus } =
17 | await Notifications.getPermissionsAsync();
18 | if (existingStatus !== "granted") {
19 | const { status } = await Notifications.requestPermissionsAsync();
20 | return status;
21 | } else {
22 | return existingStatus;
23 | }
24 | } else {
25 | return null;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/components/LiveStreamInfo.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "./Button";
2 | import { InfoSection } from "./InfoSection";
3 | import { ThemedText } from "./Themed";
4 | import openWebBrowserAsync from "@/utils/openWebBrowserAsync";
5 |
6 | import { theme } from "@/theme";
7 |
8 | export function LiveStreamInfo() {
9 | const handlePress = () => {
10 | openWebBrowserAsync(
11 | "https://ti.to/reactconf/2024/with/free-livestream-access",
12 | );
13 | };
14 | return (
15 |
16 |
21 | Free Livestream Access
22 |
23 |
24 | Join React Conf 2024 from anywhere with our Free Livestream Access!
25 | Watch all the talks remotely.
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/patches/expo-modules-core+1.12.10.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/expo-modules-core/ios/ReactDelegates/RCTAppDelegate+Recreate.mm b/node_modules/expo-modules-core/ios/ReactDelegates/RCTAppDelegate+Recreate.mm
2 | index 18957b4..8a8b483 100644
3 | --- a/node_modules/expo-modules-core/ios/ReactDelegates/RCTAppDelegate+Recreate.mm
4 | +++ b/node_modules/expo-modules-core/ios/ReactDelegates/RCTAppDelegate+Recreate.mm
5 | @@ -38,6 +38,7 @@ - (UIView *)recreateRootViewWithBundleURL:(nullable NSURL *)bundleURL
6 | // we don't want to loop the ReactDelegate again. Otherwise it will be infinite loop.
7 | EXReactRootViewFactory *factory = (EXReactRootViewFactory *)rootViewFactory;
8 | rootView = [factory superViewWithModuleName:self.moduleName initialProperties:self.initialProps launchOptions:launchOptions];
9 | + [self customizeRootView:(RCTRootView *)rootView];
10 | } else {
11 | rootView = [rootViewFactory viewWithModuleName:self.moduleName initialProperties:self.initialProps launchOptions:launchOptions];
12 | }
13 |
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 7.6.2",
4 | "appVersionSource": "remote",
5 | "promptToConfigurePushNotifications": false
6 | },
7 | "build": {
8 | "localdev": {
9 | "developmentClient": true,
10 | "distribution": "internal",
11 | "ios": {
12 | "simulator": true
13 | },
14 | "env": {
15 | "APP_VARIANT": "development"
16 | }
17 | },
18 | "development": {
19 | "developmentClient": true,
20 | "distribution": "internal",
21 | "env": {
22 | "APP_VARIANT": "development"
23 | }
24 | },
25 | "preview": {
26 | "distribution": "internal",
27 | "env": {
28 | "APP_VARIANT": "preview"
29 | },
30 | "channel": "preview"
31 | },
32 | "production": {
33 | "autoIncrement": true,
34 | "resourceClass": "large",
35 | "env": {
36 | "APP_VARIANT": "production"
37 | },
38 | "channel": "production"
39 | }
40 | },
41 | "submit": {
42 | "production": {}
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/components/BackButton.tsx:
--------------------------------------------------------------------------------
1 | import AntDesign from "@expo/vector-icons/build/AntDesign";
2 | import Entypo from "@expo/vector-icons/build/Entypo";
3 | import { useRouter } from "expo-router";
4 | import { Platform, StyleSheet } from "react-native";
5 | import { TouchableOpacity } from "react-native-gesture-handler";
6 |
7 | import { useThemeColor } from "./Themed";
8 |
9 | import { theme } from "@/theme";
10 |
11 | export function BackButton() {
12 | const router = useRouter();
13 | const color = useThemeColor({
14 | light: theme.colorBlack,
15 | dark: theme.colorWhite,
16 | });
17 | return (
18 |
19 | {Platform.OS === "ios" ? (
20 |
21 | ) : (
22 |
23 | )}
24 |
25 | );
26 | }
27 |
28 | const styles = StyleSheet.create({
29 | button: {
30 | padding: 10,
31 | marginLeft: -10,
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/components/PressableArea.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, forwardRef } from "react";
2 | import { Pressable, PressableProps } from "react-native";
3 | import Animated, {
4 | useAnimatedStyle,
5 | useSharedValue,
6 | withTiming,
7 | } from "react-native-reanimated";
8 |
9 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
10 |
11 | // eslint-disable-next-line react/display-name
12 | export const PressableArea = forwardRef(
13 | (props: PropsWithChildren, ref: React.Ref) => {
14 | const opacity = useSharedValue(1);
15 |
16 | const animatedStyle = useAnimatedStyle(() => ({
17 | opacity: opacity.value,
18 | }));
19 |
20 | return (
21 | {
26 | opacity.value = withTiming(0.75, { duration: 150 });
27 | }}
28 | onPressOut={() => {
29 | opacity.value = withTiming(1, { duration: 150 });
30 | }}
31 | />
32 | );
33 | },
34 | );
35 |
--------------------------------------------------------------------------------
/components/Tags.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet } from "react-native";
2 |
3 | import { ThemedText, ThemedView } from "./Themed";
4 | import { theme } from "../theme";
5 |
6 | export function Tags({ tags }: { tags: string[] }) {
7 | if (!tags.length) {
8 | return null;
9 | }
10 |
11 | return (
12 |
13 | {tags.map((tag) => (
14 |
20 | {tag}
21 |
22 | ))}
23 |
24 | );
25 | }
26 |
27 | const styles = StyleSheet.create({
28 | container: {
29 | marginBottom: theme.space16,
30 | flexDirection: "row",
31 | flexWrap: "wrap",
32 | },
33 | tag: {
34 | borderRadius: theme.borderRadius20,
35 | paddingVertical: theme.space4,
36 | paddingHorizontal: theme.space8,
37 | marginRight: theme.space8,
38 | marginBottom: theme.space8,
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/components/InfoSection.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, View } from "react-native";
2 |
3 | import { ThemedText, ThemedView } from "./Themed";
4 |
5 | import { theme } from "@/theme";
6 |
7 | export function InfoSection({
8 | title,
9 | children,
10 | }: {
11 | title: string;
12 | children: React.ReactElement | React.ReactElement[];
13 | }) {
14 | return (
15 |
16 |
17 | {title}
18 |
19 |
24 | {children}
25 |
26 |
27 | );
28 | }
29 |
30 | const styles = StyleSheet.create({
31 | container: {
32 | paddingVertical: theme.space24,
33 | paddingHorizontal: theme.space16,
34 | },
35 | heading: {
36 | marginBottom: theme.space12,
37 | marginLeft: theme.space16,
38 | marginTop: theme.space24,
39 | },
40 | infoContainer: {
41 | marginBottom: theme.space16,
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/assets/images/sponsor-vercel.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/components/Heading.tsx:
--------------------------------------------------------------------------------
1 | import { Image, View, StyleSheet } from "react-native";
2 |
3 | import { ThemedText } from "./Themed";
4 | import { theme } from "../theme";
5 |
6 | const DESERT_SIZE = 100;
7 |
8 | export function Heading() {
9 | return (
10 |
11 |
15 |
16 |
21 | REACT CONF
22 |
23 |
24 | May 15th - 16th 2024
25 |
26 | Henderson, Nevada
27 |
28 |
29 | );
30 | }
31 |
32 | const styles = StyleSheet.create({
33 | image: {
34 | width: DESERT_SIZE,
35 | height: DESERT_SIZE,
36 | },
37 | heading: {
38 | flexDirection: "row",
39 | justifyContent: "space-evenly",
40 | alignItems: "center",
41 | marginBottom: theme.space12,
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/store/bookmarkStore.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from "@react-native-async-storage/async-storage";
2 | import { create } from "zustand";
3 | import { persist, createJSONStorage } from "zustand/middleware";
4 |
5 | type BookmarkState = {
6 | bookmarks: { sessionId: string; notificationId?: string }[];
7 | toggleBookmarked: (sessionId: string, notificationId?: string) => void;
8 | };
9 |
10 | export const useBookmarkStore = create(
11 | persist(
12 | (set) => ({
13 | bookmarks: [],
14 | toggleBookmarked: (sessionId: string, notificationId?: string) => {
15 | set((state) => {
16 | if (state.bookmarks.find((b) => b.sessionId === sessionId)) {
17 | const newBookmarks = state.bookmarks.filter(
18 | (b) => b.sessionId !== sessionId,
19 | );
20 | return {
21 | bookmarks: newBookmarks,
22 | };
23 | } else {
24 | return {
25 | bookmarks: [...state.bookmarks, { sessionId, notificationId }],
26 | };
27 | }
28 | });
29 | },
30 | }),
31 | {
32 | name: "react-conf-2024-bookmarks",
33 | storage: createJSONStorage(() => AsyncStorage),
34 | },
35 | ),
36 | );
37 |
--------------------------------------------------------------------------------
/app/(tabs)/speakers/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { ThemedText, useThemeColor } from "@/components/Themed";
2 | import { theme } from "@/theme";
3 | import { Stack, useRouter } from "expo-router";
4 |
5 | export default function Layout() {
6 | const router = useRouter();
7 | const tabBarBackgroundColor = useThemeColor({
8 | light: theme.colorWhite,
9 | dark: theme.colorDarkestBlue,
10 | });
11 | const tabBarTintColor = useThemeColor({
12 | light: theme.colorReactDarkBlue,
13 | dark: theme.colorWhite,
14 | });
15 |
16 | return (
17 |
18 | (
25 |
26 | Speakers
27 |
28 | ),
29 |
30 | headerSearchBarOptions: {
31 | headerIconColor: tabBarTintColor,
32 | tintColor: tabBarTintColor,
33 | textColor: tabBarTintColor,
34 | hintTextColor: tabBarTintColor,
35 | onChangeText: (event) => {
36 | router.setParams({
37 | q: event.nativeEvent.text,
38 | });
39 | },
40 | },
41 | }}
42 | />
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/components/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 |
3 | import { ThemedView, useThemeColor } from "./Themed";
4 | import { TouchableOpacity } from "react-native-gesture-handler";
5 |
6 | import { theme } from "@/theme";
7 |
8 | export function IconButton({
9 | onPress,
10 | children,
11 | isActive,
12 | }: {
13 | onPress: () => void;
14 | children: React.ReactElement;
15 | isActive?: boolean;
16 | }) {
17 | const backgroundColor = useThemeColor({
18 | light: theme.colorWhite,
19 | dark: `rgba(255, 255, 255, 0.15)`,
20 | });
21 | const backgroundColorActive = useThemeColor({
22 | light: theme.colorReactLightBlue,
23 | dark: theme.colorReactLightBlue,
24 | });
25 | const shadow = useThemeColor({ light: theme.dropShadow, dark: undefined });
26 |
27 | return (
28 |
29 |
38 | {children}
39 |
40 |
41 | );
42 | }
43 |
44 | const styles = StyleSheet.create({
45 | button: {
46 | padding: theme.space12,
47 | borderRadius: theme.borderRadius6,
48 | marginHorizontal: theme.space8,
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/components/BuildDetails.tsx:
--------------------------------------------------------------------------------
1 | import * as Application from "expo-application";
2 | import { View, StyleSheet } from "react-native";
3 |
4 | import { ThemedText } from "./Themed";
5 |
6 | import { useReactConfStore } from "@/store/reactConfStore";
7 | import { theme } from "@/theme";
8 | import { formatFullDate } from "@/utils/formatDate";
9 | import { useUpdates } from "expo-updates";
10 |
11 | export function BuildDetails() {
12 | const lastRefreshed = useReactConfStore((state) => state.lastRefreshed);
13 | const updates = useUpdates();
14 |
15 | const currentUpdateId = updates?.currentlyRunning?.updateId;
16 |
17 | return (
18 |
19 |
20 | v{Application.nativeApplicationVersion} (
21 | {Application.nativeBuildVersion})
22 |
23 |
24 | Schedule last refreshed:{" "}
25 | {lastRefreshed ? formatFullDate(lastRefreshed) : "Never"}
26 |
27 | {currentUpdateId ? (
28 |
29 | {currentUpdateId}
30 |
31 | ) : null}
32 |
33 | );
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | container: {
38 | justifyContent: "center",
39 | alignItems: "center",
40 | paddingTop: theme.space12,
41 | paddingBottom: theme.space8,
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/components/ActivityCard.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, View } from "react-native";
2 |
3 | import { ThemedText, ThemedView } from "./Themed";
4 |
5 | import { useReactConfStore } from "@/store/reactConfStore";
6 | import { theme } from "@/theme";
7 | import { Session } from "@/types";
8 | import { formatSessionTime } from "@/utils/formatDate";
9 |
10 | type Props = {
11 | session: Session;
12 | };
13 |
14 | export function ActivityCard({ session }: Props) {
15 | const shouldUseLocalTz = useReactConfStore((state) => state.shouldUseLocalTz);
16 |
17 | return (
18 |
19 |
20 | {formatSessionTime(session, shouldUseLocalTz)}
21 |
22 |
23 |
24 | {session.title}
25 |
26 |
27 | {session.room}
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | const styles = StyleSheet.create({
35 | container: {
36 | marginHorizontal: theme.space16,
37 | marginBottom: theme.space16,
38 | paddingHorizontal: theme.space12,
39 | paddingVertical: theme.space8,
40 | borderRadius: theme.borderRadius10,
41 | },
42 | row: {
43 | flexDirection: "row",
44 | justifyContent: "space-between",
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/utils/useQuickActionCallback.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Action as QuickAction,
4 | initial as INITIAL_QUICK_ACTION,
5 | addListener,
6 | } from "expo-quick-actions";
7 |
8 | let _initialAction: QuickAction | undefined = INITIAL_QUICK_ACTION;
9 |
10 | function popInitialAction() {
11 | if (!_initialAction) {
12 | return;
13 | }
14 |
15 | let result = _initialAction;
16 | _initialAction = undefined;
17 | return result;
18 | }
19 |
20 | /**
21 | * Handle quick actions with a callback function. This prevents the entire
22 | * component from re-rendering when the action changes. Use `useQuickAction` if
23 | * you want to re-render the component.
24 | *
25 | * @param callback function that's called when a quick action launches the app.
26 | * Will be instantly called with the initial action if it exists.
27 | */
28 | export function useQuickActionCallback(
29 | callback: (data: QuickAction) => void | Promise,
30 | ) {
31 | React.useEffect(() => {
32 | let isMounted = true;
33 |
34 | // Only call the initial action once
35 | let initialAction = popInitialAction();
36 | if (initialAction) {
37 | callback(initialAction);
38 | }
39 |
40 | const sub = addListener((event) => {
41 | if (isMounted) {
42 | callback(event);
43 | }
44 | });
45 | return () => {
46 | isMounted = false;
47 | sub.remove();
48 | };
49 | }, [callback]);
50 | }
51 |
--------------------------------------------------------------------------------
/assets/images/sponsor-sentry.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/theme.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from "react-native";
2 |
3 | export const theme = {
4 | colorWhite: "#FFFFFF",
5 | colorBlack: "#051726",
6 | colorDarkBlue: "#051726",
7 | colorDarkestBlue: "#091725",
8 | colorLightGreen: "#9BDFB1",
9 | colorDarkGreen: "#1AC9A2",
10 | colorGrey: "#adb5bd",
11 | colorReactLightBlue: "#58C4DC",
12 | colorReactDarkBlue: "#087EA4",
13 | colorThemeLightGrey: "#FCFBFE",
14 | colorThemeGrey: "#F5F4F3",
15 |
16 | darkActiveContent: "rgba(255,255,255, 0.3)",
17 |
18 | lightActiveContent: "rgba(0,0,0, 0.1)",
19 |
20 | space4: 4,
21 | space8: 8,
22 | space12: 12,
23 | space16: 16,
24 | space24: 24,
25 |
26 | fontSize16: 16,
27 | fontSize18: 18,
28 | fontSize24: 24,
29 | fontSize32: 32,
30 |
31 | fontFamilyLight: "FreightSansProLight-Regular",
32 | fontFamilyLightItalic: "FreightSansProLight-Italic",
33 | fontFamily: "FreightSansProBook-Regular",
34 | fontFamilyItalic: "FreightSansProBook-Italic",
35 | fontFamilyBold: "FreightSansProBold-Regular",
36 | fontFamilyBoldItalic: "FreightSansProBold-Italic",
37 |
38 | borderRadius6: 6,
39 | borderRadius10: 10,
40 | borderRadius20: 20,
41 |
42 | dropShadow: {
43 | ...Platform.select({
44 | ios: {
45 | shadowColor: "#adb5bd",
46 | shadowOffset: {
47 | width: 0,
48 | height: 0,
49 | },
50 | shadowOpacity: 0.4,
51 | shadowRadius: 2,
52 | },
53 | default: {},
54 | }),
55 | },
56 | };
57 |
--------------------------------------------------------------------------------
/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, ActivityIndicator } from "react-native";
2 | import { TouchableOpacity } from "react-native-gesture-handler";
3 |
4 | import { ThemedText, useThemeColor } from "./Themed";
5 |
6 | import { theme } from "@/theme";
7 |
8 | const buttonTextSize = 22;
9 |
10 | export function Button({
11 | title,
12 | onPress,
13 | isLoading,
14 | }: {
15 | title: string;
16 | onPress: () => void;
17 | isLoading?: boolean;
18 | }) {
19 | const shadow = useThemeColor({ light: theme.dropShadow, dark: undefined });
20 |
21 | return (
22 |
27 | {isLoading ? (
28 |
29 | ) : (
30 |
35 | {title}
36 |
37 | )}
38 |
39 | );
40 | }
41 |
42 | const styles = StyleSheet.create({
43 | text: {
44 | color: theme.colorWhite,
45 | lineHeight: buttonTextSize,
46 | },
47 | button: {
48 | paddingVertical: theme.space8,
49 | paddingHorizontal: theme.space24,
50 | borderRadius: theme.borderRadius6,
51 | backgroundColor: theme.colorReactDarkBlue,
52 | minWidth: 150,
53 | minHeight: 40,
54 | alignItems: "center",
55 | justifyContent: "center",
56 | },
57 | });
58 |
--------------------------------------------------------------------------------
/app/(tabs)/info.tsx:
--------------------------------------------------------------------------------
1 | import { useScrollToTop } from "@react-navigation/native";
2 | import React from "react";
3 | import { StyleSheet } from "react-native";
4 |
5 | import { BuildDetails } from "@/components/BuildDetails";
6 | import { LiveStreamInfo } from "@/components/LiveStreamInfo";
7 | import { OrganizersInfo } from "@/components/OrganizersInfo";
8 | import { SponsorsInfo } from "@/components/SponsorsInfo";
9 | import { DiscordInfo } from "@/components/DiscordInfo";
10 | import { PoweredByExpo } from "@/components/PoweredByExpo";
11 | import { ThemedView, useThemeColor } from "@/components/Themed";
12 | import { VenueInfo } from "@/components/VenueInfo";
13 | import { theme } from "@/theme";
14 | import { ScrollView } from "react-native-gesture-handler";
15 |
16 | export default function Info() {
17 | const backgroundColor = useThemeColor({
18 | light: theme.colorWhite,
19 | dark: theme.colorDarkBlue,
20 | });
21 | const ref = React.useRef(null);
22 | useScrollToTop(ref);
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
39 | const styles = StyleSheet.create({
40 | container: {
41 | flex: 1,
42 | },
43 | scrollView: {
44 | flex: 1,
45 | },
46 | });
47 |
--------------------------------------------------------------------------------
/components/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import { Image } from "expo-image";
2 | import { StyleSheet } from "react-native";
3 |
4 | import { Button } from "./Button";
5 | import { ThemedText, ThemedView, useThemeColor } from "./Themed";
6 |
7 | import { useReactConfStore } from "@/store/reactConfStore";
8 | import { theme } from "@/theme";
9 |
10 | export function NotFound({ message }: { message: string }) {
11 | const refetch = useReactConfStore((state) => state.refreshData);
12 | const isRefetching = useReactConfStore((state) => state.isRefreshing);
13 | const iconColor = useThemeColor({
14 | light: theme.colorGrey,
15 | dark: theme.colorWhite,
16 | });
17 | return (
18 |
23 |
24 | {message}
25 |
26 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | const styles = StyleSheet.create({
38 | container: {
39 | flex: 1,
40 | justifyContent: "center",
41 | alignItems: "center",
42 | padding: theme.space24,
43 | },
44 | image: {
45 | width: 100,
46 | height: 100,
47 | marginBottom: theme.space24 * 2,
48 | },
49 | heading: {
50 | marginBottom: theme.space24,
51 | },
52 | });
53 |
--------------------------------------------------------------------------------
/utils/sessions.ts:
--------------------------------------------------------------------------------
1 | import { ApiAllSessions, Session, Speaker } from "@/types";
2 | // @ts-ignore
3 | import { allTalks } from "@/utils/testData/allSessions";
4 | import { isDayOneSession, isDayTwoSession } from "./formatDate";
5 |
6 | export const formatSessions = (talks: ApiAllSessions): Session[][] => {
7 | const allSessions = talks.sessions.map((talk) => ({
8 | id: talk.id,
9 | title: talk.title,
10 | description: talk.description,
11 | startsAt: talk.startsAt,
12 | endsAt: talk.endsAt,
13 | isServiceSession: talk.isServiceSession,
14 | speakers: (talk.speakers
15 | ?.map((speakerId) => talks.speakers.find((sp) => sp.id === speakerId))
16 | .filter(Boolean) || []) as Speaker[],
17 | room: talks.rooms.find((room) => room.id === talk.roomId)?.name || "...",
18 | }));
19 |
20 | const dayOne = allSessions.filter((session) =>
21 | isDayOneSession(session.startsAt),
22 | );
23 |
24 | const dayTwo = allSessions.filter((session) =>
25 | isDayTwoSession(session.startsAt),
26 | );
27 |
28 | return [dayOne, dayTwo];
29 | };
30 |
31 | export const formatSession = (
32 | talk: ApiAllSessions["sessions"][number],
33 | talks: typeof allTalks,
34 | ): Session => {
35 | return {
36 | id: talk.id,
37 | title: talk.title,
38 | description: talk.description,
39 | startsAt: talk.startsAt,
40 | endsAt: talk.endsAt,
41 | isServiceSession: talk.isServiceSession,
42 | speakers: (talk.speakers
43 | // @ts-ignore
44 | ?.map((speakerId) => talks.speakers.find((sp) => sp.id === speakerId))
45 | .filter(Boolean) || []) as Speaker[],
46 | // @ts-ignore
47 | room: talks.rooms.find((room) => room.id === talk.roomId)?.name || "...",
48 | };
49 | };
50 |
--------------------------------------------------------------------------------
/components/TabBarButton.tsx:
--------------------------------------------------------------------------------
1 | import * as Haptics from "expo-haptics";
2 | import {
3 | AccessibilityState,
4 | GestureResponderEvent,
5 | Platform,
6 | Pressable,
7 | StyleSheet,
8 | } from "react-native";
9 | import Animated, {
10 | useAnimatedStyle,
11 | useSharedValue,
12 | withSpring,
13 | } from "react-native-reanimated";
14 |
15 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
16 |
17 | interface TabBarButtonProps {
18 | icon: React.FC<{ color: string }>;
19 | onPress?: (e: GestureResponderEvent) => void;
20 | accessibilityState?: AccessibilityState;
21 | activeTintColor: string;
22 | inactiveTintColor: string;
23 | }
24 |
25 | export function TabBarButton({
26 | icon,
27 | onPress,
28 | accessibilityState,
29 | activeTintColor,
30 | inactiveTintColor,
31 | }: TabBarButtonProps) {
32 | const focused = accessibilityState?.selected;
33 | const color = focused ? activeTintColor : inactiveTintColor;
34 | const scale = useSharedValue(1);
35 |
36 | const animatedStyle = useAnimatedStyle(() => ({
37 | transform: [{ scale: scale.value }],
38 | }));
39 |
40 | return (
41 | {
43 | if (Platform.OS !== "web") {
44 | Haptics.selectionAsync();
45 | }
46 | onPress?.(e);
47 | }}
48 | onPressIn={() => {
49 | scale.value = withSpring(0.92);
50 | }}
51 | onPressOut={() => {
52 | scale.value = withSpring(1);
53 | }}
54 | style={[styles.pressable, animatedStyle]}
55 | >
56 | {icon({ color })}
57 |
58 | );
59 | }
60 |
61 | const styles = StyleSheet.create({
62 | pressable: {
63 | flex: 1,
64 | justifyContent: "center",
65 | alignItems: "center",
66 | },
67 | });
68 |
--------------------------------------------------------------------------------
/utils/formatDate.ts:
--------------------------------------------------------------------------------
1 | import { formatDate } from "date-fns";
2 | import { formatInTimeZone } from "date-fns-tz";
3 |
4 | import { Session } from "../types";
5 |
6 | const timeFormat = "h:mm aaa";
7 | const dateTimeFormat = `${timeFormat}, LLL d`;
8 | const fullDateFormat = `${timeFormat}, LLL d, yyyy`;
9 |
10 | export const formatSessionTime = (
11 | session: Session,
12 | shouldUseLocalTz: boolean,
13 | ) => {
14 | try {
15 | const startsAtDate = new Date(session.startsAt);
16 | const endsAtDate = new Date(session.endsAt);
17 |
18 | if (shouldUseLocalTz) {
19 | return `${formatDate(startsAtDate, dateTimeFormat)} - ${formatDate(endsAtDate, dateTimeFormat)}`;
20 | } else {
21 | return `${formatInTimeZone(startsAtDate, "America/Los_Angeles", timeFormat)} - ${formatInTimeZone(endsAtDate, "America/Los_Angeles", timeFormat)}`;
22 | }
23 | } catch {
24 | return "...";
25 | }
26 | };
27 |
28 | export const formatFullDate = (dateString: string) => {
29 | try {
30 | return formatDate(new Date(dateString), fullDateFormat);
31 | } catch {
32 | return "...";
33 | }
34 | };
35 |
36 | export const getCurrentTimezone = () => {
37 | try {
38 | return formatDate(new Date(), "zzzz");
39 | } catch {
40 | return "...";
41 | }
42 | };
43 |
44 | export const isDayOneSession = (date: string) => {
45 | try {
46 | return (
47 | formatInTimeZone(new Date(date), "America/Los_Angeles", `LLL d, yyyy`) ===
48 | "May 15, 2024"
49 | );
50 | } catch {
51 | return false;
52 | }
53 | };
54 |
55 | export const isDayTwoSession = (date: string) => {
56 | try {
57 | return (
58 | formatInTimeZone(new Date(date), "America/Los_Angeles", `LLL d, yyyy`) ===
59 | "May 16, 2024"
60 | );
61 | } catch {
62 | return false;
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/components/ChangeAppIcon.tsx:
--------------------------------------------------------------------------------
1 | import { Image } from "expo-image";
2 | import { View, StyleSheet, Platform } from "react-native";
3 | import AppIcon from "react-native-dynamic-app-icon";
4 | import { TouchableOpacity } from "react-native-gesture-handler";
5 |
6 | import { ThemedText, ThemedView } from "../components/Themed";
7 | import { theme } from "../theme";
8 |
9 | const defaultIcon = require("../assets/icon.png");
10 | const desertIcon = require("../assets/icons/icon-desert.png");
11 |
12 | export default function ChangeAppIcon() {
13 | if (Platform.OS === "android") {
14 | return null;
15 | }
16 | return (
17 |
22 |
28 | Choose a custom app icon
29 |
30 |
31 | {[defaultIcon, desertIcon].map((icon, index) => (
32 | {
36 | AppIcon.setAppIcon(String(index));
37 | }}
38 | >
39 |
40 |
41 | ))}
42 |
43 |
44 | );
45 | }
46 |
47 | const styles = StyleSheet.create({
48 | icon: {
49 | width: 80,
50 | height: 80,
51 | borderRadius: 10,
52 | margin: 4,
53 | },
54 | icons: {
55 | flexDirection: "row",
56 | justifyContent: "center",
57 | flexWrap: "wrap",
58 | },
59 | heading: {
60 | textAlign: "center",
61 | },
62 | centered: {
63 | textAlign: "center",
64 | },
65 | section: {
66 | padding: theme.space24,
67 | marginBottom: theme.space24,
68 | },
69 | });
70 |
--------------------------------------------------------------------------------
/assets/images/sponsor-mui.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/images/sponsor-expo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/SpeakerCard.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "expo-router";
2 | import { StyleSheet, View } from "react-native";
3 |
4 | import { SpeakerImage } from "./SpeakerImage";
5 | import { ThemedText, ThemedView, useThemeColor } from "./Themed";
6 | import { theme } from "../theme";
7 | import { Speaker } from "../types";
8 | import { TouchableOpacity } from "react-native-gesture-handler";
9 |
10 | type Props = {
11 | speaker: Speaker;
12 | };
13 |
14 | export function SpeakerCard({ speaker }: Props) {
15 | const shadow = useThemeColor({ light: theme.dropShadow, dark: undefined });
16 |
17 | return (
18 |
27 |
28 |
33 |
34 |
39 |
40 |
41 | {speaker.fullName}
42 |
43 |
44 | {speaker.tagLine}
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | const styles = StyleSheet.create({
55 | speakerCard: {
56 | flex: 1,
57 | padding: theme.space16,
58 | marginHorizontal: theme.space16,
59 | borderRadius: theme.borderRadius10,
60 | },
61 | speakerDetail: {
62 | flex: 1,
63 | justifyContent: "center",
64 | },
65 | speakerHeadline: {
66 | flex: 1,
67 | flexDirection: "row",
68 | },
69 | });
70 |
--------------------------------------------------------------------------------
/components/OfflineBanner.tsx:
--------------------------------------------------------------------------------
1 | import { useNetInfo } from "@react-native-community/netinfo";
2 | import { useEffect } from "react";
3 | import { StyleSheet } from "react-native";
4 | import Animated, {
5 | useAnimatedStyle,
6 | useSharedValue,
7 | withTiming,
8 | interpolate,
9 | } from "react-native-reanimated";
10 | import { useSafeAreaInsets } from "react-native-safe-area-context";
11 |
12 | import { ThemedText, ThemedView } from "./Themed";
13 |
14 | import { theme } from "@/theme";
15 |
16 | const minHeight = 0;
17 |
18 | export function OfflineBanner() {
19 | const netinfo = useNetInfo();
20 | const insets = useSafeAreaInsets();
21 | const height = useSharedValue(0);
22 |
23 | const isOffline = netinfo.isInternetReachable === false;
24 | const maxHeight = 28 + insets.bottom / 2;
25 |
26 | useEffect(() => {
27 | if (isOffline) {
28 | height.value = withTiming(maxHeight);
29 | } else {
30 | height.value = withTiming(minHeight);
31 | }
32 | }, [isOffline, height, maxHeight]);
33 |
34 | const animatedStyle = useAnimatedStyle(() => ({
35 | height: height.value,
36 | marginTop: interpolate(
37 | height.value,
38 | [minHeight, maxHeight],
39 | [minHeight, -insets.bottom + theme.space4],
40 | ),
41 | }));
42 |
43 | return (
44 |
45 |
50 |
55 |
56 | App is offline
57 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | const styles = StyleSheet.create({
65 | container: {
66 | height: "100%",
67 | position: "absolute",
68 | bottom: 0,
69 | right: 0,
70 | left: 0,
71 | },
72 | textContainer: {
73 | alignItems: "center",
74 | paddingVertical: theme.space4,
75 | },
76 | });
77 |
--------------------------------------------------------------------------------
/components/TimeZoneSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 | import Entypo from "@expo/vector-icons/Entypo";
3 |
4 | import { ThemedText, useThemeColor } from "./Themed";
5 |
6 | import { useReactConfStore } from "@/store/reactConfStore";
7 | import { theme } from "@/theme";
8 | import { getCurrentTimezone } from "@/utils/formatDate";
9 | import { useActionSheet } from "@expo/react-native-action-sheet";
10 | import { PressableArea } from "./PressableArea";
11 |
12 | export function TimeZoneSwitch() {
13 | const shouldUseLocalTz = useReactConfStore((state) => state.shouldUseLocalTz);
14 | const toggleLocalTz = useReactConfStore((state) => state.toggleLocalTz);
15 |
16 | const iconColor = useThemeColor({
17 | light: theme.colorBlack,
18 | dark: theme.colorWhite,
19 | });
20 |
21 | const { showActionSheetWithOptions } = useActionSheet();
22 |
23 | const onPress = () => {
24 | const options = [
25 | shouldUseLocalTz
26 | ? "Use venue time (PDT)"
27 | : `Use local time (${getCurrentTimezone()})`,
28 | "Cancel",
29 | ];
30 | const cancelButtonIndex = 1;
31 |
32 | showActionSheetWithOptions(
33 | {
34 | options,
35 | cancelButtonIndex,
36 | },
37 | (selectedIndex) => {
38 | if (selectedIndex === 0) {
39 | toggleLocalTz();
40 | }
41 | },
42 | );
43 | };
44 |
45 | return (
46 |
47 |
48 | {shouldUseLocalTz ? "Local Time " : "Venue Time Zone "}
49 |
50 | ({shouldUseLocalTz ? getCurrentTimezone() : "PDT"})
51 |
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | const styles = StyleSheet.create({
59 | container: {
60 | marginHorizontal: theme.space16,
61 | flexDirection: "row",
62 | alignItems: "center",
63 | height: 55,
64 | justifyContent: "flex-end",
65 | flex: 1,
66 | },
67 | switch: {
68 | marginHorizontal: theme.space8,
69 | },
70 | });
71 |
--------------------------------------------------------------------------------
/app/secretModal.tsx:
--------------------------------------------------------------------------------
1 | import LottieView from "lottie-react-native";
2 | import { Platform, StyleSheet, useWindowDimensions } from "react-native";
3 |
4 | import { ThemedText, ThemedView } from "../components/Themed";
5 | import { theme } from "../theme";
6 | import { ExpoImageDemo } from "@/components/ExpoImageDemo";
7 | import ChangeAppIcon from "@/components/ChangeAppIcon";
8 | import { ScrollView } from "react-native-gesture-handler";
9 |
10 | export default function SecretModal() {
11 | const { width } = useWindowDimensions();
12 |
13 | return (
14 |
15 |
20 | {Platform.OS === "ios" ? (
21 |
27 | ) : null}
28 |
34 | You found the secret modal!
35 |
36 |
37 |
43 | We'll use this place for more demos in the future. For now, here's a
44 | little example of native image transitions.
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | const styles = StyleSheet.create({
53 | container: {
54 | flex: 1,
55 | paddingVertical: theme.space24,
56 | },
57 | content: {
58 | paddingHorizontal: theme.space24,
59 | },
60 | heading: {
61 | paddingHorizontal: theme.space24,
62 | textAlign: "center",
63 | },
64 | animation: {
65 | height: 200,
66 | position: "absolute",
67 | },
68 | description: {
69 | paddingHorizontal: theme.space24,
70 | marginBottom: theme.fontSize24,
71 | },
72 | });
73 |
--------------------------------------------------------------------------------
/patches/expo-quick-actions+2.0.0.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/expo-quick-actions/build/hooks.js b/node_modules/expo-quick-actions/build/hooks.js
2 | index 689ed90..a72f1e3 100644
3 | --- a/node_modules/expo-quick-actions/build/hooks.js
4 | +++ b/node_modules/expo-quick-actions/build/hooks.js
5 | @@ -1,5 +1,17 @@
6 | import React from "react";
7 | import * as QuickActions from "./index";
8 | +
9 | +let _initialAction = QuickActions.initial;
10 | +
11 | +function getInitialAction() {
12 | + if (!_initialAction) {
13 | + return null;
14 | + }
15 | + let initialAction = _initialAction;
16 | + _initialAction = null;
17 | + return initialAction;
18 | +}
19 | +
20 | /**
21 | * Handle quick actions with a callback function. This prevents the entire component from re-rendering when the action changes. Use `useQuickAction` if you want to re-render the component.
22 | *
23 | @@ -8,8 +20,9 @@ import * as QuickActions from "./index";
24 | export function useQuickActionCallback(callback) {
25 | React.useEffect(() => {
26 | let isMounted = true;
27 | - if (QuickActions.initial) {
28 | - callback(QuickActions.initial);
29 | + const initialAction = getInitialAction();
30 | + if (initialAction) {
31 | + callback(initialAction);
32 | }
33 | const sub = QuickActions.addListener((event) => {
34 | if (isMounted) {
35 | @@ -20,7 +33,7 @@ export function useQuickActionCallback(callback) {
36 | isMounted = false;
37 | sub.remove();
38 | };
39 | - }, [QuickActions.initial, callback]);
40 | + }, [callback]);
41 | }
42 | /**
43 | * A hook to get the most recent quick action to launch the app. Use `useQuickActionCallback` if you want to handle the action in a callback without re-rendering the component.
44 | @@ -28,7 +41,7 @@ export function useQuickActionCallback(callback) {
45 | * @returns the most recent quick action to launch the app or null if there is none.
46 | */
47 | export function useQuickAction() {
48 | - const [action, setAction] = React.useState(QuickActions.initial ?? null);
49 | + const [action, setAction] = React.useState(getInitialAction() ?? null);
50 | React.useEffect(() => {
51 | let isMounted = true;
52 | const sub = QuickActions.addListener((event) => {
53 |
--------------------------------------------------------------------------------
/components/Themed.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Text, View, useColorScheme, TextStyle } from "react-native";
3 | import Animated from "react-native-reanimated";
4 |
5 | import { theme } from "../theme";
6 |
7 | type ThemeProps = {
8 | lightColor?: string;
9 | darkColor?: string;
10 | };
11 |
12 | export type TextProps = ThemeProps & {
13 | marginBottom?: number;
14 | fontSize?: TextStyle["fontSize"];
15 | fontWeight?: "light" | "medium" | "bold";
16 | italic?: boolean;
17 | animated?: boolean;
18 | } & Text["props"];
19 | export type ViewProps = ThemeProps & View["props"] & { animated?: boolean };
20 |
21 | export function useThemeColor(props: { light: T; dark: U }) {
22 | const theme = useColorScheme() ?? "light";
23 | // const theme = "dark";
24 | return props[theme];
25 | }
26 |
27 | export function ThemedText(props: TextProps) {
28 | const {
29 | style,
30 | lightColor,
31 | darkColor,
32 | marginBottom = 0,
33 | fontSize = theme.fontSize16,
34 | fontWeight,
35 | italic,
36 | animated,
37 | ...otherProps
38 | } = props;
39 | const color = useThemeColor({
40 | light: lightColor || theme.colorBlack,
41 | dark: darkColor || theme.colorWhite,
42 | });
43 | const fontFamily = (() => {
44 | if (fontWeight === "light") {
45 | return italic ? theme.fontFamilyLightItalic : theme.fontFamilyLight;
46 | } else if (fontWeight === "bold") {
47 | return italic ? theme.fontFamilyBoldItalic : theme.fontFamilyBold;
48 | } else {
49 | return italic ? theme.fontFamilyItalic : theme.fontFamily;
50 | }
51 | })();
52 |
53 | if (animated) {
54 | return (
55 |
59 | );
60 | }
61 |
62 | return (
63 |
67 | );
68 | }
69 |
70 | export function ThemedView(props: ViewProps) {
71 | const { style, lightColor, darkColor, animated, ...otherProps } = props;
72 | const backgroundColor = useThemeColor({
73 | light: lightColor || "transparent",
74 | dark: darkColor || "transparent",
75 | });
76 |
77 | if (animated) {
78 | return (
79 |
80 | );
81 | }
82 |
83 | return ;
84 | }
85 |
--------------------------------------------------------------------------------
/components/VenueInfo.tsx:
--------------------------------------------------------------------------------
1 | import FontAwesome6 from "@expo/vector-icons/build/FontAwesome6";
2 | import { Image } from "expo-image";
3 | import { StyleSheet, View, useWindowDimensions } from "react-native";
4 | import { TouchableOpacity } from "react-native-gesture-handler";
5 | import * as Linking from "expo-linking";
6 | import { InfoSection } from "./InfoSection";
7 | import { ThemedText, useThemeColor } from "./Themed";
8 |
9 | import { theme } from "@/theme";
10 |
11 | const venueAddress = "101 Montelago Blvd, Henderson, NV 89011, United States";
12 | const venueName = "The Westin Lake Las Vegas Resort & Spa";
13 |
14 | export function VenueInfo() {
15 | const { width } = useWindowDimensions();
16 |
17 | const hotelImageSize = width / 3;
18 |
19 | const onOpenVenue = () => {
20 | Linking.openURL(
21 | `https://www.google.com/maps?q=${venueName}, ${venueAddress}`,
22 | );
23 | };
24 |
25 | const iconColor = useThemeColor({
26 | light: theme.colorBlack,
27 | dark: theme.colorWhite,
28 | });
29 |
30 | return (
31 |
32 |
33 |
41 |
42 |
43 | The Westin Lake Las Vegas Resort & Spa
44 |
45 |
46 |
47 |
52 |
53 | {venueAddress}
54 |
55 |
56 | );
57 | }
58 |
59 | const styles = StyleSheet.create({
60 | venueContainer: {
61 | flexDirection: "row",
62 | marginBottom: theme.space24,
63 | },
64 | hotelName: {
65 | flex: 1,
66 | paddingLeft: theme.space12,
67 | justifyContent: "center",
68 | },
69 | venueAddress: {
70 | flexDirection: "row",
71 | marginHorizontal: theme.space24,
72 | alignItems: "center",
73 | },
74 | address: {
75 | marginLeft: theme.space24,
76 | flex: 1,
77 | textDecorationLine: "underline",
78 | },
79 | });
80 |
--------------------------------------------------------------------------------
/components/OrganizersInfo.tsx:
--------------------------------------------------------------------------------
1 | import { Image, ImageSource } from "expo-image";
2 | import { StyleSheet, View } from "react-native";
3 | import openWebBrowserAsync from "@/utils/openWebBrowserAsync";
4 | import { TouchableOpacity } from "react-native-gesture-handler";
5 |
6 | import { InfoSection } from "./InfoSection";
7 | import { ThemedText, ThemedView } from "./Themed";
8 |
9 | import { theme } from "@/theme";
10 |
11 | const organizers = {
12 | meta: {
13 | image: require("../assets/images/meta-logo.svg"),
14 | description:
15 | "Giving people the power to build community and bring the world closer together.",
16 | url: "https://www.meta.com/",
17 | },
18 | callstack: {
19 | image: require("../assets/images/callstack-logo.svg"),
20 | description:
21 | "Callstack unlocks a universe of possibilities for your business with the React tech stack.",
22 | url: "https://callstack.com/",
23 | },
24 | };
25 |
26 | export function OrganizersInfo() {
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | const Organizer = ({
38 | organizer,
39 | }: {
40 | organizer: { image: ImageSource; description: string; url: string };
41 | }) => {
42 | return (
43 | <>
44 |
49 | openWebBrowserAsync(organizer.url)}
51 | activeOpacity={0.6}
52 | >
53 |
58 |
59 |
60 |
61 | {organizer.description}
62 |
63 | >
64 | );
65 | };
66 |
67 | const styles = StyleSheet.create({
68 | content: {
69 | alignItems: "center",
70 | },
71 | image: {
72 | height: 40,
73 | width: "100%",
74 | },
75 | imageContainer: {
76 | padding: theme.space16,
77 | borderRadius: theme.borderRadius10,
78 | width: "50%",
79 | marginBottom: theme.space16,
80 | marginTop: theme.space16,
81 | },
82 | organizerText: {
83 | marginBottom: theme.space24,
84 | textAlign: "center",
85 | },
86 | });
87 |
--------------------------------------------------------------------------------
/components/SearchInput.tsx:
--------------------------------------------------------------------------------
1 | import Entypo from "@expo/vector-icons/build/Entypo";
2 | import Feather from "@expo/vector-icons/build/Feather";
3 | import { TextInput, StyleSheet, View } from "react-native";
4 |
5 | import { useThemeColor } from "./Themed";
6 |
7 | import { theme } from "@/theme";
8 | import { PressableArea } from "./PressableArea";
9 |
10 | export function SearchInput({
11 | value,
12 | onChange,
13 | }: {
14 | value: string;
15 | onChange: (value: string) => void;
16 | }) {
17 | const iconColor = useThemeColor({
18 | light: theme.colorGrey,
19 | dark: theme.colorWhite,
20 | });
21 | const textColor = useThemeColor({
22 | light: theme.colorBlack,
23 | dark: theme.colorWhite,
24 | });
25 | const searchInputColor = useThemeColor({
26 | light: theme.colorWhite,
27 | dark: "rgba(255,255,255,0.15)",
28 | });
29 |
30 | const placeholderTextColor = useThemeColor({
31 | light: theme.colorGrey,
32 | dark: "rgba(255,255,255,0.5)",
33 | });
34 |
35 | const shadow = useThemeColor({ light: theme.dropShadow, dark: undefined });
36 |
37 | return (
38 |
39 |
52 |
58 | {value ? (
59 | onChange("")}
61 | style={styles.clearIcon}
62 | hitSlop={30}
63 | >
64 |
65 |
66 | ) : null}
67 |
68 | );
69 | }
70 |
71 | const styles = StyleSheet.create({
72 | input: {
73 | borderRadius: theme.borderRadius6,
74 | marginHorizontal: theme.space16,
75 | marginTop: theme.space16,
76 | marginBottom: theme.space12,
77 | padding: theme.space8,
78 | paddingLeft: 42,
79 | fontFamily: theme.fontFamily,
80 | fontSize: theme.fontSize18,
81 | },
82 | searchIcon: {
83 | position: "absolute",
84 | top: theme.space24,
85 | left: theme.space24,
86 | },
87 | clearIcon: {
88 | position: "absolute",
89 | top: theme.space24,
90 | right: theme.space24,
91 | },
92 | });
93 |
--------------------------------------------------------------------------------
/app/(tabs)/bookmarks.tsx:
--------------------------------------------------------------------------------
1 | import { useScrollToTop } from "@react-navigation/native";
2 | import React from "react";
3 | import { StyleSheet, View } from "react-native";
4 |
5 | import { ThemedText, ThemedView } from "@/components/Themed";
6 | import { theme } from "@/theme";
7 | import { TalkCard } from "@/components/TalkCard";
8 | import { useBookmarkStore } from "@/store/bookmarkStore";
9 | import { useReactConfStore } from "@/store/reactConfStore";
10 | import { FlatList } from "react-native-gesture-handler";
11 |
12 | export default function Bookmarks() {
13 | const scrollRef = React.useRef(null);
14 | useScrollToTop(scrollRef);
15 |
16 | const bookmarks = useBookmarkStore((state) => state.bookmarks);
17 |
18 | const { dayOne, dayTwo } = useReactConfStore((state) => state.schedule);
19 |
20 | const dayOneFiltered = dayOne.filter(
21 | (session) => !!bookmarks.find((b) => b.sessionId === session.id),
22 | );
23 |
24 | const dayTwoFiltered = dayTwo.filter(
25 | (session) => !!bookmarks.find((b) => b.sessionId === session.id),
26 | );
27 |
28 | return (
29 |
34 | {dayOneFiltered.length || dayTwoFiltered.length ? (
35 | ({ talk, isDayOne: true })),
40 | ...dayTwoFiltered.map((talk) => ({ talk, isDayOne: false })),
41 | ]}
42 | renderItem={({ item }) => (
43 |
48 | )}
49 | />
50 | ) : (
51 |
52 |
57 | No sessions bookmarked
58 |
59 |
60 | Tap on the bookmark icon on a session to add it to your bookmarks,
61 | and it will be displayed here.
62 |
63 |
64 | )}
65 |
66 | );
67 | }
68 |
69 | const styles = StyleSheet.create({
70 | container: {
71 | flex: 1,
72 | },
73 | flatListContainer: {
74 | paddingTop: theme.space16,
75 | },
76 | bookmarks: {
77 | flex: 1,
78 | justifyContent: "center",
79 | paddingHorizontal: theme.space24,
80 | },
81 | });
82 |
--------------------------------------------------------------------------------
/app/(tabs)/speakers/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Keyboard, Platform, StyleSheet, View } from "react-native";
3 |
4 | import { NotFound } from "@/components/NotFound";
5 |
6 | import { SpeakerCard } from "@/components/SpeakerCard";
7 | import { ThemedText, ThemedView } from "@/components/Themed";
8 | import { useReactConfStore } from "@/store/reactConfStore";
9 | import { theme } from "@/theme";
10 | import { FlatList } from "react-native-gesture-handler";
11 | import { useLocalSearchParams } from "expo-router";
12 | import useScrollToTopWithOffset from "@/utils/useScrollToTopWithOffset";
13 |
14 | export default function Speakers() {
15 | const ref = React.useRef(null);
16 | useScrollToTopWithOffset(
17 | ref,
18 | Platform.select({
19 | ios: -90,
20 | default: 0,
21 | }),
22 | );
23 | const speakers = useReactConfStore((state) => state.allSessions.speakers);
24 |
25 | const params = useLocalSearchParams<{ q?: string }>();
26 |
27 | if (!speakers.length) {
28 | return ;
29 | }
30 |
31 | const searchText = params?.q?.toLowerCase() || "";
32 |
33 | const filteredSpeakers = speakers.filter((speaker) => {
34 | if (!searchText) {
35 | return true;
36 | }
37 | return speaker.fullName.toLowerCase().includes(searchText);
38 | });
39 |
40 | const dismissKeyboard = () => {
41 | Keyboard.dismiss();
42 | };
43 |
44 | return (
45 |
50 | (
59 |
60 | )}
61 | renderItem={({ item }) => }
62 | data={filteredSpeakers}
63 | ListEmptyComponent={
64 |
65 |
66 | No results found for{" "}
67 | {searchText}
68 |
69 |
70 | }
71 | />
72 |
73 | );
74 | }
75 |
76 | export const styles = StyleSheet.create({
77 | container: {
78 | flex: 1,
79 | },
80 | contentContainer: {
81 | paddingTop: theme.space16,
82 | },
83 | noResultsContainer: {
84 | paddingHorizontal: theme.space24,
85 | },
86 | });
87 |
--------------------------------------------------------------------------------
/components/SpeakerImage.tsx:
--------------------------------------------------------------------------------
1 | import { Image } from "expo-image";
2 | import { StyleSheet, View, ViewStyle } from "react-native";
3 |
4 | import { theme } from "@/theme";
5 | import { ThemedView } from "./Themed";
6 |
7 | export function SpeakerImage({
8 | profilePicture,
9 | size,
10 | style,
11 | animated,
12 | }: {
13 | profilePicture?: string | null;
14 | size?: "medium" | "large" | "xlarge";
15 | style?: ViewStyle;
16 | animated?: boolean;
17 | }) {
18 | const imageSize = (() => {
19 | switch (size) {
20 | case "large":
21 | return styles.imageSizeLarge;
22 | case "xlarge":
23 | return styles.imageSizeExtraLarge;
24 | case "medium":
25 | default:
26 | return styles.imageSizeMedium;
27 | }
28 | })();
29 | const imageStyles = [styles.profileImage, imageSize];
30 |
31 | const reactLogoSize = (() => {
32 | switch (size) {
33 | case "large":
34 | return styles.reactLogoSizeLarge;
35 | case "xlarge":
36 | return styles.reactLogoSizeExtraLarge;
37 | case "medium":
38 | default:
39 | return styles.reactLogoSizeMedium;
40 | }
41 | })();
42 |
43 | const placeholder = (
44 |
45 |
49 |
50 | );
51 |
52 | return (
53 |
58 | {profilePicture ? (
59 |
64 | ) : (
65 | placeholder
66 | )}
67 |
68 | );
69 | }
70 |
71 | const styles = StyleSheet.create({
72 | imageContainer: {
73 | marginRight: theme.space12,
74 | borderRadius: theme.borderRadius10,
75 | overflow: "hidden",
76 | },
77 | profileImage: {
78 | width: 50,
79 | height: 70,
80 | ...StyleSheet.absoluteFillObject,
81 | },
82 | imageSizeMedium: {
83 | width: 60,
84 | height: 60,
85 | },
86 | imageSizeLarge: {
87 | width: 100,
88 | height: 100,
89 | },
90 | imageSizeExtraLarge: {
91 | width: 200,
92 | height: 200,
93 | },
94 | fallbackImage: {
95 | backgroundColor: theme.colorReactDarkBlue,
96 | justifyContent: "center",
97 | alignItems: "center",
98 | },
99 | reactLogoSizeMedium: {
100 | width: 30,
101 | height: 30,
102 | },
103 | reactLogoSizeLarge: {
104 | width: 50,
105 | height: 50,
106 | },
107 | reactLogoSizeExtraLarge: {
108 | width: 100,
109 | height: 100,
110 | },
111 | });
112 |
--------------------------------------------------------------------------------
/assets/images/not-found.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-conf-app",
3 | "version": "1.0.2",
4 | "main": "expo-router/entry",
5 | "private": true,
6 | "scripts": {
7 | "start": "expo start",
8 | "android": "expo run:android",
9 | "ios": "expo run:ios",
10 | "web": "expo start --web",
11 | "test": "jest",
12 | "sync-api": "node ./scripts/syncApi.js",
13 | "postinstall": "patch-package",
14 | "lint": "eslint ."
15 | },
16 | "dependencies": {
17 | "@config-plugins/react-native-dynamic-app-icon": "^8.0.0",
18 | "@expo/react-native-action-sheet": "^4.0.1",
19 | "@expo/vector-icons": "^14.0.0",
20 | "@react-native-async-storage/async-storage": "1.23.1",
21 | "@react-native-community/netinfo": "11.3.1",
22 | "@react-native-masked-view/masked-view": "0.3.1",
23 | "date-fns": "^3.6.0",
24 | "date-fns-tz": "^3.1.3",
25 | "eslint-plugin-react-native": "^4.1.0",
26 | "expo": "~51.0.7",
27 | "expo-application": "~5.9.1",
28 | "expo-blur": "~13.0.2",
29 | "expo-build-properties": "~0.12.1",
30 | "expo-constants": "~16.0.1",
31 | "expo-dev-client": "~4.0.14",
32 | "expo-device": "~6.0.2",
33 | "expo-font": "~12.0.5",
34 | "expo-haptics": "~13.0.1",
35 | "expo-image": "~1.12.9",
36 | "expo-linking": "~6.3.1",
37 | "expo-notifications": "~0.28.2",
38 | "expo-quick-actions": "^2.0.0",
39 | "expo-router": "~3.5.14",
40 | "expo-status-bar": "~1.12.1",
41 | "expo-system-ui": "~3.0.4",
42 | "expo-updates": "~0.25.13",
43 | "expo-web-browser": "~13.0.3",
44 | "jest": "^29.3.1",
45 | "jest-expo": "~51.0.1",
46 | "lottie-react-native": "6.7.0",
47 | "react": "18.2.0",
48 | "react-compiler-runtime": "file:./lib/react-compiler-runtime",
49 | "react-native": "0.74.1",
50 | "react-native-bootsplash": "^6.0.0-beta.6",
51 | "react-native-dynamic-app-icon": "^1.1.0",
52 | "react-native-gesture-handler": "~2.16.1",
53 | "react-native-reanimated": "~3.10.1",
54 | "react-native-safe-area-context": "4.10.1",
55 | "react-native-screens": "3.31.1",
56 | "zustand": "^4.5.2"
57 | },
58 | "devDependencies": {
59 | "@babel/core": "^7.20.0",
60 | "@types/jest": "^29.5.12",
61 | "@types/react": "~18.2.79",
62 | "babel-plugin-react-compiler": "^0.0.0-experimental-4690415-20240515",
63 | "eslint": "^8.57.0",
64 | "eslint-config-expo": "^7.0.0",
65 | "eslint-config-prettier": "^9.1.0",
66 | "eslint-plugin-prettier": "^5.1.3",
67 | "patch-package": "^8.0.0",
68 | "prettier": "^3.2.5",
69 | "typescript": "~5.3.3"
70 | },
71 | "resolutions": {
72 | "react": "18.2.0"
73 | },
74 | "jest": {
75 | "preset": "jest-expo",
76 | "transformIgnorePatterns": [
77 | "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)"
78 | ]
79 | },
80 | "expo": {
81 | "autolinking": {
82 | "exclude": [
83 | "expo-splash-screen"
84 | ]
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/store/reactConfStore.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from "@react-native-async-storage/async-storage";
2 | import { create } from "zustand";
3 | import { persist, createJSONStorage } from "zustand/middleware";
4 |
5 | import initialAllSessions from "@/data/allSessions.json";
6 | import { ApiAllSessions, Session } from "@/types";
7 | import { formatSessions } from "@/utils/sessions";
8 |
9 | const doFetch = async (url: string) => {
10 | try {
11 | const result = await fetch(url);
12 | return await result.json();
13 | } catch {
14 | return null;
15 | }
16 | };
17 |
18 | type ConfState = {
19 | schedule: {
20 | dayOne: Session[];
21 | dayTwo: Session[];
22 | };
23 | allSessions: ApiAllSessions;
24 | isRefreshing?: boolean;
25 | lastRefreshed: string | null;
26 | refreshData: (options?: { ttlMs?: number }) => Promise;
27 | shouldUseLocalTz: boolean;
28 | toggleLocalTz: () => void;
29 | };
30 |
31 | const getInitialSchedule = () => {
32 | const [dayOne, dayTwo] = formatSessions(initialAllSessions);
33 | return {
34 | schedule: {
35 | dayOne,
36 | dayTwo,
37 | },
38 | allSessions: initialAllSessions as ApiAllSessions,
39 | };
40 | };
41 |
42 | export const useReactConfStore = create(
43 | persist(
44 | (set, get) => ({
45 | ...getInitialSchedule(),
46 | isRefreshing: false,
47 | lastRefreshed: null,
48 | shouldUseLocalTz: false,
49 | refreshData: async (options) => {
50 | const ttlMs = options?.ttlMs;
51 | const { isRefreshing, lastRefreshed } = get();
52 |
53 | // Bail out if already refreshing
54 | if (isRefreshing) {
55 | return;
56 | }
57 |
58 | // Bail out if last refresh was within TTL
59 | if (lastRefreshed) {
60 | const diff = new Date().getTime() - new Date(lastRefreshed).getTime();
61 | if (ttlMs && diff < ttlMs) {
62 | return;
63 | }
64 | }
65 |
66 | try {
67 | set({ isRefreshing: true });
68 |
69 | const allSessions = await doFetch(
70 | "https://sessionize.com/api/v2/ctta9bhe/view/All",
71 | );
72 |
73 | if (allSessions) {
74 | const [dayOne, dayTwo] = formatSessions(allSessions);
75 | set({
76 | schedule: {
77 | dayOne,
78 | dayTwo,
79 | },
80 | allSessions,
81 | lastRefreshed: new Date().toISOString(),
82 | });
83 | }
84 | } catch (e) {
85 | console.warn(e);
86 | } finally {
87 | set({ isRefreshing: false });
88 | }
89 | },
90 | toggleLocalTz: () => {
91 | set((state) => ({ shouldUseLocalTz: !state.shouldUseLocalTz }));
92 | },
93 | }),
94 | {
95 | name: "react-conf-2024-store",
96 | storage: createJSONStorage(() => AsyncStorage),
97 | partialize: (state) => {
98 | const { isRefreshing: _, ...dataToPersist } = state;
99 | return dataToPersist;
100 | },
101 | },
102 | ),
103 | );
104 |
--------------------------------------------------------------------------------
/components/Bookmark.tsx:
--------------------------------------------------------------------------------
1 | import MaterialCommunityIcons from "@expo/vector-icons/build/MaterialCommunityIcons";
2 | import * as Haptics from "expo-haptics";
3 | import { Platform } from "react-native";
4 | import Animated, {
5 | useAnimatedStyle,
6 | useSharedValue,
7 | withTiming,
8 | } from "react-native-reanimated";
9 | import { TouchableOpacity } from "react-native-gesture-handler";
10 | import * as Notifications from "expo-notifications";
11 |
12 | import { useBookmarkStore } from "@/store/bookmarkStore";
13 | import { theme } from "@/theme";
14 | import { registerForPushNotificationsAsync } from "@/utils/registerForPushNotificationsAsync";
15 | import { isPast, subMinutes } from "date-fns";
16 | import { Session } from "@/types";
17 |
18 | const AnimatedTouchableOpacity =
19 | Animated.createAnimatedComponent(TouchableOpacity);
20 |
21 | export function Bookmark({ session }: { session: Session }) {
22 | const toggleBookmarked = useBookmarkStore((state) => state.toggleBookmarked);
23 | const bookmarks = useBookmarkStore((state) => state.bookmarks);
24 | const currentBookmark = bookmarks.find((b) => b.sessionId === session.id);
25 | const scale = useSharedValue(1);
26 |
27 | const animatedStyle = useAnimatedStyle(() => ({
28 | transform: [{ scale: scale.value }],
29 | }));
30 |
31 | const setNotification = async () => {
32 | const fiveMinutesTillSession = subMinutes(new Date(session.startsAt), 5);
33 |
34 | if (isPast(fiveMinutesTillSession)) {
35 | return undefined;
36 | }
37 |
38 | const status = await registerForPushNotificationsAsync();
39 |
40 | if (status === "granted") {
41 | return Notifications.scheduleNotificationAsync({
42 | content: {
43 | title: `"${session.title}" starts in 5 minutes`,
44 | data: {
45 | url: `/talk/${session.id}`,
46 | },
47 | },
48 | trigger: {
49 | date: fiveMinutesTillSession,
50 | },
51 | });
52 | }
53 | };
54 |
55 | const handlePress = async () => {
56 | if (Platform.OS !== "web") {
57 | Haptics.selectionAsync();
58 | }
59 |
60 | if (currentBookmark) {
61 | if (currentBookmark?.notificationId) {
62 | await Notifications.cancelScheduledNotificationAsync(
63 | currentBookmark?.notificationId,
64 | );
65 | }
66 | toggleBookmarked(session.id);
67 | } else {
68 | const notificationId = await setNotification();
69 | toggleBookmarked(session.id, notificationId);
70 | }
71 | };
72 |
73 | return (
74 | {
77 | if (Platform.OS !== "web" && !currentBookmark) {
78 | Haptics.selectionAsync();
79 | }
80 | handlePress();
81 | }}
82 | onPressIn={() => {
83 | scale.value = currentBookmark ? withTiming(0.8) : withTiming(1.6);
84 | }}
85 | onPressOut={() => {
86 | scale.value = withTiming(1);
87 | }}
88 | style={animatedStyle}
89 | >
90 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Conference App for React Conf 2024
2 |
3 | This is the source code for the React Conf 2024 app. Download it from stores: [Google Play](https://play.google.com/store/apps/details?id=com.reactconf.app), [App Store](https://apps.apple.com/gb/app/react-conf/id6499559897).
4 |
5 | To run the app locally, clone the repo and install dependencies with `yarn` (**yarn.lock** was generated with Yarn v1). Next, either [compile and run it locally](#compile-and-run-locally) or [build and run it with EAS](#build-and-run-with-eas).
6 |
7 | ## Compile and run locally
8 |
9 | To compile the app locally, you will need to have Xcode ([learn more](https://docs.expo.dev/guides/local-app-development/#ios)) and/or Android ([learn more](https://docs.expo.dev/guides/local-app-development/#android)) toolchains installed and configured.
10 |
11 | > [!NOTE]
12 | > In order to be able to sign the app for an iOS device with a development certificate, you need a unique bundle identifier. Change the `APP_ID_PREFIX` in **app.config.js** to a unique ID, such as `yourname.reactconf`. Run `npx expo prebuild --clean` when you've updated the value to sync it to the native project.
13 |
14 | ### Android
15 |
16 | ```sh
17 | # Generate the `android/` directory
18 | npx expo prebuild -p android
19 |
20 | # Compile with Gradle
21 | npx expo run:android
22 | # Alternatively, start the dev server and manually open in Android Studio and build
23 | npx expo start
24 | ```
25 |
26 | ### iOS
27 |
28 | ```sh
29 | # Generate the `ios/` directory
30 | npx expo prebuild -p ios
31 |
32 | # Compile with xcodebuild and run on simulator.
33 | npx expo run:ios
34 | # Alternatively, start the dev server and manually open Xcode and build
35 | npx expo start
36 | ```
37 |
38 | For development on the Android Emulator / iOS Simulator:
39 |
40 | ## Build and run with EAS
41 |
42 | ### Initial configuration
43 |
44 | In order to run a build with EAS, you will need to update the EAS owner and project ID fields in **app.config.js**. Change the `EAS_APP_OWNER`, `EAS_PROJECT_ID`, and `EAS_UPDATE_URL` to empty strings, then run `eas init` and `eas update:configure` to get the new values for your username (never used EAS before? [look at this guide](https://docs.expo.dev/build/setup/)).
45 |
46 | ### Android
47 |
48 | ```sh
49 | # Create a development build. When it's completed, you will be prompted to install it
50 | eas build --platform android --profile localdev
51 | # Create a preview build. This is like a production build, but intended to be
52 | # installed directly to your device
53 | eas build --platform android --profile preview
54 | ```
55 |
56 | ### iOS
57 |
58 | ```sh
59 | # Create a development build. When it's completed, you will be prompted to install it
60 | eas build --platform ios --profile localdev
61 | # Create a preview build. This is like a production build, but intended to be
62 | # installed directly to your device
63 | eas build --platform ios --profile preview
64 | ```
65 |
66 | ## Learn more
67 |
68 | - [Get started with Expo](https://docs.expo.dev/get-started/introduction/).
69 | - Check out the [Expo "Getting Started" tutorial](https://docs.expo.dev/tutorial/introduction/).
70 | - Check out the [EAS Tutorial](https://docs.expo.dev/tutorial/eas/introduction/) or the [EggHead course](https://egghead.io/courses/build-and-deploy-react-native-apps-with-expo-eas-85ab521e).
--------------------------------------------------------------------------------
/components/MiniTalkCard.tsx:
--------------------------------------------------------------------------------
1 | import { Image } from "expo-image";
2 | import { Link } from "expo-router";
3 | import { StyleSheet, View } from "react-native";
4 | import { TouchableOpacity } from "react-native-gesture-handler";
5 |
6 | import { ThemedText, ThemedView, useThemeColor } from "./Themed";
7 |
8 | import { useReactConfStore } from "@/store/reactConfStore";
9 | import { theme } from "@/theme";
10 | import { formatSessionTime } from "@/utils/formatDate";
11 | import { Bookmark } from "./Bookmark";
12 |
13 | export function MiniTalkCard({ sessionId }: { sessionId: string | number }) {
14 | const shouldUseLocalTz = useReactConfStore((state) => state.shouldUseLocalTz);
15 | const { dayOne, dayTwo } = useReactConfStore((state) => state.schedule);
16 | const iconColor = useThemeColor({
17 | light: theme.colorWhite,
18 | dark: theme.colorDarkBlue,
19 | });
20 |
21 | const { talk, isDayOne } = (() => {
22 | const dayOneTalk = dayOne.find(
23 | (session) => session.id === String(sessionId),
24 | );
25 | if (dayOneTalk) {
26 | return { talk: dayOneTalk, isDayOne: true };
27 | }
28 | const dayTwoTalk = dayTwo.find(
29 | (session) => session.id === String(sessionId),
30 | );
31 | if (dayTwoTalk) {
32 | return { talk: dayTwoTalk, isDayOne: false };
33 | }
34 | return { talk: null, isDayOne: null };
35 | })();
36 |
37 | if (!talk) {
38 | return null;
39 | }
40 |
41 | return (
42 |
50 |
51 |
60 |
65 |
66 |
71 | {formatSessionTime(talk, shouldUseLocalTz)}
72 | {` `}({isDayOne ? "Day 1" : "Day 2"})
73 |
74 |
75 |
76 |
77 | {talk.title}
78 |
79 |
80 |
81 |
82 | );
83 | }
84 |
85 | const styles = StyleSheet.create({
86 | container: {
87 | borderRadius: theme.borderRadius10,
88 | padding: theme.space16,
89 | marginBottom: theme.space24,
90 | },
91 | heading: {
92 | flexDirection: "row",
93 | justifyContent: "space-between",
94 | },
95 | reactLogo: {
96 | position: "absolute",
97 | height: 200,
98 | width: 200,
99 | opacity: 0.2,
100 | right: 0,
101 | },
102 | });
103 |
--------------------------------------------------------------------------------
/components/AnimatedBootSplash.tsx:
--------------------------------------------------------------------------------
1 | import BootSplash, { Manifest } from "react-native-bootsplash";
2 | import MaskedView from "@react-native-masked-view/masked-view";
3 | import Animated, {
4 | Easing,
5 | runOnJS,
6 | useAnimatedStyle,
7 | useSharedValue,
8 | withTiming,
9 | } from "react-native-reanimated";
10 | import { StyleSheet, View } from "react-native";
11 | import { ReactNode, useState } from "react";
12 |
13 | const MAX_SCALE = 10;
14 |
15 | const manifest: Manifest = require("../assets/bootsplash/manifest.json");
16 |
17 | const styles = StyleSheet.create({
18 | mask: {
19 | backgroundColor: "black",
20 | borderRadius: manifest.logo.width,
21 | width: manifest.logo.width,
22 | height: manifest.logo.height,
23 | },
24 | transparent: {
25 | backgroundColor: "transparent",
26 | },
27 | });
28 |
29 | type Props = {
30 | animationEnded: boolean;
31 | children: ReactNode;
32 | onAnimationEnd: () => void;
33 | };
34 |
35 | export const AnimatedBootSplash = ({
36 | animationEnded,
37 | children,
38 | onAnimationEnd,
39 | }: Props) => {
40 | const [ready, setReady] = useState(false);
41 |
42 | const opacity = useSharedValue(1);
43 | const scale = useSharedValue(animationEnded ? MAX_SCALE : 1);
44 |
45 | const opacityStyle = useAnimatedStyle(() => ({
46 | opacity: opacity.value,
47 | }));
48 |
49 | const scaleStyle = useAnimatedStyle(() => ({
50 | transform: [{ scale: scale.value }],
51 | }));
52 |
53 | const { container, logo, brand } = BootSplash.useHideAnimation({
54 | manifest,
55 | ready,
56 |
57 | logo: require("../assets/bootsplash/logo.png"),
58 | brand: require("../assets/bootsplash/brand.png"),
59 |
60 | statusBarTranslucent: true,
61 | navigationBarTranslucent: false,
62 |
63 | animate: () => {
64 | opacity.value = withTiming(0, {
65 | duration: 250,
66 | easing: Easing.out(Easing.ease),
67 | });
68 |
69 | scale.value = withTiming(
70 | MAX_SCALE,
71 | {
72 | duration: 350,
73 | easing: Easing.back(0.75),
74 | },
75 | () => {
76 | runOnJS(onAnimationEnd)();
77 | },
78 | );
79 | },
80 | });
81 |
82 | return (
83 | <>
84 | {/* Apply background color under the mask */}
85 | {!animationEnded && }
86 |
87 |
92 | {
95 | setReady(true);
96 | }}
97 | />
98 |
99 | }
100 | >
101 | {children}
102 |
103 |
104 | {!animationEnded && (
105 | // Don't apply background color above the mask
106 |
107 |
111 |
112 |
113 |
114 | )}
115 | >
116 | );
117 | };
118 |
--------------------------------------------------------------------------------
/components/ReactConfHeader.tsx:
--------------------------------------------------------------------------------
1 | import { Image } from "expo-image";
2 | import React from "react";
3 | import { StyleSheet, View } from "react-native";
4 | import Animated, {
5 | Extrapolation,
6 | SharedValue,
7 | interpolate,
8 | useAnimatedStyle,
9 | } from "react-native-reanimated";
10 |
11 | import { ThemedText, ThemedView, useThemeColor } from "./Themed";
12 | import { theme } from "../theme";
13 |
14 | import { COLLAPSED_HEADER, EXPANDED_HEADER, ROW_HEIGHT } from "@/consts";
15 |
16 | const AnimatedImage = Animated.createAnimatedComponent(Image);
17 |
18 | const interpolateHeader = (
19 | scrollOffset: SharedValue,
20 | outputRange: number[],
21 | ) => {
22 | "worklet";
23 | return interpolate(
24 | scrollOffset.value,
25 | [COLLAPSED_HEADER, EXPANDED_HEADER],
26 | outputRange,
27 | Extrapolation.CLAMP,
28 | );
29 | };
30 |
31 | interface ReactConfHeaderProps {
32 | scrollOffset: SharedValue;
33 | }
34 |
35 | export function ReactConfHeader({ scrollOffset }: ReactConfHeaderProps) {
36 | const tintColor = useThemeColor({
37 | light: theme.colorReactDarkBlue,
38 | dark: theme.colorReactLightBlue,
39 | });
40 |
41 | const animatedLogoStyle = useAnimatedStyle(() => ({
42 | transform: [
43 | { translateX: interpolateHeader(scrollOffset, [0, -30]) },
44 | { scale: interpolateHeader(scrollOffset, [1, 0.6]) },
45 | ],
46 | }));
47 |
48 | const firstLineStyle = useAnimatedStyle(() => ({
49 | transform: [
50 | { translateX: interpolateHeader(scrollOffset, [0, -45]) },
51 | { translateY: interpolateHeader(scrollOffset, [0, 13]) },
52 | ],
53 | fontSize: interpolateHeader(scrollOffset, [36, 24]),
54 | }));
55 |
56 | const secondLineStyle = useAnimatedStyle(() => ({
57 | transform: [
58 | { translateX: interpolateHeader(scrollOffset, [0, 35]) },
59 | { translateY: interpolateHeader(scrollOffset, [0, -18]) },
60 | ],
61 | }));
62 |
63 | const headerStyle = useAnimatedStyle(() => ({
64 | height: interpolateHeader(scrollOffset, [
65 | EXPANDED_HEADER - ROW_HEIGHT,
66 | COLLAPSED_HEADER,
67 | ]),
68 | }));
69 |
70 | return (
71 |
77 |
83 |
84 |
92 | REACT
93 |
94 |
99 | CONF 2024
100 |
101 |
102 |
103 | );
104 | }
105 |
106 | const styles = StyleSheet.create({
107 | reactImage: {
108 | width: 75,
109 | height: 75,
110 | },
111 | logoText: {
112 | paddingStart: theme.space8,
113 | },
114 | header: {
115 | alignItems: "center",
116 | flexDirection: "row",
117 | justifyContent: "center",
118 | marginHorizontal: theme.space8,
119 | },
120 | });
121 |
--------------------------------------------------------------------------------
/patches/react-native-bootsplash+6.0.0-beta.6.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/react-native-bootsplash/dist/commonjs/generate.js b/node_modules/react-native-bootsplash/dist/commonjs/generate.js
2 | index 219be98..012c291 100644
3 | --- a/node_modules/react-native-bootsplash/dist/commonjs/generate.js
4 | +++ b/node_modules/react-native-bootsplash/dist/commonjs/generate.js
5 | @@ -966,10 +966,11 @@ const withAppDelegate = config => Expo.withAppDelegate(config, config => {
6 | offset: 0,
7 | anchor: /@end/,
8 | newSrc: (0, _tsDedent.dedent)`
9 | - - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName initProps:(NSDictionary *)initProps {
10 | - UIView *rootView = [super createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
11 | + - (void)customizeRootView:(RCTRootView *)rootView {
12 | + // Expected to be called twice from expo-updates setup:
13 | + // 1. when expo-updates creates the deferred root view.
14 | + // 2. when expo-updates finishing its setup and re-create the real root view.
15 | [RNBootSplash initWithStoryboard:@"BootSplash" rootView:rootView];
16 | - return rootView;
17 | }
18 | `
19 | });
20 | diff --git a/node_modules/react-native-bootsplash/ios/RNBootSplash.mm b/node_modules/react-native-bootsplash/ios/RNBootSplash.mm
21 | index 56a8a61..218acaa 100755
22 | --- a/node_modules/react-native-bootsplash/ios/RNBootSplash.mm
23 | +++ b/node_modules/react-native-bootsplash/ios/RNBootSplash.mm
24 | @@ -77,9 +77,9 @@ + (void)initWithStoryboard:(NSString * _Nonnull)storyboardName
25 | return;
26 | }
27 |
28 | - static dispatch_once_t onceToken;
29 | -
30 | - dispatch_once(&onceToken, ^(void) {
31 | +// static dispatch_once_t onceToken;
32 | +//
33 | +// dispatch_once(&onceToken, ^(void) {
34 | [NSTimer scheduledTimerWithTimeInterval:0.35
35 | repeats:NO
36 | block:^(NSTimer * _Nonnull timer) {
37 | @@ -96,7 +96,7 @@ + (void)initWithStoryboard:(NSString * _Nonnull)storyboardName
38 | if (rootView != nil && [rootView isKindOfClass:[RCTSurfaceHostingProxyRootView class]]) {
39 | _rootView = (RCTSurfaceHostingProxyRootView *)rootView;
40 | #else
41 | - if (rootView != nil && [rootView isKindOfClass:[RCTRootView class]]) {
42 | + if (rootView != nil) {
43 | _rootView = (RCTRootView *)rootView;
44 | #endif
45 | UIStoryboard *storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
46 | @@ -124,7 +124,7 @@ + (void)initWithStoryboard:(NSString * _Nonnull)storyboardName
47 | name:RCTJavaScriptDidFailToLoadNotification
48 | object:nil];
49 | }
50 | - });
51 | +// });
52 | }
53 |
54 | + (void)onJavaScriptDidLoad {
55 | diff --git a/node_modules/react-native-bootsplash/src/generate.ts b/node_modules/react-native-bootsplash/src/generate.ts
56 | index 95a0f4b..7d74725 100644
57 | --- a/node_modules/react-native-bootsplash/src/generate.ts
58 | +++ b/node_modules/react-native-bootsplash/src/generate.ts
59 | @@ -1323,10 +1323,11 @@ const withAppDelegate: ExpoPlugin = (config) =>
60 | offset: 0,
61 | anchor: /@end/,
62 | newSrc: dedent`
63 | - - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName initProps:(NSDictionary *)initProps {
64 | - UIView *rootView = [super createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
65 | + - (void)customizeRootView:(RCTRootView *)rootView {
66 | + // Expected to be called twice from expo-updates setup:
67 | + // 1. when expo-updates creates the deferred root view.
68 | + // 2. when expo-updates finishing its setup and re-create the real root view.
69 | [RNBootSplash initWithStoryboard:@"BootSplash" rootView:rootView];
70 | - return rootView;
71 | }
72 | `,
73 | });
74 |
--------------------------------------------------------------------------------
/assets/images/meta-logo.svg:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/components/ExpoImageDemo.tsx:
--------------------------------------------------------------------------------
1 | import { Platform, StyleSheet, View } from "react-native";
2 | import { Image } from "expo-image";
3 | import { theme } from "@/theme";
4 | import { ThemedText, ThemedView, useThemeColor } from "./Themed";
5 | import { useState } from "react";
6 | import { TouchableOpacity } from "react-native-gesture-handler";
7 |
8 | const effects = [
9 | {
10 | name: "None - image 1",
11 | effect: null,
12 | image: require("../assets/images/react-blue.png"),
13 | },
14 | {
15 | name: "Cross Dissolve - image 2",
16 | effect: "cross-dissolve",
17 | image: require("../assets/images/react-green.png"),
18 | },
19 | {
20 | name: Platform.select({
21 | ios: "Flip From Right - image 3",
22 | default: "Cross Dissolve - image 3",
23 | }),
24 | effect: Platform.select({
25 | ios: "flip-from-right",
26 | default: "cross-dissolve",
27 | }),
28 | image: require("../assets/images/react-dark-blue.png"),
29 | },
30 | {
31 | name: Platform.select({
32 | ios: "Curl Down - image 4",
33 | default: "Cross Dissolve - image 4",
34 | }),
35 | effect: Platform.select({
36 | ios: "curl-down",
37 | default: "cross-dissolve",
38 | }),
39 | image: require("../assets/images/react-orange.png"),
40 | },
41 | ];
42 |
43 | export function ExpoImageDemo() {
44 | const [imageEffect, setImageEffect] = useState<(typeof effects)[number]>(
45 | effects[0],
46 | );
47 |
48 | const radioColor = useThemeColor({
49 | light: theme.colorBlack,
50 | dark: theme.colorWhite,
51 | });
52 |
53 | return (
54 | <>
55 |
61 | Expo Image Transitions
62 |
63 |
68 |
87 | {effects.map((effect) => (
88 | {
91 | setImageEffect(effect);
92 | }}
93 | key={effect.name}
94 | >
95 |
96 | {effect === imageEffect ? (
97 |
100 | ) : null}
101 |
102 |
103 |
104 | {effect.name || "None"}
105 |
106 |
107 | ))}
108 |
109 | >
110 | );
111 | }
112 |
113 | const styles = StyleSheet.create({
114 | image: {
115 | width: 250,
116 | height: 250,
117 | alignSelf: "center",
118 | },
119 | section: {
120 | padding: theme.space24,
121 | marginBottom: 200,
122 | },
123 | centered: {
124 | textAlign: "center",
125 | },
126 | option: {
127 | flexDirection: "row",
128 | marginBottom: theme.space8,
129 | alignItems: "center",
130 | },
131 | radioOuter: {
132 | height: 24,
133 | width: 24,
134 | borderWidth: 2,
135 | borderRadius: 24,
136 | marginRight: theme.space8,
137 | justifyContent: "center",
138 | alignItems: "center",
139 | },
140 | radioInner: {
141 | width: 12,
142 | height: 12,
143 | borderRadius: 12,
144 | },
145 | });
146 |
--------------------------------------------------------------------------------
/components/TalkCard.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "expo-router";
2 | import { StyleSheet, View } from "react-native";
3 |
4 | import { Bookmark } from "./Bookmark";
5 | import { SpeakerImage } from "./SpeakerImage";
6 | import { ThemedText, ThemedView, useThemeColor } from "./Themed";
7 | import { theme } from "../theme";
8 | import { Session, Speaker } from "../types";
9 | import { formatSessionTime } from "../utils/formatDate";
10 |
11 | import { useReactConfStore } from "@/store/reactConfStore";
12 | import { TouchableOpacity } from "react-native-gesture-handler";
13 |
14 | type Props = {
15 | session: Session;
16 | isDayOne: boolean;
17 | };
18 |
19 | export function TalkCard({ session, isDayOne }: Props) {
20 | const shouldUseLocalTz = useReactConfStore((state) => state.shouldUseLocalTz);
21 |
22 | const shadow = useThemeColor({ light: theme.dropShadow, dark: undefined });
23 |
24 | return (
25 |
33 |
34 |
39 |
48 |
49 |
50 | {formatSessionTime(session, shouldUseLocalTz)}
51 |
52 |
53 |
54 |
59 | {session.title}
60 |
61 |
62 |
71 | {session.speakers.map((speaker) => (
72 |
73 | ))}
74 |
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | function SpeakerDetails({ speaker }: { speaker: Speaker }) {
82 | return (
83 |
84 |
85 |
86 |
87 | {speaker.fullName}
88 |
89 |
90 | {speaker.tagLine}
91 |
92 |
93 |
94 | );
95 | }
96 |
97 | const styles = StyleSheet.create({
98 | container: {
99 | marginHorizontal: theme.space16,
100 | marginBottom: theme.space16,
101 | borderRadius: theme.borderRadius10,
102 | },
103 | heading: {
104 | borderTopRightRadius: theme.borderRadius10,
105 | borderTopLeftRadius: theme.borderRadius10,
106 | paddingHorizontal: theme.space12,
107 | paddingTop: theme.space12,
108 | },
109 | speaker: {
110 | flexDirection: "row",
111 | marginBottom: theme.space12,
112 | },
113 | speakerDetails: {
114 | flex: 1,
115 | justifyContent: "center",
116 | },
117 | content: {
118 | paddingTop: theme.space12,
119 | paddingHorizontal: theme.space12,
120 | borderBottomRightRadius: theme.borderRadius10,
121 | borderBottomLeftRadius: theme.borderRadius10,
122 | },
123 | timeAndBookmark: {
124 | flexDirection: "row",
125 | justifyContent: "space-between",
126 | },
127 | });
128 |
--------------------------------------------------------------------------------
/assets/bootsplash/ios/BootSplash.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 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/utils/sessions.test.ts:
--------------------------------------------------------------------------------
1 | import { formatSession } from "./sessions";
2 |
3 | import { allSessions } from "@/utils/testData/allSessions";
4 |
5 | describe(formatSession, () => {
6 | it("formats non-talk correctly", () => {
7 | const session = {
8 | id: "2e115a1f-803c-4c38-a0c9-a0c809457d51",
9 | title: "Registration",
10 | description: null,
11 | startsAt: "2024-05-15T01:00:00Z",
12 | endsAt: "2024-05-15T04:00:00Z",
13 | isServiceSession: true,
14 | isPlenumSession: false,
15 | speakers: [],
16 | categoryItems: [],
17 | questionAnswers: [],
18 | roomId: 45058,
19 | liveUrl: null,
20 | recordingUrl: null,
21 | status: null,
22 | isInformed: false,
23 | isConfirmed: false,
24 | };
25 |
26 | expect(formatSession(session, allSessions)).toEqual({
27 | id: "2e115a1f-803c-4c38-a0c9-a0c809457d51",
28 | title: "Registration",
29 | description: null,
30 | startsAt: "2024-05-15T01:00:00Z",
31 | endsAt: "2024-05-15T04:00:00Z",
32 | isServiceSession: true,
33 | room: "Lobby Hallway",
34 | speakers: [],
35 | });
36 | });
37 |
38 | it("formats a talk correcly", () => {
39 | const session = {
40 | id: "665505",
41 | title: "Vanilla React",
42 | description:
43 | "In 2014 Ryan and Michael first published React Router. Over the past decade, React Router has been the backbone of countless React apps, and has provided a stable foundation for anyone building with React. More recently, React Router has grown into a full stack framework with some help from Remix and Shopify. This talk will explore what we've done to keep React Router up to date as React evolves, and show off some of the latest developments we've been working on.",
44 | startsAt: "2024-05-15T17:15:00Z",
45 | endsAt: "2024-05-15T17:35:00Z",
46 | isServiceSession: false,
47 | isPlenumSession: false,
48 | speakers: ["e3e97117-b273-4cff-9c59-388b21a4b5b7"],
49 | categoryItems: [],
50 | questionAnswers: [],
51 | roomId: 45056,
52 | liveUrl: null,
53 | recordingUrl: null,
54 | status: "Accepted",
55 | isInformed: true,
56 | isConfirmed: false,
57 | };
58 | expect(formatSession(session, allSessions)).toEqual({
59 | description:
60 | "In 2014 Ryan and Michael first published React Router. Over the past decade, React Router has been the backbone of countless React apps, and has provided a stable foundation for anyone building with React. More recently, React Router has grown into a full stack framework with some help from Remix and Shopify. This talk will explore what we've done to keep React Router up to date as React evolves, and show off some of the latest developments we've been working on.",
61 | endsAt: "2024-05-15T17:35:00Z",
62 | id: "665505",
63 | isServiceSession: false,
64 | room: "Casablanca South",
65 | speakers: [
66 | {
67 | bio: "Obsessed with UX since using an Intellivision.",
68 | categoryItems: [],
69 | firstName: "Ryan",
70 | fullName: "Ryan Florence",
71 | id: "e3e97117-b273-4cff-9c59-388b21a4b5b7",
72 | isTopSpeaker: false,
73 | lastName: "Florence",
74 | links: [
75 | {
76 | linkType: "Twitter",
77 | title: "Twitter",
78 | url: "https://twitter.com/ryanflorence",
79 | },
80 | {
81 | linkType: "Blog",
82 | title: "Blog",
83 | url: "https://ryanflorence.com",
84 | },
85 | {
86 | linkType: "Company_Website",
87 | title: "Company Website",
88 | url: "https://remix.run",
89 | },
90 | ],
91 | profilePicture:
92 | "https://sessionize.com/image/8295-400o400o1-96d9fe36-2eb4-43ed-8f63-83765aa47767.jpg",
93 | questionAnswers: [
94 | {
95 | answerValue: "ryanflorence",
96 | questionId: 70280,
97 | },
98 | ],
99 | sessions: [665505],
100 | tagLine: "Co-creator of Remix",
101 | },
102 | ],
103 | startsAt: "2024-05-15T17:15:00Z",
104 | title: "Vanilla React",
105 | });
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/app/(tabs)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import Feather from "@expo/vector-icons/build/Feather";
2 | import Ionicons from "@expo/vector-icons/build/Ionicons";
3 | import Octicons from "@expo/vector-icons/build/Octicons";
4 | import MaterialCommunityIcons from "@expo/vector-icons/build/MaterialCommunityIcons";
5 | import { Tabs } from "expo-router";
6 | import React from "react";
7 |
8 | import { TabBarButton } from "@/components/TabBarButton";
9 | import { ThemedText, useThemeColor } from "@/components/Themed";
10 | import { theme } from "@/theme";
11 | import { useBookmarkStore } from "@/store/bookmarkStore";
12 |
13 | export default function TabLayout() {
14 | const tabBarBackgroundColor = useThemeColor({
15 | light: theme.colorWhite,
16 | dark: theme.colorDarkestBlue,
17 | });
18 |
19 | const tabBarActiveTintColor = useThemeColor({
20 | light: theme.colorReactDarkBlue,
21 | dark: theme.colorWhite,
22 | });
23 |
24 | const tabBarInactiveTintColor = useThemeColor({
25 | light: theme.colorGrey,
26 | dark: `rgba(255, 255, 255, 0.35)`,
27 | });
28 |
29 | return (
30 |
39 | (
44 | (
49 |
50 | )}
51 | />
52 | ),
53 | }}
54 | />
55 | (
62 |
63 | Bookmarked sessions
64 |
65 | ),
66 | tabBarButton: (props) => (
67 | }
72 | />
73 | ),
74 | }}
75 | />
76 | (
84 |
85 | Speakers
86 |
87 | ),
88 | tabBarButton: (props) => (
89 | (
94 |
95 | )}
96 | />
97 | ),
98 | }}
99 | />
100 | (
107 |
108 | Info
109 |
110 | ),
111 | tabBarButton: (props) => (
112 | (
117 |
118 | )}
119 | />
120 | ),
121 | }}
122 | />
123 |
124 | );
125 | }
126 |
127 | const BookmarkIcon = ({ color }: { color: string }) => {
128 | const bookmarks = useBookmarkStore((state) => state.bookmarks);
129 | return (
130 |
135 | );
136 | };
137 |
--------------------------------------------------------------------------------
/assets/images/react-logo.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/app.config.js:
--------------------------------------------------------------------------------
1 | // Update this value to something unique in order to be able to build for a
2 | // physical iOS device.
3 | const APP_ID_PREFIX = "com.reactconf";
4 |
5 | // These values are tied to EAS. If you would like to use EAS Build or Update
6 | // on this project while playing with it, then remove these values and run
7 | // `eas init` and `eas update:configure` to get new values for your account.
8 | const EAS_UPDATE_URL =
9 | "https://u.expo.dev/66251e1b-0290-4ef8-87a4-e533cac914dd";
10 | const EAS_PROJECT_ID = "66251e1b-0290-4ef8-87a4-e533cac914dd";
11 | const EAS_APP_OWNER = "expo";
12 |
13 | // If you change this value, run `npx expo prebuild --clean` afterwards if you
14 | // are building the project locally.
15 | const IS_NEW_ARCH_ENABLED = false;
16 |
17 | const IS_DEV = process.env.APP_VARIANT === "development";
18 | const IS_PREVIEW = process.env.APP_VARIANT === "preview";
19 |
20 | const getName = () => {
21 | if (IS_DEV) {
22 | return "React Conf (Dev)";
23 | }
24 |
25 | if (IS_PREVIEW) {
26 | return "React Conf (Prev)";
27 | }
28 |
29 | return "React Conf";
30 | };
31 |
32 | const getAppId = () => {
33 | if (IS_DEV) {
34 | return `${APP_ID_PREFIX}.dev`;
35 | }
36 |
37 | if (IS_PREVIEW) {
38 | return `${APP_ID_PREFIX}.preview`;
39 | }
40 |
41 | return `${APP_ID_PREFIX}.app`;
42 | };
43 |
44 | export default {
45 | expo: {
46 | name: getName(),
47 | slug: "react-conf-app",
48 | version: "1.0.3",
49 | orientation: "portrait",
50 | icon: "./assets/icon.png",
51 | userInterfaceStyle: "automatic",
52 | scheme: "reactconfapp",
53 | assetBundlePatterns: ["**/*"],
54 | ios: {
55 | supportsTablet: true,
56 | bundleIdentifier: getAppId(),
57 | userInterfaceStyle: "automatic",
58 | config: {
59 | usesNonExemptEncryption: false,
60 | },
61 | },
62 | android: {
63 | adaptiveIcon: {
64 | foregroundImage: "./assets/icon-android-foreground.png",
65 | monochromeImage: "./assets/icon-android-foreground.png",
66 | backgroundColor: "#051726",
67 | },
68 | userInterfaceStyle: "automatic",
69 | package: getAppId(),
70 | },
71 | web: {
72 | favicon: "./assets/favicon.png",
73 | },
74 | extra: {
75 | eas: {
76 | projectId: EAS_PROJECT_ID,
77 | },
78 | },
79 | owner: EAS_APP_OWNER,
80 | plugins: [
81 | [
82 | "expo-build-properties",
83 | {
84 | ios: {
85 | newArchEnabled: IS_NEW_ARCH_ENABLED,
86 | },
87 | android: {
88 | newArchEnabled: IS_NEW_ARCH_ENABLED,
89 | },
90 | },
91 | ],
92 | [
93 | "expo-quick-actions",
94 | {
95 | androidIcons: {
96 | gift: {
97 | foregroundImage: "./assets/icons/gift.png",
98 | backgroundColor: "#FFFFFF",
99 | },
100 | },
101 | },
102 | ],
103 | "expo-router",
104 | [
105 | "@config-plugins/react-native-dynamic-app-icon",
106 | ["./assets/icon.png", "./assets/icons/icon-desert.png"],
107 | ],
108 | [
109 | "expo-font",
110 | {
111 | fonts: [
112 | "./assets/fonts/FreightSansProBlack-Italic.ttf",
113 | "./assets/fonts/FreightSansProBlack-Regular.ttf",
114 | "./assets/fonts/FreightSansProBold-Italic.ttf",
115 | "./assets/fonts/FreightSansProBold-Regular.ttf",
116 | "./assets/fonts/FreightSansProBook-Italic.ttf",
117 | "./assets/fonts/FreightSansProBook-Regular.ttf",
118 | "./assets/fonts/FreightSansProLight-Italic.ttf",
119 | "./assets/fonts/FreightSansProLight-Regular.ttf",
120 | "./assets/fonts/FreightSansProMedium-Italic.ttf",
121 | "./assets/fonts/FreightSansProMedium-Regular.ttf",
122 | "./assets/fonts/FreightSansProSemibold-Italic.ttf",
123 | "./assets/fonts/FreightSansProSemibold-Regular.ttf",
124 | ],
125 | },
126 | ],
127 | [
128 | "react-native-bootsplash",
129 | {
130 | android: {
131 | parentTheme: "TransparentStatus",
132 | darkContentBarsStyle: false,
133 | },
134 | },
135 | ],
136 | ],
137 | updates: {
138 | url: EAS_UPDATE_URL,
139 | // Configure the channel to "local" for local development, if we
140 | // compile/run locally EAS Build will configure this for us automatically
141 | // based on the value provided in the build profile, and that will
142 | // overwrite this value.
143 | requestHeaders: {
144 | "expo-channel-name": "local",
145 | },
146 | },
147 | runtimeVersion: {
148 | policy: "appVersion",
149 | },
150 | },
151 | };
152 |
--------------------------------------------------------------------------------
/assets/images/sponsor-abbott.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/utils/useScrollToTopWithOffset.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EventArg,
3 | NavigationContext,
4 | NavigationProp,
5 | ParamListBase,
6 | useRoute,
7 | } from "@react-navigation/core";
8 | import * as React from "react";
9 | import type { ScrollView } from "react-native";
10 |
11 | type ScrollOptions = { x?: number; y?: number; animated?: boolean };
12 |
13 | type ScrollableView =
14 | | { scrollToTop(): void }
15 | | { scrollTo(options: ScrollOptions): void }
16 | | { scrollToOffset(options: { offset?: number; animated?: boolean }): void }
17 | | { scrollResponderScrollTo(options: ScrollOptions): void };
18 |
19 | type ScrollableWrapper =
20 | | { getScrollResponder(): React.ReactNode | ScrollView }
21 | | { getNode(): ScrollableView }
22 | | ScrollableView;
23 |
24 | function getScrollableNode(ref: React.RefObject) {
25 | if (ref.current == null) {
26 | return null;
27 | }
28 |
29 | if (
30 | "scrollToTop" in ref.current ||
31 | "scrollTo" in ref.current ||
32 | "scrollToOffset" in ref.current ||
33 | "scrollResponderScrollTo" in ref.current
34 | ) {
35 | // This is already a scrollable node.
36 | return ref.current;
37 | } else if ("getScrollResponder" in ref.current) {
38 | // If the view is a wrapper like FlatList, SectionList etc.
39 | // We need to use `getScrollResponder` to get access to the scroll responder
40 | return ref.current.getScrollResponder();
41 | } else if ("getNode" in ref.current) {
42 | // When a `ScrollView` is wraped in `Animated.createAnimatedComponent`
43 | // we need to use `getNode` to get the ref to the actual scrollview.
44 | // Note that `getNode` is deprecated in newer versions of react-native
45 | // this is why we check if we already have a scrollable node above.
46 | return ref.current.getNode();
47 | } else {
48 | return ref.current;
49 | }
50 | }
51 |
52 | export default function useScrollToTopWithOffset(
53 | ref: React.RefObject,
54 | offset: number = 0,
55 | ) {
56 | const navigation = React.useContext(NavigationContext);
57 | const route = useRoute();
58 |
59 | if (navigation === undefined) {
60 | throw new Error(
61 | "Couldn't find a navigation object. Is your component inside NavigationContainer?",
62 | );
63 | }
64 |
65 | React.useEffect(() => {
66 | const tabNavigations: NavigationProp[] = [];
67 | let currentNavigation = navigation;
68 | // If the screen is nested inside multiple tab navigators, we should scroll to top for any of them
69 | // So we need to find all the parent tab navigators and add the listeners there
70 | while (currentNavigation) {
71 | if (currentNavigation.getState().type === "tab") {
72 | tabNavigations.push(currentNavigation);
73 | }
74 |
75 | currentNavigation = currentNavigation.getParent();
76 | }
77 |
78 | if (tabNavigations.length === 0) {
79 | return;
80 | }
81 |
82 | const unsubscribers = tabNavigations.map((tab) => {
83 | return tab.addListener(
84 | // We don't wanna import tab types here to avoid extra deps
85 | // in addition, there are multiple tab implementations
86 | // @ts-expect-error
87 | "tabPress",
88 | (e: EventArg<"tabPress", true>) => {
89 | // We should scroll to top only when the screen is focused
90 | const isFocused = navigation.isFocused();
91 |
92 | // In a nested stack navigator, tab press resets the stack to first screen
93 | // So we should scroll to top only when we are on first screen
94 | const isFirst =
95 | tabNavigations.includes(navigation) ||
96 | navigation.getState().routes[0].key === route.key;
97 |
98 | // Run the operation in the next frame so we're sure all listeners have been run
99 | // This is necessary to know if preventDefault() has been called
100 | requestAnimationFrame(() => {
101 | const scrollable = getScrollableNode(ref) as ScrollableWrapper;
102 |
103 | if (isFocused && isFirst && scrollable && !e.defaultPrevented) {
104 | if ("scrollToTop" in scrollable) {
105 | scrollable.scrollToTop();
106 | } else if ("scrollTo" in scrollable) {
107 | scrollable.scrollTo({ y: offset, animated: true });
108 | } else if ("scrollToOffset" in scrollable) {
109 | scrollable.scrollToOffset({ offset: offset, animated: true });
110 | } else if ("scrollResponderScrollTo" in scrollable) {
111 | scrollable.scrollResponderScrollTo({
112 | y: offset,
113 | animated: true,
114 | });
115 | }
116 | }
117 | });
118 | },
119 | );
120 | });
121 |
122 | return () => {
123 | unsubscribers.forEach((unsubscribe) => unsubscribe());
124 | };
125 | }, [navigation, ref, route.key, offset]);
126 | }
127 |
--------------------------------------------------------------------------------
/components/SponsorsInfo.tsx:
--------------------------------------------------------------------------------
1 | import { Image, ImageSource, ImageStyle } from "expo-image";
2 | import { StyleSheet, View, ViewStyle } from "react-native";
3 | import openWebBrowserAsync from "@/utils/openWebBrowserAsync";
4 | import { TouchableOpacity } from "react-native-gesture-handler";
5 |
6 | import { InfoSection } from "./InfoSection";
7 | import { ThemedText, ThemedView } from "./Themed";
8 |
9 | import { theme } from "@/theme";
10 |
11 | const logoHeight = 40;
12 |
13 | const sponsors = {
14 | remixSpotify: {
15 | image: require("../assets/images/sponsor-remix-spotify.svg"),
16 | url: "https://remix.run/",
17 | },
18 | mui: {
19 | image: require("../assets/images/sponsor-mui.svg"),
20 | url: "https://mui.com/",
21 | },
22 | sentry: {
23 | image: require("../assets/images/sponsor-sentry.svg"),
24 | url: "https://sentry.io/for/react/?utm_source=sponsored-conf&utm_medium=sponsored-event&utm_campaign=frontend-fy25q2-evergreen&utm_content=logo-reactconf2024-learnmore",
25 | },
26 | expo: {
27 | image: require("../assets/images/sponsor-expo.svg"),
28 | url: "https://expo.dev/",
29 | },
30 | redwood: {
31 | image: require("../assets/images/sponsor-redwood.svg"),
32 | url: "https://redwoodjs.com/",
33 | },
34 | vercel: {
35 | image: require("../assets/images/sponsor-vercel.svg"),
36 | url: "https://vercel.com/",
37 | },
38 | abbott: {
39 | image: require("../assets/images/sponsor-abbott.svg"),
40 | url: "https://www.jobs.abbott/software",
41 | },
42 | amazon: {
43 | image: require("../assets/images/sponsor-amazon.webp"),
44 | url: "https://developer.amazon.com/apps-and-games?cmp=US_2024_05_3P_React-Conf-2024&ch=prtnr&chlast=prtnr&pub=ref&publast=ref&type=org&typelast=org",
45 | },
46 | };
47 |
48 | export function SponsorsInfo() {
49 | return (
50 |
51 |
52 | Diamond
53 |
54 |
58 |
59 | Platinum
60 |
61 |
62 |
63 | Gold
64 |
65 |
66 |
70 |
74 |
75 |
76 |
77 |
78 |
79 | Silver
80 |
81 |
82 |
86 |
90 |
91 |
92 | Community Sponsor
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
100 |
101 | const SponsorImage = ({
102 | sponsor,
103 | style,
104 | imageStyle,
105 | }: {
106 | sponsor: {
107 | url: string;
108 | image: ImageSource;
109 | };
110 | style?: ViewStyle;
111 | imageStyle?: ImageStyle;
112 | }) => {
113 | return (
114 |
119 | openWebBrowserAsync(sponsor.url)}
122 | style={styles.imageContent}
123 | >
124 |
129 |
130 |
131 | );
132 | };
133 |
134 | const styles = StyleSheet.create({
135 | twoSponsorContainer: {
136 | flexDirection: "row",
137 | justifyContent: "space-around",
138 | },
139 | heading: {
140 | marginTop: theme.space24,
141 | marginBottom: theme.space12,
142 | textAlign: "center",
143 | },
144 | firstHeading: {
145 | marginBottom: theme.space16,
146 | textAlign: "center",
147 | },
148 | imageContainer: {
149 | borderRadius: theme.borderRadius10,
150 | },
151 | imageContent: {
152 | padding: theme.space16,
153 | },
154 | smallImageContainer: {
155 | flex: 1,
156 | marginRight: theme.space12,
157 | },
158 | mainImage: {
159 | height: logoHeight * 2,
160 | width: "100%",
161 | },
162 | image: {
163 | height: logoHeight,
164 | width: "100%",
165 | },
166 | halfWidth: {
167 | width: "50%",
168 | },
169 | });
170 |
--------------------------------------------------------------------------------
/app/speaker/[speakerId].tsx:
--------------------------------------------------------------------------------
1 | import Feather from "@expo/vector-icons/build/Feather";
2 | import Ionicons from "@expo/vector-icons/build/Ionicons";
3 | import { Image } from "expo-image";
4 | import { useNavigation, useLocalSearchParams } from "expo-router";
5 | import React, { useEffect } from "react";
6 | import { StyleSheet, View } from "react-native";
7 | import openWebBrowserAsync from "@/utils/openWebBrowserAsync";
8 | import { ScrollView } from "react-native-gesture-handler";
9 |
10 | import { IconButton } from "@/components/IconButton";
11 | import { MiniTalkCard } from "@/components/MiniTalkCard";
12 | import { NotFound } from "@/components/NotFound";
13 | import { SpeakerImage } from "@/components/SpeakerImage";
14 | import { ThemedText, ThemedView, useThemeColor } from "@/components/Themed";
15 | import { useReactConfStore } from "@/store/reactConfStore";
16 | import { theme } from "@/theme";
17 | import { Speaker } from "@/types";
18 |
19 | export default function SpeakerDetail() {
20 | const params = useLocalSearchParams();
21 | const speakers = useReactConfStore((state) => state.allSessions.speakers);
22 | const speaker = speakers.find((speaker) => speaker.id === params.speakerId);
23 | const navigation = useNavigation();
24 |
25 | useEffect(() => {
26 | navigation.setOptions({ title: speaker?.fullName });
27 | }, [speaker, navigation]);
28 |
29 | return (
30 |
35 | {speaker ? (
36 |
40 |
41 |
46 | {speaker.tagLine ? (
47 |
53 | {speaker.tagLine}
54 |
55 | ) : null}
56 |
57 | {speaker.links.length ? : null}
58 | {speaker.bio ? (
59 |
66 | {speaker.bio}
67 |
68 | ) : null}
69 | {speaker.sessions.map((sessionId) => (
70 |
71 | ))}
72 |
73 | ) : (
74 |
75 | )}
76 |
77 | );
78 | }
79 |
80 | function Socials({ speaker }: { speaker: Speaker }) {
81 | const iconColor = useThemeColor({
82 | light: theme.colorBlack,
83 | dark: theme.colorWhite,
84 | });
85 | return (
86 |
87 | {speaker.links.map((link) => {
88 | const icon = (() => {
89 | switch (link.linkType) {
90 | case "Twitter": {
91 | return (
92 |
97 | );
98 | }
99 | case "LinkedIn": {
100 | return (
101 |
106 | );
107 | }
108 | case "Blog": {
109 | return (
110 |
116 | );
117 | }
118 | case "Company_Website": {
119 | return (
120 |
126 | );
127 | }
128 | }
129 | })();
130 |
131 | if (!icon) {
132 | return null;
133 | }
134 |
135 | return (
136 | openWebBrowserAsync(link.url)}
138 | key={link.title}
139 | >
140 | {icon}
141 |
142 | );
143 | })}
144 |
145 | );
146 | }
147 |
148 | const styles = StyleSheet.create({
149 | container: {
150 | flex: 1,
151 | },
152 | contentContainer: {
153 | padding: theme.space16,
154 | paddingTop: theme.space24,
155 | borderBottomRightRadius: theme.borderRadius20,
156 | borderBottomLeftRadius: theme.borderRadius20,
157 | },
158 | centered: {
159 | alignItems: "center",
160 | },
161 | tagLine: {
162 | marginBottom: theme.space24,
163 | },
164 | speakerImage: {
165 | marginBottom: theme.space24,
166 | },
167 | icon: {
168 | height: 20,
169 | width: 20,
170 | },
171 | socials: {
172 | marginBottom: theme.space24,
173 | flexDirection: "row",
174 | justifyContent: "center",
175 | },
176 | });
177 |
--------------------------------------------------------------------------------
/assets/images/callstack-logo.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DarkTheme,
3 | DefaultTheme,
4 | ThemeProvider,
5 | } from "@react-navigation/native";
6 | import { differenceInMinutes } from "date-fns";
7 | import * as QuickActions from "expo-quick-actions";
8 | import { usePathname, useRouter } from "expo-router";
9 | import { Stack } from "expo-router/stack";
10 | import { StatusBar } from "expo-status-bar";
11 | import { useEffect, useState } from "react";
12 | import { Platform, useColorScheme } from "react-native";
13 | import { setBackgroundColorAsync } from "expo-system-ui";
14 | import { ActionSheetProvider } from "@expo/react-native-action-sheet";
15 | import { GestureHandlerRootView } from "react-native-gesture-handler";
16 | import * as Notifications from "expo-notifications";
17 |
18 | import { theme } from "../theme";
19 |
20 | import { BackButton } from "@/components/BackButton";
21 | import { OfflineBanner } from "@/components/OfflineBanner";
22 | import { ThemedText, useThemeColor } from "@/components/Themed";
23 | import { useReactConfStore } from "@/store/reactConfStore";
24 | import { AnimatedBootSplash } from "@/components/AnimatedBootSplash";
25 | import { useQuickActionCallback } from "@/utils/useQuickActionCallback";
26 |
27 | Notifications.setNotificationHandler({
28 | handleNotification: async () => ({
29 | shouldShowAlert: true,
30 | shouldPlaySound: false,
31 | shouldSetBadge: false,
32 | }),
33 | });
34 |
35 | export default function Layout() {
36 | const [splashVisible, setSplashVisible] = useState(true);
37 | const router = useRouter();
38 | const pathName = usePathname();
39 | const colorScheme = useColorScheme() || "light";
40 |
41 | const { refreshData, lastRefreshed } = useReactConfStore();
42 |
43 | const tabBarBackgroundColor = useThemeColor({
44 | light: theme.colorWhite,
45 | dark: theme.colorDarkestBlue,
46 | });
47 |
48 | // Keep the root view background color in sync with the current theme
49 | useEffect(() => {
50 | setBackgroundColorAsync(
51 | colorScheme === "dark" ? theme.colorDarkestBlue : theme.colorWhite,
52 | );
53 | }, [colorScheme]);
54 |
55 | useEffect(() => {
56 | QuickActions.setItems([
57 | {
58 | title: "Just one more thing",
59 | subtitle: "Return to app...",
60 | icon: Platform.OS === "ios" ? "symbol:gift" : "gift",
61 | id: "0",
62 | params: { href: "/secretModal" },
63 | },
64 | ]);
65 | }, []);
66 |
67 | const lastNotificationResponse = Notifications.useLastNotificationResponse();
68 | useEffect(() => {
69 | if (
70 | lastNotificationResponse &&
71 | lastNotificationResponse.actionIdentifier ===
72 | Notifications.DEFAULT_ACTION_IDENTIFIER
73 | ) {
74 | try {
75 | const url =
76 | lastNotificationResponse.notification.request.content.data.url;
77 | if (pathName !== url) {
78 | router.push(url);
79 | }
80 | } catch {}
81 | }
82 | // eslint-disable-next-line react-hooks/exhaustive-deps
83 | }, [lastNotificationResponse]);
84 |
85 | useQuickActionCallback((action) => {
86 | const href = action.params?.href;
87 | if (href && typeof href === "string") {
88 | router.navigate(href);
89 | }
90 | });
91 |
92 | useEffect(() => {
93 | const fetchData = async () => {
94 | if (
95 | !lastRefreshed ||
96 | differenceInMinutes(new Date(), new Date(lastRefreshed)) > 5
97 | ) {
98 | await refreshData();
99 | }
100 | };
101 |
102 | fetchData();
103 | }, [lastRefreshed, refreshData]);
104 |
105 | return (
106 |
107 |
108 | {
111 | setSplashVisible(false);
112 | }}
113 | >
114 |
117 |
118 |
119 |
120 |
126 |
130 | Platform.OS === "ios" ? : null,
131 | title: "",
132 | headerTransparent: true,
133 | // `headerBlurEffect` prop does not work on New Architecture at the moment
134 | // headerBlurEffect: "systemUltraThinMaterialLight",
135 | presentation: "modal",
136 | }}
137 | />
138 |
143 | Platform.OS === "ios" ? : null,
144 | headerStyle: {
145 | backgroundColor: tabBarBackgroundColor,
146 | },
147 | headerTitleAlign: "center",
148 | headerTitle: (props) => (
149 |
150 | {props.children}
151 |
152 | ),
153 | }}
154 | />
155 | (
162 |
163 | {props.children}
164 |
165 | ),
166 | ...(colorScheme === "dark"
167 | ? {
168 | headerStyle: { backgroundColor: theme.colorDarkBlue },
169 | headerTitleStyle: { color: "white" },
170 | }
171 | : {}),
172 | }}
173 | />
174 |
175 |
176 |
177 |
178 |
179 |
180 | );
181 | }
182 |
--------------------------------------------------------------------------------
/assets/images/sponsor-remix-spotify.svg:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/assets/images/sponsor-redwood.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------