Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(fix) Top Margin & Scroll Flash #189

Merged
merged 6 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 4 additions & 117 deletions app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,17 @@
import "./i18n"
import "./utils/ignoreWarnings"
import { useFonts } from "expo-font"
import React, { useLayoutEffect, useState } from "react"
import {
initialWindowMetrics,
SafeAreaProvider,
useSafeAreaInsets,
} from "react-native-safe-area-context"
import * as Updates from "expo-updates"
import React, { useLayoutEffect } from "react"
import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context"
import { AppNavigator, useNavigationPersistence } from "./navigators"
import { ErrorBoundary } from "./screens/ErrorScreen/ErrorBoundary"
import * as storage from "./utils/storage"
import { colors, customFontsToLoad, spacing } from "./theme"
import { customFontsToLoad } from "./theme"
import { setupReactotron } from "./services/reactotron/reactotron"
import Config from "./config"
import { QueryClientProvider } from "@tanstack/react-query"
import messaging from "@react-native-firebase/messaging"
import Toast, { BaseToast, ToastConfig } from "react-native-toast-message"
import { $baseSecondaryStyle, $baseStyle } from "./components"
import { Dimensions, ViewStyle } from "react-native"
import { queryClient } from "./services/api/react-query"
import { reportCrash } from "./utils/crashReporting"
import { useAppState } from "./hooks"
import { Modal } from "./components/Modal"
import { CustomToast, OTAUpdates } from "./components"

// Set up Reactotron, which is a free desktop app for inspecting and debugging
// React Native apps. Learn more here: https://github.com/infinitered/reactotron
Expand All @@ -56,96 +45,6 @@ interface AppProps {
hideSplashScreen: () => Promise<void>
}

// Setting up our custom Toast component
const CustomToast = () => {
const insets = useSafeAreaInsets()

useLayoutEffect(() => {
// handle a new push notification received while the app is in "foreground" state
const unsubscribe = messaging().onMessage(async (remoteMessage) => {
if (
remoteMessage.notification &&
(remoteMessage.notification.title || remoteMessage.notification.body)
) {
Toast.show({
text1: remoteMessage.notification.title,
text2: remoteMessage.notification.body,
})
}
})
return unsubscribe
})

const toastConfig: ToastConfig = {
success: (props) => (
<BaseToast
{...props}
contentContainerStyle={$toastContainer}
style={$toast}
text1Style={$baseStyle}
text2Style={$baseSecondaryStyle}
/>
),
}

return <Toast config={toastConfig} topOffset={insets.top} />
}

// Setting up our OTA Updates component
const OTAUpdates = () => {
const [isModalVisible, setIsModalVisible] = useState(false)
const [isUpdating, setIsUpdating] = useState(false)

async function fetchAndRestartApp() {
const fetchUpdate = await Updates.fetchUpdateAsync()
if (fetchUpdate.isNew) {
await Updates.reloadAsync()
} else {
setIsModalVisible(false)
setIsUpdating(false)
reportCrash("Fetch Update failed")
}
}

async function onFetchUpdateAsync() {
if (__DEV__ || process.env.NODE_ENV === "development") return
try {
const update = await Updates.checkForUpdateAsync()
setIsModalVisible(update.isAvailable)
} catch (error) {
reportCrash(error)
}
}

useAppState({
match: /background/,
nextAppState: "active",
callback: onFetchUpdateAsync,
})

return (
<Modal
title="ota.title"
subtitle="ota.subtitle"
confirmOnPress={{
cta: async () => {
setIsUpdating(true)
await fetchAndRestartApp()
},
label: isUpdating ? "ota.confirmLabelUpdating" : "ota.confirmLabel",
disabled: isUpdating,
}}
cancelOnPress={{
cta: () => {
setIsModalVisible(false)
},
label: "ota.cancelLabel",
}}
isVisible={isModalVisible}
/>
)
}

/**
* This is the root component of our app.
*/
Expand Down Expand Up @@ -196,15 +95,3 @@ function App(props: AppProps) {
}

export default App

const $toast: ViewStyle = {
backgroundColor: colors.palette.neutral400,
borderLeftWidth: 0,
borderRadius: spacing.extraSmall,
width: Dimensions.get("window").width - spacing.extraSmall * 2,
}

const $toastContainer: ViewStyle = {
paddingHorizontal: spacing.large,
paddingVertical: spacing.medium,
}
54 changes: 54 additions & 0 deletions app/components/CustomToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useLayoutEffect } from "react"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { colors, spacing } from "../theme"
import messaging from "@react-native-firebase/messaging"
import Toast, { BaseToast, ToastConfig } from "react-native-toast-message"
import { $baseSecondaryStyle, $baseStyle } from "./"
import { Dimensions, ViewStyle } from "react-native"

// Setting up our custom Toast component
export const CustomToast = () => {
const insets = useSafeAreaInsets()

useLayoutEffect(() => {
// handle a new push notification received while the app is in "foreground" state
const unsubscribe = messaging().onMessage(async (remoteMessage) => {
if (
remoteMessage.notification &&
(remoteMessage.notification.title || remoteMessage.notification.body)
) {
Toast.show({
text1: remoteMessage.notification.title,
text2: remoteMessage.notification.body,
})
}
})
return unsubscribe
})

const toastConfig: ToastConfig = {
success: (props) => (
<BaseToast
{...props}
contentContainerStyle={$toastContainer}
style={$toast}
text1Style={$baseStyle}
text2Style={$baseSecondaryStyle}
/>
),
}

return <Toast config={toastConfig} topOffset={insets.top} />
}

const $toast: ViewStyle = {
backgroundColor: colors.palette.neutral400,
borderLeftWidth: 0,
borderRadius: spacing.extraSmall,
width: Dimensions.get("window").width - spacing.extraSmall * 2,
}

