Skip to content

Commit

Permalink
feat: message sink haptic feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
zhuanghongji committed Jun 3, 2023
1 parent 548d8c1 commit 87eed58
Show file tree
Hide file tree
Showing 17 changed files with 107 additions and 48 deletions.
15 changes: 8 additions & 7 deletions src/components/chat/InputBar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { hapticSoft } from '../../haptic'
import { colors } from '../../res/colors'
import { dimensions } from '../../res/dimensions'
import { useThemeScheme, useThemeSelector } from '../../themes/hooks'
Expand All @@ -16,6 +17,8 @@ import Animated, {
} from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'

const H_EDGE = 8

export interface InputBarProps {
value: string
sendDisabled: boolean
Expand All @@ -24,16 +27,14 @@ export interface InputBarProps {
onSendPress: () => void
}

const H_EDGE = 8

export function InputBar(props: InputBarProps): JSX.Element {
const { value, sendDisabled, renderToolsBar, onChangeText, onSendPress } = props

const { t } = useTranslation()
const tintColor = useThemeSelector(colors.white, colors.black)
const textColor = useThemeSelector(colors.white, colors.black)
const backgroundColor = useThemeSelector(colors.c1C, colors.cF7)
const backgroundColor2 = useThemeSelector(colors.c28, colors.white)

const { placeholder: placeholderColor } = useThemeScheme()

const { bottom } = useSafeAreaInsets()
Expand All @@ -45,9 +46,6 @@ export function InputBar(props: InputBarProps): JSX.Element {
}),
[]
)

const { t } = useTranslation()

