Skip to content

Commit 50b8f85

Browse files
authored
Merge pull request #66 from chrisglein/saveSettings
Save settings to app storage
2 parents 7313131 + 72eb352 commit 50b8f85

File tree

10 files changed

+181
-35
lines changed

10 files changed

+181
-35
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,14 @@ The app's settings are handled by a `SettingsContext` object, which has dialog U
6161
## App Fundamentals
6262
| File | Type | Information |
6363
| --- | --- | --- |
64-
| App.tsx | `App` | Root of the app, publishes the `StylesContext` and `SettingsContext` |
64+
| About.tsx | `AboutPopup` | Popup for basic app information |
65+
| App.tsx | `App` | Root of the app, publishes the `StylesContext` and `SettingsContext`. Hosts the `SettingsPopup` |
6566
| ChatSession.tsx | `ChatSession` | Owns the `ChatElement` list, publishes the `ChatHistoryContext`, and handles any writes to that list |
6667
| ChatSession.tsx | `AutomatedChatSession` | Populates the `ChatSession` with either scripted responses or by creating components that query OpenAi |
67-
| Chat.tsx | `Chat` | The scrolling list of chat entries. Publishes the `FeedbackContext`, and `ChatScrollContext` services. Hosts a `ChatEntry` for the user input. Hosts the dialogs of the app (`FeedbackPopup` and `SettingsPopups`).
68+
| Chat.tsx | `Chat` | The scrolling list of chat entries. Publishes the `FeedbackContext`, and `ChatScrollContext` services. Hosts a `ChatEntry` for the user input. Hosts the dialogs of the app (`FeedbackPopup` and `AboutPopup`).
6869
| Chat.tsx | `ChatEntry` | Takes in the user's text input |
6970
| Feedback.tsx | `FeedbackPopup` | Popup for giving feedback on AI responses |
70-
| Settings.tsx | `SettingsPopup` | Popup that shows controls for modifying the `SettingsContext` |
71+
| Settings.tsx | `SettingsPopup` | Popup that shows controls for modifying the `SettingsContext` and loads persistant values from app storage |
7172
| Styles.tsx | `StylesContext` | StyleSheet that is light/dark mode aware |
7273

