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

feat: add session renaming functionality and context menu in sidebar #185

Merged
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
129 changes: 114 additions & 15 deletions src/components/SidebarContent/SidebarContent.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, {useEffect, useState} from 'react';
import {TouchableOpacity, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {TouchableOpacity, View, TextInput, Modal} from 'react-native';

import {observer} from 'mobx-react';
import {Drawer, Text} from 'react-native-paper';
import DeviceInfo from 'react-native-device-info';
import Clipboard from '@react-native-clipboard/clipboard';
import {SafeAreaView} from 'react-native-safe-area-context';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import {
DrawerContentScrollView,
Expand All @@ -18,7 +18,7 @@ import {createStyles} from './styles';

import {chatSessionStore} from '../../store';

import {AppleStyleSwipeableRow} from '..';
import {Menu} from '..';

export const SidebarContent: React.FC<DrawerContentComponentProps> = observer(
props => {
Expand All @@ -27,6 +27,12 @@ export const SidebarContent: React.FC<DrawerContentComponentProps> = observer(
build: '',
});

const [menuVisible, setMenuVisible] = useState<string | null>(null); // Track which menu is visible
const [menuPosition, setMenuPosition] = useState({x: 0, y: 0}); // Track menu position
const [renameModalVisible, setRenameModalVisible] = useState(false);
const [newTitle, setNewTitle] = useState('');
const [sessionToRename, setSessionToRename] = useState<string | null>(null);

useEffect(() => {
chatSessionStore.loadSessionList();

Expand All @@ -47,6 +53,26 @@ export const SidebarContent: React.FC<DrawerContentComponentProps> = observer(
Clipboard.setString(versionString);
};

const openMenu = (sessionId: string, event: any) => {
const {nativeEvent} = event;
setMenuPosition({x: nativeEvent.pageX, y: nativeEvent.pageY});
setMenuVisible(sessionId);
};

const closeMenu = () => setMenuVisible(null);

const handleRename = () => {
if (sessionToRename && newTitle.trim()) {
chatSessionStore.updateSessionTitleBySessionId(
sessionToRename,
newTitle,
);
setRenameModalVisible(false);
setNewTitle('');
setSessionToRename(null);
}
};

return (
<GestureHandlerRootView style={styles.sidebarContainer}>
<View style={styles.contentWrapper}>
Expand Down Expand Up @@ -85,22 +111,47 @@ export const SidebarContent: React.FC<DrawerContentComponentProps> = observer(
const isActive =
chatSessionStore.activeSessionId === session.id;
return (
<AppleStyleSwipeableRow
key={session.id} // Ensure each swipeable row has a unique key
onDelete={() =>
chatSessionStore.deleteSession(session.id)
}>
<Drawer.Item
active={isActive}
key={session.id}
label={session.title}
<View key={session.id} style={styles.sessionItem}>
<TouchableOpacity
onPress={() => {
chatSessionStore.setActiveSession(session.id);
props.navigation.navigate('Chat');
console.log(`Navigating to session: ${session.id}`);
}}
/>
</AppleStyleSwipeableRow>
onLongPress={event => openMenu(session.id, event)} // Open menu on long press
style={styles.sessionTouchable}>
<Drawer.Item
active={isActive}
label={session.title}
/>
</TouchableOpacity>

{/* Menu for the session item */}
<Menu
visible={menuVisible === session.id}
onDismiss={closeMenu}
anchor={menuPosition}>
<Menu.Item
style={styles.menu}
onPress={() => {
setSessionToRename(session.id);
setNewTitle(session.title);
setRenameModalVisible(true);
closeMenu();
}}
leadingIcon="pencil"
label="Rename"
/>
<Menu.Item
style={styles.menu}
onPress={() => {
chatSessionStore.deleteSession(session.id);
closeMenu();
}}
leadingIcon="trash-can-outline"
label="Delete"
/>
</Menu>
</View>
);
})}
</Drawer.Section>
Expand All @@ -121,6 +172,54 @@ export const SidebarContent: React.FC<DrawerContentComponentProps> = observer(
</TouchableOpacity>
</SafeAreaView>
</View>

{/* Rename Modal */}
<Modal
transparent={true}
visible={renameModalVisible}
onRequestClose={() => {
setRenameModalVisible(false);
setNewTitle('');
setSessionToRename(null);
}}
animationType="fade">
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}>Rename Chat</Text>
<TextInput
style={styles.textInput}
placeholder="New Title"
placeholderTextColor={theme.colors.onSurfaceVariant}
value={newTitle}
maxLength={40}
onChangeText={setNewTitle}
autoFocus={true}
onSubmitEditing={handleRename}
returnKeyType="done"
/>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => {
setRenameModalVisible(false);
setNewTitle('');
setSessionToRename(null);
}}>
<Text style={styles.cancelText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.confirmButton,
!newTitle.trim() && styles.disabledButton,
]}
onPress={handleRename}
disabled={!newTitle.trim()}>
<Text style={styles.confirmText}>Done</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
</GestureHandlerRootView>
);
},
Expand Down
86 changes: 86 additions & 0 deletions src/components/SidebarContent/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,90 @@ export const createStyles = (theme: MD3Theme) =>
versionSafeArea: {
backgroundColor: theme.colors.surface,
},
menu: {
width: 170,
},
sessionItem: {
position: 'relative',
},
sessionTouchable: {
flex: 1,
},
modalOverlay: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
modalContent: {
width: '80%',
backgroundColor: theme.colors.surface,
borderRadius: 14,
overflow: 'hidden',
borderWidth: StyleSheet.hairlineWidth,
borderColor: theme.dark
? theme.colors.outline + '50'
: theme.colors.outline + '30',
},
modalTitle: {
fontSize: 17,
fontWeight: '600',
color: theme.colors.onSurface,
textAlign: 'center',
paddingVertical: 16,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: theme.dark
? theme.colors.outline + '50'
: theme.colors.outline + '30',
},
textInput: {
margin: 16,
padding: 12,
backgroundColor: theme.dark
? theme.colors.surfaceVariant + '80'
: theme.colors.surface + '90',
color: theme.colors.onSurface,
fontSize: 17,
borderWidth: StyleSheet.hairlineWidth * 2,
borderColor: theme.dark
? theme.colors.outline + '50'
: theme.colors.outline + '30',
borderRadius: 10,
},
buttonContainer: {
flexDirection: 'row',
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: theme.dark
? theme.colors.outline + '50'
: theme.colors.outline + '30',
},
cancelButton: {
flex: 1,
paddingVertical: 12,
borderRightWidth: StyleSheet.hairlineWidth,
borderRightColor: theme.dark
? theme.colors.outline + '50'
: theme.colors.outline + '30',
alignItems: 'center',
justifyContent: 'center',
},
cancelText: {
color: theme.colors.onSurfaceVariant,
fontSize: 17,
fontWeight: '400',
},
confirmButton: {
flex: 1,
paddingVertical: 12,
alignItems: 'center',
justifyContent: 'center',
},
confirmText: {
color: theme.colors.primary,
fontSize: 17,
fontWeight: '600',
},
disabledButton: {
opacity: 0.4,
},
});
11 changes: 11 additions & 0 deletions src/store/ChatSessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ class ChatSessionStore {
});
}

// Update session title by session ID
updateSessionTitleBySessionId(sessionId: string, newTitle: string): void {
const session = this.sessions.find(s => s.id === sessionId);
if (session) {
runInAction(() => {
session.title = newTitle;
});
this.saveSessionsMetadata();
}
}

updateSessionTitle(session: SessionMetaData) {
if (session.messages.length > 0) {
const message = session.messages[session.messages.length - 1];
Expand Down
Loading