const $toastContainer: ViewStyle = {
paddingHorizontal: spacing.large,
paddingVertical: spacing.medium,
}
60 changes: 60 additions & 0 deletions app/components/OTAUpdates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useState } from "react"
import * as Updates from "expo-updates"
import { reportCrash } from "../utils/crashReporting"
import { useAppState } from "../hooks"
import { Modal } from "./Modal"

// Setting up our OTA Updates component
export const OTAUpdates = () => {
const [isModalVisible, setIsModalVisible] = useState(false)
const [isUpdating, setIsUpdating] = useState(false)

async function fetchAndRestartApp() {
const fetchUpdate = await Updates.fetchUpdateAsync()
if (fetchUpdate.isNew) {
await Updates.reloadAsync()
} else {
setIsModalVisible(false)
setIsUpdating(false)
reportCrash("Fetch Update failed")
}
}

async function onFetchUpdateAsync() {
if (__DEV__ || process.env.NODE_ENV === "development") return
try {
const update = await Updates.checkForUpdateAsync()
setIsModalVisible(update.isAvailable)
} catch (error) {
reportCrash(error)
}
}

useAppState({
match: /background/,
nextAppState: "active",
callback: onFetchUpdateAsync,
})

return (
<Modal
title="ota.title"
subtitle="ota.subtitle"
confirmOnPress={{
cta: async () => {
setIsUpdating(true)
await fetchAndRestartApp()
},
label: isUpdating ? "ota.confirmLabelUpdating" : "ota.confirmLabel",
disabled: isUpdating,
}}
cancelOnPress={{
cta: () => {
setIsModalVisible(false)
},
label: "ota.cancelLabel",
}}
isVisible={isModalVisible}
/>
)
}
3 changes: 3 additions & 0 deletions app/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ export * from "./Button"
export * from "./ButtonLink"
export * from "./Card"
export * from "./carousel/Carousel"
export * from "./CustomToast"
export * from "./EmptyState"
export * from "./Header"
export * from "./Icon"
export * from "./IconButton"
export * from "./ListItem"
export * from "./MediaButton"
export * from "./Modal"
export * from "./OTAUpdates"
export * from "./Screen"
export * from "./ScrollToButton"
export * from "./Tag"
Expand Down
97 changes: 53 additions & 44 deletions app/screens/ChatScreen/ChatScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { translate } from "../../i18n"
import { GiftedChat, IMessage } from "react-native-gifted-chat"
import AsyncStorage from "@react-native-async-storage/async-storage"
import { aiPrompt } from "./ai"
import { View, ViewStyle } from "react-native"

const NUMBER_OF_MESSAGES_TO_SEND = 8
const chatbotAvatarURL =
Expand Down Expand Up @@ -87,51 +88,59 @@ export const ChatScreen: React.FunctionComponent<TabScreenProps<"Chat">> = () =>
return (
<>
{uuid && (
<GiftedChat
messages={messages}
isTyping={typingIndicator}
onSend={async (newMessages) => {
const appendedMessages = [
...newMessages.map((message) => ({ ...message, _id: Date.now() })),
...messages,
]
saveMessages(() => appendedMessages)
setTypingIndicator(true)

// ask Claude for AI response
// first, build the prompt using the last 15 messages
const prompt = appendedMessages
.slice(0, NUMBER_OF_MESSAGES_TO_SEND)
.reverse()
.map(
(message) => (message.user._id === uuid ? "Human: " : "Assistant: ") + message.text,
)
.join("\n\n")

// turn on the GiftedChat "typing" indicator
// then, ask Claude for a response
const response = await aiPrompt({ prompt, userId: uuid })
const claudeMessage = {
_id: Date.now(),
text: response.completion.trim(),
createdAt: new Date(),
user: {
_id: 2,
name: chatbotName,
avatar: chatbotAvatarURL,
},
}
setTypingIndicator(false)
saveMessages(() => [claudeMessage, ...appendedMessages])
}}
user={{
_id: uuid,
}}
textInputProps={{
testID: "aiChatInput",
}}
/>
<View style={$root}>
<GiftedChat
messages={messages}
isTyping={typingIndicator}
onSend={async (newMessages) => {
const appendedMessages = [
...newMessages.map((message) => ({ ...message, _id: Date.now() })),
...messages,
]
saveMessages(() => appendedMessages)
setTypingIndicator(true)

// ask Claude for AI response
// first, build the prompt using the last 15 messages
const prompt = appendedMessages
.slice(0, NUMBER_OF_MESSAGES_TO_SEND)
.reverse()
.map(
(message) =>
(message.user._id === uuid ? "Human: " : "Assistant: ") + message.text,
)
.join("\n\n")

// turn on the GiftedChat "typing" indicator
// then, ask Claude for a response
const response = await aiPrompt({ prompt, userId: uuid })
const claudeMessage = {
_id: Date.now(),
text: response.completion.trim(),
createdAt: new Date(),
user: {
_id: 2,
name: chatbotName,
avatar: chatbotAvatarURL,
},
}
setTypingIndicator(false)
saveMessages(() => [claudeMessage, ...appendedMessages])
}}
user={{
_id: uuid,
}}
textInputProps={{
testID: "aiChatInput",
}}
listViewProps={{ keyboardDismissMode: "on-drag" }}
/>
</View>
)}
</>
)
}

const $root: ViewStyle = {
flex: 1,
}
1 change: 0 additions & 1 deletion app/screens/InfoScreen/CodeOfConductScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,5 @@ const $phoneNumber: TextStyle = {
}

const $codeOfConductHeading: TextStyle = {
marginTop: spacing.extraLarge * 2,
marginBottom: spacing.extraSmall,
}
Loading