const sendAnim = useSharedValue(0)
const sendAnimStyle = useAnimatedStyle(() => {
return {
Expand Down Expand Up @@ -91,7 +89,10 @@ export function InputBar(props: InputBarProps): JSX.Element {
right: dimensions.edge,
bottom: H_EDGE,
}}
onPress={onSendPress}>
onPress={() => {
hapticSoft()
onSendPress()
}}>
<SvgIcon size={dimensions.iconLarge} color={tintColor} name="send" />
</Pressable>
</Animated.View>
Expand Down
6 changes: 5 additions & 1 deletion src/components/chat/InputToolsBar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { hapticSoft } from '../../haptic'
import { dimensions } from '../../res/dimensions'
import { useThemeScheme } from '../../themes/hooks'
import { SvgIcon } from '../SvgIcon'
Expand Down Expand Up @@ -30,7 +31,10 @@ export function InputToolsBar(props: InputToolsBarProps): JSX.Element {
containerSize={36}
name="chat-new"
disabled={newDialogueDisabled}
onPress={onNewDialoguePress}
onPress={() => {
hapticSoft()
onNewDialoguePress()
}}
/>
<SvgIcon
style={{ marginLeft: 4, marginBottom: 1 }}
Expand Down
3 changes: 2 additions & 1 deletion src/components/chat/SSEMessageView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { dimensions } from '../../res/dimensions'
import { images } from '../../res/images'
import { stylez } from '../../res/stylez'
import { texts } from '../../res/texts'
import { TText } from '../../themes/TText'
import { useThemeScheme } from '../../themes/hooks'
import { trimContent } from '../../utils'
Expand Down Expand Up @@ -39,7 +40,7 @@ export function SSEMessageView(props: SSEMessageProps) {
<TText
style={[styles.text, stylez.contentText, { fontSize, lineHeight: fontSize * 1.2 }]}
typo="text">
{content ? trimContent(content) : '...'}
{`${trimContent(content)}${texts.assistantCursor}`}
</TText>
</View>
</View>
Expand Down
14 changes: 14 additions & 0 deletions src/haptic/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { hapticSoft } from '.'
import { useMessageSinkHapticFeedbackPref } from '../preferences/storages'
import { useCallback } from 'react'

export function useHapticFeedbackMessaging() {
const [isHaptic] = useMessageSinkHapticFeedbackPref()
const onNextHaptic = useCallback(() => {
if (!isHaptic) {
return
}
hapticSoft()
}, [isHaptic])
return { isHaptic, onNextHaptic }
}
3 changes: 2 additions & 1 deletion src/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,6 @@
"Copy to Clipboard": "Copy to Clipboard",
"No valid messages": "No valid messages",
"CreateFirstChatTip": "There is no chat yet,\nclick to create one.",
"Chat Configs": "Chat Configs"
"Chat Configs": "Chat Configs",
"Message Sink Haptic Feedback": "Message Sink Haptic Feedback"
}
3 changes: 2 additions & 1 deletion src/i18n/zh-Hans/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,6 @@
"Copy to Clipboard": "复制到粘贴板",
"No valid messages": "无有效消息",
"CreateFirstChatTip": "还没有对话,\n点击创建一个",
"Chat Configs": "对话配置"
"Chat Configs": "对话配置",
"Message Sink Haptic Feedback": "消息接收震动反馈"
}
1 change: 1 addition & 0 deletions src/mmkv/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const enum StorageKey {
enableClipboardDetect = 'enable_clipboard_detect',
showChatAvatar = 'show_chat_avatar',
colouredContextMessage = 'coloured_context_message',
messageSinkHapticFeedback = 'message_sink_haptic_feedback',

// others
lastDetectedText = 'last_detected_text',
Expand Down
2 changes: 2 additions & 0 deletions src/preferences/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const DEFAULTS: {
enableClipboardDetect: boolean
showChatAvatar: boolean
colouredContextMessage: boolean
messageSinkHapticFeedback: boolean
avatar: string
contextMessagesNum: number
fontSize: number
Expand All @@ -38,6 +39,7 @@ export const DEFAULTS: {
enableClipboardDetect: true,
showChatAvatar: true,
colouredContextMessage: true,
messageSinkHapticFeedback: true,
avatar: '😀',
contextMessagesNum: 4,
fontSize: 16,
Expand Down
4 changes: 4 additions & 0 deletions src/preferences/storages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,7 @@ export function useShowChatAvatarPref() {
export function useColouredContextMessagePref() {
return useStorageBoolean(StorageKey.colouredContextMessage, DEFAULTS.colouredContextMessage)
}

export function useMessageSinkHapticFeedbackPref() {
return useStorageBoolean(StorageKey.messageSinkHapticFeedback, DEFAULTS.messageSinkHapticFeedback)
}
2 changes: 2 additions & 0 deletions src/res/texts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export const texts = {
requestError: 'Request error',
requestTimeout: 'Request timeout',
checkNetworkOrSettings: 'Please check if your network or settings are valid',
userCursor: '○',
assistantCursor: '●',
}
30 changes: 16 additions & 14 deletions src/screens/custom-chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useInfiniteQueryCustomChatMessagePageable,
} from '../../db/table/t-custom-chat-message'
import { hapticError, hapticSuccess, hapticWarning } from '../../haptic'
import { useHapticFeedbackMessaging } from '../../haptic/hooks'
import { useOpenAIApiCustomizedOptions, useOpenAIApiUrlOptions } from '../../http/apis/hooks'
import {
ChatCompletionsCustomizedOptions,
Expand Down Expand Up @@ -53,13 +54,17 @@ export function CustomChatScreen({ navigation, route }: Props): JSX.Element {
const { chat } = route.params
const { id } = chat

const { t } = useTranslation()
const { backgroundChat: backgroundColor } = useThemeScheme()

const [showChatAvatar] = useShowChatAvatarPref()
const [colouredContextMessage] = useColouredContextMessagePref()

const settingsModalRef = useRef<SettingsSelectorModalHandle>(null)
const settings = fillTCustomChatWithDefaults(id, useCustomChatSettings(id))
const { chat_name, system_prompt, model, temperature, avatar, font_size, context_messages_num } =
settings

const { t } = useTranslation()

const [freshMessages, setFreshMessages] = useState<BaseMessage[]>([])
const legacyPageSize = context_messages_num > 20 ? 100 : 20
const legacyResult = useInfiniteQueryCustomChatMessagePageable(id, legacyPageSize)
Expand Down Expand Up @@ -110,18 +115,6 @@ export function CustomChatScreen({ navigation, route }: Props): JSX.Element {
}
return count
}, [finalMessages])

const { urlOptions, checkIsOptionsValid } = useOpenAIApiUrlOptions()
const customizedOptions: ChatCompletionsCustomizedOptions = {
...useOpenAIApiCustomizedOptions(),
model,
temperature,
}

const { backgroundChat: backgroundColor } = useThemeScheme()
const [showChatAvatar] = useShowChatAvatarPref()
const [colouredContextMessage] = useColouredContextMessagePref()

const { height: keyboardHeight } = useReanimatedKeyboardAnimation()
const enablekeyboardAvoid = useSharedValue(true)
const transformStyle = useAnimatedStyle(() => {
Expand All @@ -140,6 +133,14 @@ export function CustomChatScreen({ navigation, route }: Props): JSX.Element {

const [inputText, setInputText] = useState('')

const { onNextHaptic } = useHapticFeedbackMessaging()
const { urlOptions, checkIsOptionsValid } = useOpenAIApiUrlOptions()
const customizedOptions: ChatCompletionsCustomizedOptions = {
...useOpenAIApiCustomizedOptions(),
model,
temperature,
}

const status = useSSEMessageStore(state => state.status)
const sendDisabled = inputText.trim() && status !== 'sending' ? false : true
const setStatus = useSSEMessageStore(state => state.setStatus)
Expand Down Expand Up @@ -178,6 +179,7 @@ export function CustomChatScreen({ navigation, route }: Props): JSX.Element {
esRef.current = sseRequestChatCompletions(urlOptions, customizedOptions, messages, {
onNext: content => {
setContent(content)
onNextHaptic()
scrollToTop(0)
},
onError: (code, message) => {
Expand Down
22 changes: 14 additions & 8 deletions src/screens/main/modes/ModeScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
useQueryFindModeResultWhere,
} from '../../../db/table/t-mode-result'
import { hapticError, hapticSoft, hapticSuccess } from '../../../haptic'
import { useHapticFeedbackMessaging } from '../../../haptic/hooks'
import { useRefetchFocusEffect } from '../../../hooks'
import { useOpenAIApiCustomizedOptions, useOpenAIApiUrlOptions } from '../../../http/apis/hooks'
import { sseRequestChatCompletions } from '../../../http/apis/v1/chat/completions'
Expand Down Expand Up @@ -49,15 +50,9 @@ export type ModeSceneHandle = {

export const ModeScene = React.forwardRef<ModeSceneHandle, ModeSceneProps>((props, ref) => {
const { style, focused, targetLang, translatorMode } = props

const ttsModalRef = useRef<TTSModalHandle>(null)

const { t } = useTranslation()
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()

const { urlOptions, checkIsOptionsValid } = useOpenAIApiUrlOptions()
const customizedOptions = useOpenAIApiCustomizedOptions()
const [status, setStatus] = useState<TranslatorStatus>('none')
const ttsModalRef = useRef<TTSModalHandle>(null)

// input
const inputViewRef = useRef<InputViewHandle>(null)
Expand Down Expand Up @@ -137,6 +132,11 @@ export const ModeScene = React.forwardRef<ModeSceneHandle, ModeSceneProps>((prop
})
}

const { onNextHaptic } = useHapticFeedbackMessaging()
const { urlOptions, checkIsOptionsValid } = useOpenAIApiUrlOptions()
const customizedOptions = useOpenAIApiCustomizedOptions()
const [status, setStatus] = useState<TranslatorStatus>('none')

const eventSourceRef = useRef<EventSource | null>(null)
const perfromChatCompletions = (messages: ApiMessage[]) => {
if (!checkIsOptionsValid()) {
Expand All @@ -150,6 +150,7 @@ export const ModeScene = React.forwardRef<ModeSceneHandle, ModeSceneProps>((prop
setStatus('pending')
eventSourceRef.current = sseRequestChatCompletions(urlOptions, customizedOptions, messages, {
onNext: content => {
onNextHaptic()
outputViewRef.current?.updateText(content)
},
onError: (code, message) => {
Expand Down Expand Up @@ -263,7 +264,12 @@ export const ModeScene = React.forwardRef<ModeSceneHandle, ModeSceneProps>((prop
<ScrollView
style={{ flex: 1, marginTop: dimensions.edge }}
contentContainerStyle={{ paddingBottom: dimensions.spaceBottom }}>
<OutputView ref={outputViewRef} assistantText={assistantText} text={outputText} />
<OutputView
ref={outputViewRef}
status={status}
assistantText={assistantText}
outputText={outputText}
/>
<View style={styles.toolsRow}>
<ModeSceneResultButton
mode={translatorMode}
Expand Down
19 changes: 11 additions & 8 deletions src/screens/main/modes/OutputView.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import { dimensions } from '../../../res/dimensions'
import { stylez } from '../../../res/stylez'
import { texts } from '../../../res/texts'
import { useThemeScheme } from '../../../themes/hooks'
import { TranslatorStatus } from '../../../types'
import React, { useImperativeHandle, useState } from 'react'
import { StyleProp, StyleSheet, Text, TextStyle } from 'react-native'

export interface OutputViewProps {
style?: StyleProp<TextStyle>
status: TranslatorStatus
assistantText: string | null
text: string
outputText: string
}

export interface OutputViewHandle {
updateText: (text: string) => void
}

export const OutputView = React.forwardRef<OutputViewHandle, OutputViewProps>((props, ref) => {
const { style, assistantText, text } = props
const { style, status, assistantText, outputText } = props
const { text: textColor, text3: text3Color } = useThemeScheme()
const color = text !== assistantText ? text3Color : textColor
const color = outputText !== assistantText ? text3Color : textColor

const [displayText, setDisplayText] = useState('')
const [preText, setPreText] = useState(text)
if (text !== preText) {
setPreText(text)
setDisplayText(text)
const [preOutputText, setPreOutputText] = useState(outputText)
if (outputText !== preOutputText) {
setPreOutputText(outputText)
setDisplayText(outputText)
}

useImperativeHandle(ref, () => ({
Expand All @@ -32,7 +35,7 @@ export const OutputView = React.forwardRef<OutputViewHandle, OutputViewProps>((p

return (
<Text selectable style={[stylez.contentText, styles.text, { color }, style]}>
{displayText}
{`${displayText}${status === 'pending' ? texts.assistantCursor : ''}`}
</Text>
)
})
Expand Down
16 changes: 10 additions & 6 deletions src/screens/mode-chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useQueryModeChatMessageOfResultId,
} from '../../db/table/t-mode-chat-message'
import { hapticError, hapticSuccess, hapticWarning } from '../../haptic'
import { useHapticFeedbackMessaging } from '../../haptic/hooks'
import { useOpenAIApiCustomizedOptions, useOpenAIApiUrlOptions } from '../../http/apis/hooks'
import { sseRequestChatCompletions } from '../../http/apis/v1/chat/completions'
import { DEFAULTS } from '../../preferences/defaults'
Expand Down Expand Up @@ -77,16 +78,14 @@ export function ModeChatScreen({ navigation, route }: Props): JSX.Element {
const assistantIconName = getAssistantIconName(translatorMode)

const { t } = useTranslation()
const [clearMessagesModalVisible, setClearMessagesModalVisible] = useState(false)
const fontSize = DEFAULTS.fontSize

const { urlOptions, checkIsOptionsValid } = useOpenAIApiUrlOptions()
const customizedOptions = useOpenAIApiCustomizedOptions()

const { backgroundChat: backgroundColor } = useThemeScheme()

const [showChatAvatar] = useShowChatAvatarPref()
const [colouredContextMessage] = useColouredContextMessagePref()

const [clearMessagesModalVisible, setClearMessagesModalVisible] = useState(false)
const fontSize = DEFAULTS.fontSize

const { height: keyboardHeight } = useReanimatedKeyboardAnimation()
const transformStyle = useAnimatedStyle(() => {
return { transform: [{ translateY: keyboardHeight.value }] }
Expand Down Expand Up @@ -154,6 +153,10 @@ export function ModeChatScreen({ navigation, route }: Props): JSX.Element {

const [inputText, setInputText] = useState('')

const { onNextHaptic } = useHapticFeedbackMessaging()
const { urlOptions, checkIsOptionsValid } = useOpenAIApiUrlOptions()
const customizedOptions = useOpenAIApiCustomizedOptions()

const status = useSSEMessageStore(state => state.status)
const sendDisabled = inputText.trim() && status !== 'sending' ? false : true
const setStatus = useSSEMessageStore(state => state.setStatus)
Expand Down Expand Up @@ -192,6 +195,7 @@ export function ModeChatScreen({ navigation, route }: Props): JSX.Element {
esRef.current = sseRequestChatCompletions(urlOptions, customizedOptions, messages, {
onNext: content => {
setContent(content)
onNextHaptic()
scrollToTop(0)
},
onError: (code, message) => {
Expand Down
Loading

0 comments on commit 87eed58

Please sign in to comment.