Skip to content

Commit

Permalink
Enhance theme and improve chat scrolling behavior (#153)
Browse files Browse the repository at this point in the history
* feat: convert benchmark profile sliders to use discrete binary steps

* chore: update theme

* feat: stop auto-scrolling when user manually scrolls chat & add scroll button
  • Loading branch information
a-ghorbani authored Dec 29, 2024
1 parent 62c9126 commit b884b23
Show file tree
Hide file tree
Showing 19 changed files with 383 additions and 234 deletions.
38 changes: 30 additions & 8 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
} from 'react-native-gesture-handler';

import {useTheme} from './src/hooks';
import {modelStore} from './src/store';
import {Theme} from './src/utils/types';
import {modelStore, chatSessionStore} from './src/store';
import {HeaderRight, SidebarContent, ModelsHeaderRight} from './src/components';
import {
ChatScreen,
Expand All @@ -40,6 +41,7 @@ const App = observer(() => {
}, []);

const theme = useTheme();
const styles = createStyles(theme);

return (
<GestureHandlerRootView style={styles.root}>
Expand All @@ -65,22 +67,32 @@ const App = observer(() => {
options={{
title: chatTitle,
headerRight: () => <HeaderRight />,
headerStyle: chatSessionStore.shouldShowHeaderDivider
? styles.headerWithDivider
: styles.headerWithoutDivider,
}}
/>
<Drawer.Screen
name="Models"
component={gestureHandlerRootHOC(ModelsScreen)}
options={({}) => ({
options={{
headerRight: () => <ModelsHeaderRight />,
})}
headerStyle: styles.headerWithoutDivider,
}}
/>
<Drawer.Screen
name="Settings"
component={gestureHandlerRootHOC(SettingsScreen)}
options={{
headerStyle: styles.headerWithoutDivider,
}}
/>
<Drawer.Screen
name="Benchmark"
component={gestureHandlerRootHOC(BenchmarkScreen)}
options={{
headerStyle: styles.headerWithoutDivider,
}}
/>
</Drawer.Navigator>
</NavigationContainer>
Expand All @@ -91,10 +103,20 @@ const App = observer(() => {
);
});

const styles = StyleSheet.create({
root: {
flex: 1,
},
});
const createStyles = (theme: Theme) =>
StyleSheet.create({
root: {
flex: 1,
},
headerWithoutDivider: {
elevation: 0,
shadowOpacity: 0,
borderBottomWidth: 0,
backgroundColor: theme.colors.background,
},
headerWithDivider: {
backgroundColor: theme.colors.background,
},
});

export default App;
1 change: 1 addition & 0 deletions __mocks__/stores/chatSessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const mockChatSessionStore = {
exitEditMode: jest.fn(),
enterEditMode: jest.fn(),
removeMessagesFromId: jest.fn(),
setIsGenerating: jest.fn(),
};

Object.defineProperty(mockChatSessionStore, 'currentSessionMessages', {
Expand Down
191 changes: 124 additions & 67 deletions src/components/ChatView/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import {
StatusBar,
StatusBarProps,
View,
TouchableOpacity,
} from 'react-native';

import dayjs from 'dayjs';
import {observer} from 'mobx-react';
import calendar from 'dayjs/plugin/calendar';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';

import {useComponentSize} from '../KeyboardAccessoryView/hooks';

Expand Down Expand Up @@ -167,15 +169,48 @@ export const ChatView = observer(
user,
}: ChatProps) => {
const theme = useTheme();
const styles = createStyles({theme});

const [inputText, setInputText] = React.useState('');

const {onLayout, size} = useComponentSize();
const animationRef = React.useRef(false);
const list = React.useRef<FlatList<MessageType.DerivedAny>>(null);
const insets = useSafeAreaInsets();
const [isImageViewVisible, setIsImageViewVisible] = React.useState(false);
const [isNextPageLoading, setNextPageLoading] = React.useState(false);
const [imageViewIndex, setImageViewIndex] = React.useState(0);
const [stackEntry, setStackEntry] = React.useState<StatusBarProps>({});

const [showScrollButton, setShowScrollButton] = React.useState(false);
const [isUserScrolling, setIsUserScrolling] = React.useState(false);

const handleScroll = React.useCallback(event => {
const {contentOffset} = event.nativeEvent;
const isAtTop = contentOffset.y <= 0;
setShowScrollButton(!isAtTop);
}, []);

const scrollToBottom = React.useCallback(() => {
list.current?.scrollToOffset({
animated: true,
offset: 0,
});
setIsUserScrolling(false);
}, []);

const handleScrollBeginDrag = React.useCallback(() => {
setIsUserScrolling(true);
}, []);

const wrappedOnSendPress = React.useCallback(
async (message: MessageType.PartialText) => {
if (chatSessionStore.isEditMode) {
chatSessionStore.commitEdit();
}
onSendPress(message);
setInputText('');
setIsUserScrolling(false);
},
[onSendPress],
);
Expand All @@ -193,17 +228,6 @@ export const ChatView = observer(
setInputText,
});

const styles = createStyles({theme});

const {onLayout, size} = useComponentSize();
const animationRef = React.useRef(false);
const list = React.useRef<FlatList<MessageType.DerivedAny>>(null);
const insets = useSafeAreaInsets();
const [isImageViewVisible, setIsImageViewVisible] = React.useState(false);
const [isNextPageLoading, setNextPageLoading] = React.useState(false);
const [imageViewIndex, setImageViewIndex] = React.useState(0);
const [stackEntry, setStackEntry] = React.useState<StatusBarProps>({});

const l10nValue = React.useMemo(
() => ({...l10n[locale], ...unwrap(l10nOverride)}),
[l10nOverride, locale],
Expand Down Expand Up @@ -464,7 +488,7 @@ export const ChatView = observer(
const messageWidth =
showUserAvatars &&
message.type !== 'dateHeader' &&
message.author.id !== user.id
message.author?.id !== user.id
? Math.floor(Math.min(size.width * 0.9, 440))
: Math.floor(Math.min(size.width * 0.92, 440));

Expand All @@ -476,27 +500,29 @@ export const ChatView = observer(
const showStatus = message.type !== 'dateHeader' && message.showStatus;

return (
<Message
{...{
enableAnimation,
message,
messageWidth,
onMessageLongPress: handleMessageLongPress,
onMessagePress: handleMessagePress,
onPreviewDataFetched,
renderBubble,
renderCustomMessage,
renderFileMessage,
renderImageMessage,
renderTextMessage,
roundBorder,
showAvatar,
showName,
showStatus,
showUserAvatars,
usePreviewData,
}}
/>
<View>
<Message
{...{
enableAnimation,
message,
messageWidth,
onMessageLongPress: handleMessageLongPress,
onMessagePress: handleMessagePress,
onPreviewDataFetched,
renderBubble,
renderCustomMessage,
renderFileMessage,
renderImageMessage,
renderTextMessage,
roundBorder,
showAvatar,
showName,
showStatus,
showUserAvatars,
usePreviewData,
}}
/>
</View>
);
},
[
Expand Down Expand Up @@ -547,47 +573,78 @@ export const ChatView = observer(

const renderScrollable = React.useCallback(
(panHandlers: GestureResponderHandlers) => (
<FlatList
automaticallyAdjustContentInsets={false}
contentContainerStyle={[
styles.flatListContentContainer,
// eslint-disable-next-line react-native/no-inline-styles
{
justifyContent: chatMessages.length !== 0 ? undefined : 'center',
paddingTop: insets.bottom,
},
]}
initialNumToRender={10}
ListEmptyComponent={renderListEmptyComponent}
ListFooterComponent={renderListFooterComponent}
ListHeaderComponent={renderListHeaderComponent}
maxToRenderPerBatch={6}
onEndReachedThreshold={0.75}
style={styles.flatList}
showsVerticalScrollIndicator={false}
{...unwrap(flatListProps)}
data={chatMessages}
inverted
keyboardDismissMode="interactive"
keyExtractor={keyExtractor}
onEndReached={handleEndReached}
ref={list}
renderItem={renderMessage}
{...panHandlers}
/>
<View style={styles.container}>
<FlatList
automaticallyAdjustContentInsets={false}
contentContainerStyle={[
styles.flatListContentContainer,
// eslint-disable-next-line react-native/no-inline-styles
{
justifyContent:
chatMessages.length !== 0 ? undefined : 'center',
paddingTop: insets.bottom,
},
]}
initialNumToRender={10}
ListEmptyComponent={renderListEmptyComponent}
ListFooterComponent={renderListFooterComponent}
ListHeaderComponent={renderListHeaderComponent}
maxToRenderPerBatch={6}
onEndReachedThreshold={0.75}
style={styles.flatList}
showsVerticalScrollIndicator={false}
onScroll={handleScroll}
{...unwrap(flatListProps)}
data={chatMessages}
inverted
keyboardDismissMode="interactive"
keyExtractor={keyExtractor}
onEndReached={handleEndReached}
ref={list}
renderItem={renderMessage}
onScrollBeginDrag={handleScrollBeginDrag}
maintainVisibleContentPosition={
!isUserScrolling
? undefined
: {
minIndexForVisible: 1,
}
}
{...panHandlers}
/>
{showScrollButton && (
<TouchableOpacity
style={styles.scrollToBottomButton}
onPress={scrollToBottom}>
<Icon
name="chevron-down"
size={24}
color={theme.colors.onPrimary}
/>
</TouchableOpacity>
)}
</View>
),
[
chatMessages,
styles.flatList,
styles.container,
styles.flatListContentContainer,
flatListProps,
handleEndReached,
styles.flatList,
styles.scrollToBottomButton,
chatMessages,
insets.bottom,
keyExtractor,
renderMessage,
renderListEmptyComponent,
renderListFooterComponent,
renderListHeaderComponent,
handleScroll,
flatListProps,
keyExtractor,
handleEndReached,
renderMessage,
handleScrollBeginDrag,
isUserScrolling,
showScrollButton,
scrollToBottom,
theme.colors.onPrimary,
],
);

Expand Down
19 changes: 19 additions & 0 deletions src/components/ChatView/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,23 @@ export const createStyles = ({theme}: {theme: Theme}) =>
menu: {
width: 170,
},
scrollToBottomButton: {
position: 'absolute',
right: 16,
bottom: 40, // Above the input area
backgroundColor: theme.colors.primary,
width: 40,
height: 40,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
},
});
8 changes: 5 additions & 3 deletions src/components/Dialog/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@ export const createStyles = (theme: Theme, scrollableBorderShown?: boolean) =>
StyleSheet.create({
dialog: {
//maxHeight: '90%',
backgroundColor: theme.colors.surface,
backgroundColor: theme.colors.background,
borderRadius: 15,
margin: 0,
padding: 0,
width: '92%',
alignSelf: 'center',
},
dialogTitle: {
fontSize: 16,
fontWeight: 'bold',
},
dialogContent: {
maxHeight: dialogHeight,
paddingHorizontal: 16,
paddingHorizontal: 24,
borderTopWidth: scrollableBorderShown ? 1 : 0,
borderBottomWidth: scrollableBorderShown ? 1 : 0,
backgroundColor: theme.colors.surface,
backgroundColor: theme.colors.background,
},
dialogScrollArea: {},
dialogActionButton: {
Expand Down
Loading

0 comments on commit b884b23

Please sign in to comment.