7374
## AI Query & Response
@@ -98,3 +99,4 @@ The app's settings are handled by a `SettingsContext` object, which has dialog U
9899
- Clipboard via [@react-native-picker/picker](https://github.com/react-native-picker/picker)
99100
- Syntax Highlighting via [react-native-syntax-highlighter](https://github.com/conorhastings/react-native-syntax-highlighter)
100101
- Dependency patching via [patch-package](https://github.com/ds300/patch-package)
102+
- Storage via [react-native-async-storage](https://github.com/react-native-async-storage/async-storage)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"postinstall": "patch-package"
1313
},
1414
"dependencies": {
15+
"@react-native-async-storage/async-storage": "^1.17.11",
1516
"@react-native-clipboard/clipboard": "^1.11.2",
1617
"@react-native-picker/picker": "^2.2.1",
1718
"patch-package": "^6.5.1",

src/App.tsx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
11
import React from 'react';
2-
import { Appearance } from 'react-native';
2+
import {
3+
Appearance,
4+
View
5+
} from 'react-native';
36
import { ChatSession } from './ChatSession';
47
import {
58
StylesContext,
69
CreateStyles,
710
} from './Styles';
8-
import { SettingsContext } from './Settings';
11+
import {
12+
LoadSettingsData,
13+
SettingsContext,
14+
SettingsPopup,
15+
} from './Settings';
916

1017
function App(): JSX.Element {
1118
const [currentTheme, setCurrentTheme] = React.useState(Appearance.getColorScheme());
1219
const [apiKey, setApiKey] = React.useState<string | undefined>(undefined);
1320
const [scriptName, setScriptName] = React.useState<string | undefined>("");
1421
const [delayForArtificialResponse, setDelayForArtificialResponse] = React.useState<number>(1500);
15-
22+
const [showSettingsPopup, setShowSettingsPopup] = React.useState(false);
23+
1624
const isDarkMode = currentTheme === 'dark';
25+
const styles = CreateStyles(isDarkMode);
26+
27+
const settings = {
28+
scriptName: scriptName,
29+
setScriptName: setScriptName,
30+
apiKey: apiKey,
31+
setApiKey: setApiKey,
32+
delayForArtificialResponse: delayForArtificialResponse,
33+
setDelayForArtificialResponse: setDelayForArtificialResponse,
34+
showPopup: showSettingsPopup,
35+
setShowPopup: setShowSettingsPopup,
36+
};
1737

1838
const onAppThemeChanged = () => {
1939
setCurrentTheme(Appearance.getColorScheme());
@@ -24,16 +44,14 @@ function App(): JSX.Element {
2444
});
2545

2646
return (
27-
<StylesContext.Provider value={CreateStyles(isDarkMode)}>
28-
<SettingsContext.Provider value={{
29-
scriptName: scriptName,
30-
setScriptName: setScriptName,
31-
apiKey: apiKey,
32-
setApiKey: setApiKey,
33-
delayForArtificialResponse: delayForArtificialResponse,
34-
setDelayForArtificialResponse: setDelayForArtificialResponse,
35-
}}>
36-
<ChatSession/>
47+
<StylesContext.Provider value={styles}>
48+
<SettingsContext.Provider value={settings}>
49+
<View>
50+
<ChatSession/>
51+
<SettingsPopup
52+
show={showSettingsPopup}
53+
close={() => settings.setShowPopup(false)}/>
54+
</View>
3755
</SettingsContext.Provider>
3856
</StylesContext.Provider>
3957
);

src/Chat.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import {
1313
FeedbackContext,
1414
FeedbackPopup,
1515
} from './Feedback';
16-
import { SettingsPopup } from './Settings';
16+
import {
17+
SettingsContext,
18+
SettingsPopup
19+
} from './Settings';
1720
import { AboutPopup } from './About';
1821
import { HoverButton } from './Controls';
1922

@@ -101,14 +104,14 @@ type ChatProps = {
101104
function Chat({entries, humanText, onPrompt, clearConversation}: ChatProps): JSX.Element {
102105
const styles = React.useContext(StylesContext);
103106
const chatHistory = React.useContext(ChatHistoryContext);
107+
const settings = React.useContext(SettingsContext);
104108
const [showFeedbackPopup, setShowFeedbackPopup] = React.useState(false);
105109
const [feedbackTargetResponse, setFeedbackTargetResponse] = React.useState<string | undefined>(undefined);
106-
const [showSettingsPopup, setShowSettingsPopup] = React.useState(false);
107110
const [showAboutPopup, setShowAboutPopup] = React.useState(false);
108111
const [feedbackIsPositive, setFeedbackIsPositive] = React.useState(false);
109112
const scrollViewRef : React.RefObject<ScrollView> = React.useRef(null);
110113

111-
let showingAnyPopups = (showFeedbackPopup || showSettingsPopup || showAboutPopup);
114+
let showingAnyPopups = (showFeedbackPopup || settings.showPopup || showAboutPopup);
112115

113116
const feedbackContext = {
114117
showFeedback: (positive: boolean, response?: string) => {
@@ -179,7 +182,7 @@ function Chat({entries, humanText, onPrompt, clearConversation}: ChatProps): JSX
179182
contentShownOnHover={
180183
<>
181184
<HoverButton content="❔" tooltip="About" onPress={() => setShowAboutPopup(true)}/>
182-
<HoverButton content="⚙️" tooltip="Settings" onPress={() => setShowSettingsPopup(true)}/>
185+
<HoverButton content="⚙️" tooltip="Settings" onPress={() => settings.setShowPopup(true)}/>
183186
</>
184187
}>
185188
<ChatEntry
@@ -197,9 +200,6 @@ function Chat({entries, humanText, onPrompt, clearConversation}: ChatProps): JSX
197200
isPositive={feedbackIsPositive}
198201
response={feedbackTargetResponse}
199202
close={() => setShowFeedbackPopup(false)}/>
200-
<SettingsPopup
201-
show={showSettingsPopup}
202-
close={() => setShowSettingsPopup(false)}/>
203203
<AboutPopup
204204
show={showAboutPopup}
205205
close={() => setShowAboutPopup(false)}/>

src/Controls.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,10 @@ function ImageSelection({image}: ImageSelectionProps): JSX.Element {
9191
}
9292

9393
type HyperlinkProps = {
94-
url: string
94+
url: string,
95+
text?: string,
9596
};
96-
function Hyperlink({url}: HyperlinkProps): JSX.Element {
97+
function Hyperlink({url, text}: HyperlinkProps): JSX.Element {
9798
const styles = React.useContext(StylesContext);
9899
const [hovering, setHovering] = React.useState(false);
99100
const [pressing, setPressing] = React.useState(false);
@@ -109,7 +110,7 @@ function Hyperlink({url}: HyperlinkProps): JSX.Element {
109110
pressing ? styles.hyperlinkPressing :
110111
hovering ? styles.hyperlinkHovering :
111112
styles.hyperlinkIdle}>
112-
{url}
113+
{text ?? url}
113114
</Text>
114115
</Pressable>
115116
);

src/Settings.tsx

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import {
33
Button,
4+
Switch,
45
Text,
56
TextInput,
67
View,
@@ -10,15 +11,63 @@ import {Hyperlink} from './Controls';
1011
import {StylesContext} from './Styles';
1112
import {Picker} from '@react-native-picker/picker';
1213
import {ChatScriptNames} from './ChatScript';
14+
import AsyncStorage from '@react-native-async-storage/async-storage';
1315

14-
const SettingsContext = React.createContext<{
16+
const settingsKey = 'settings';
17+
18+
// App-wide settings that can be modified from a menu, some of which are saved between app sessions
19+
type SettingsContextType = {
1520
apiKey?: string,
16-
setApiKey: (value: string) => void,
21+
setApiKey: (value?: string) => void,
1722
scriptName?: string,
1823
setScriptName: (value: string) => void,
1924
delayForArtificialResponse?: number,
2025
setDelayForArtificialResponse: (value: number) => void,
21-
}>({});
26+
showPopup: boolean,
27+
setShowPopup: (value: boolean) => void,
28+
}
29+
const SettingsContext = React.createContext<SettingsContextType>({
30+
setApiKey: () => {},
31+
setScriptName: () => {},
32+
setDelayForArtificialResponse: () => {},
33+
showPopup: false,
34+
setShowPopup: () => {},
35+
});
36+
37+
// Settings that are saved between app sessions
38+
type SettingsData = {
39+
apiKey?: string,
40+
}
41+
42+
// Read settings from app storage
43+
const SaveSettingsData = async (value: SettingsData) => {
44+
console.debug('Saving settings data...');
45+
try {
46+
const jsonValue = JSON.stringify(value);
47+
await AsyncStorage.setItem(settingsKey, jsonValue)
48+
console.debug('Done saving settings data');
49+
} catch (e) {
50+
console.error(e);
51+
}
52+
}
53+
54+
// Write settings to ap storage
55+
const LoadSettingsData = async () => {
56+
console.debug('Loading settings data...');
57+
let value : SettingsData = {};
58+
try {
59+
const jsonValue = await AsyncStorage.getItem(settingsKey);
60+
if (jsonValue != null) {
61+
console.debug(jsonValue);
62+
const data = JSON.parse(jsonValue);
63+
64+
if (data.hasOwnProperty('apiKey')) { value.apiKey = data.apiKey; }
65+
}
66+
} catch(e) {
67+
console.error(e);
68+
}
69+
return value;
70+
}
2271

2372
type SettingsPopupProps = {
2473
show: boolean;
@@ -27,10 +76,39 @@ type SettingsPopupProps = {
2776
function SettingsPopup({show, close}: SettingsPopupProps): JSX.Element {
2877
const styles = React.useContext(StylesContext);
2978
const settings = React.useContext(SettingsContext);
30-
const [apiKey, setApiKey] = React.useState<string>(settings.apiKey ?? "");
79+
const [apiKey, setApiKey] = React.useState<string | undefined>(settings.apiKey);
80+
const [saveApiKey, setSaveApiKey] = React.useState<boolean>(false);
3181
const [scriptName, setScriptName] = React.useState<string>(settings.scriptName ?? "");
3282
const [delayForArtificialResponse, setDelayForArtificialResponse] = React.useState<number>(settings.delayForArtificialResponse ?? 0);
3383

84+
// It may seem weird to do this when the UI loads, not the app, but it's okay
85+
// because this component is loaded when the app starts but isn't shown. And
86+
// this popup needs to directly know when the settings change (which won't
87+
// happen directly if you just consume settings.apiKey inside the component.
88+
React.useEffect(() => {
89+
const load = async () => {
90+
let value = await LoadSettingsData();
91+
setApiKey(value.apiKey);
92+
settings.setApiKey(value.apiKey);
93+
94+
// If an API key was set, continue to remember it
95+
setSaveApiKey(value.apiKey !== undefined);
96+
}
97+
load();
98+
}, []);
99+
100+
const save = () => {
101+
settings.setApiKey(apiKey);
102+
settings.setScriptName(scriptName);
103+
settings.setDelayForArtificialResponse(delayForArtificialResponse);
104+
105+
close();
106+
107+
SaveSettingsData({
108+
apiKey: saveApiKey ? apiKey : undefined
109+
});
110+
}
111+
34112
return (
35113
<Popup
36114
isOpen={show}
@@ -50,7 +128,14 @@ function SettingsPopup({show, close}: SettingsPopupProps): JSX.Element {
50128
style={{flexGrow: 1, minHeight: 32}}
51129
onChangeText={value => setApiKey(value)}
52130
value={apiKey}/>
53-
<Hyperlink url="https://platform.openai.com/account/api-keys"/>
131+
<View style={styles.horizontalContainer}>
132+
<Switch
133+
value={saveApiKey}
134+
onValueChange={(value) => setSaveApiKey(value)}/>
135+
<Text>Remember this </Text>
136+
</View>
137+
<Hyperlink
138+
url="https://platform.openai.com/account/api-keys"/>
54139
</View>
55140
<View>
56141
<Text>Script</Text>
@@ -74,10 +159,7 @@ function SettingsPopup({show, close}: SettingsPopupProps): JSX.Element {
74159
<Button
75160
title="OK"
76161
onPress={() => {
77-
settings.setApiKey(apiKey);
78-
settings.setScriptName(scriptName);
79-
settings.setDelayForArtificialResponse(delayForArtificialResponse);
80-
close();
162+
save();
81163
}}/>
82164
</View>
83165
</View>

windows/artificialChat.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ReactNativePicker", "..\nod
3737
EndProject
3838
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Clipboard", "..\node_modules\@react-native-clipboard\clipboard\windows\Clipboard\Clipboard.vcxproj", "{99A938FA-9FF3-47A0-9256-195A06E7FDD1}"
3939
EndProject
40+
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ReactNativeAsyncStorage", "..\node_modules\@react-native-async-storage\async-storage\windows\ReactNativeAsyncStorage\ReactNativeAsyncStorage.vcxproj", "{4855D892-E16C-404D-8286-0089E0F7F9C4}"
41+
EndProject
4042
Global
4143
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4244
Debug|ARM64 = Debug|ARM64
@@ -151,6 +153,18 @@ Global
151153
{99A938FA-9FF3-47A0-9256-195A06E7FDD1}.Release|x64.Build.0 = Release|x64
152154
{99A938FA-9FF3-47A0-9256-195A06E7FDD1}.Release|x86.ActiveCfg = Release|Win32
153155
{99A938FA-9FF3-47A0-9256-195A06E7FDD1}.Release|x86.Build.0 = Release|Win32
156+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Debug|ARM64.ActiveCfg = Debug|ARM64
157+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Debug|ARM64.Build.0 = Debug|ARM64
158+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Debug|x64.ActiveCfg = Debug|x64
159+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Debug|x64.Build.0 = Debug|x64
160+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Debug|x86.ActiveCfg = Debug|Win32
161+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Debug|x86.Build.0 = Debug|Win32
162+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Release|ARM64.ActiveCfg = Release|ARM64
163+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Release|ARM64.Build.0 = Release|ARM64
164+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Release|x64.ActiveCfg = Release|x64
165+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Release|x64.Build.0 = Release|x64
166+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Release|x86.ActiveCfg = Release|Win32
167+
{4855D892-E16C-404D-8286-0089E0F7F9C4}.Release|x86.Build.0 = Release|Win32
154168
EndGlobalSection
155169
GlobalSection(SolutionProperties) = preSolution
156170
HideSolutionNode = FALSE

windows/artificialChat/AutolinkedNativeModules.g.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
#include "pch.h"
44
#include "AutolinkedNativeModules.g.h"
55

6+
// Includes from @react-native-async-storage/async-storage
7+
#include <winrt/ReactNativeAsyncStorage.h>
8+
69
// Includes from @react-native-clipboard/clipboard
710
#include <winrt/NativeClipboard.h>
811

@@ -14,6 +17,8 @@ namespace winrt::Microsoft::ReactNative
1417

1518
void RegisterAutolinkedNativeModulePackages(winrt::Windows::Foundation::Collections::IVector<winrt::Microsoft::ReactNative::IReactPackageProvider> const& packageProviders)
1619
{
20+
// IReactPackageProviders from @react-native-async-storage/async-storage
21+
packageProviders.Append(winrt::ReactNativeAsyncStorage::ReactPackageProvider());
1722
// IReactPackageProviders from @react-native-clipboard/clipboard
1823
packageProviders.Append(winrt::NativeClipboard::ReactPackageProvider());
1924
// IReactPackageProviders from @react-native-picker/picker

windows/artificialChat/AutolinkedNativeModules.g.targets

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
33
<!-- AutolinkedNativeModules.g.targets contents generated by "react-native autolink-windows" -->
44
<ItemGroup>
5+
<!-- Projects from @react-native-async-storage/async-storage -->
6+
<ProjectReference Include="$(ProjectDir)..\..\node_modules\@react-native-async-storage\async-storage\windows\ReactNativeAsyncStorage\ReactNativeAsyncStorage.vcxproj">
7+
<Project>{4855D892-E16C-404D-8286-0089E0F7F9C4}</Project>
8+
</ProjectReference>
59
<!-- Projects from @react-native-clipboard/clipboard -->
610
<ProjectReference Include="$(ProjectDir)..\..\node_modules\@react-native-clipboard\clipboard\windows\Clipboard\Clipboard.vcxproj">
711
<Project>{99a938fa-9ff3-47a0-9256-195a06e7fdd1}</Project>

0 commit comments

Comments
 (0)