-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: Voice support #6918
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
base: develop
Are you sure you want to change the base?
feat: Voice support #6918
Changes from 28 commits
4702ee7
e532171
1299b0e
d2cef3d
b0b78cd
0d18314
4b33a79
ece7e27
c3dd2ae
1df1b29
8f9129e
8a5de04
a9ec70d
d6229d9
e718561
26502cb
db29a47
2b16f4b
eae9137
bb2a8bb
10593d6
b6766f3
ac85af8
9b28770
5c5e2be
01e42e2
aa3ca88
548e855
abbb072
77cb36e
59f25eb
cd74d43
9b71cf9
7348135
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| export default { | ||
| setup: jest.fn(), | ||
| canMakeMultipleCalls: jest.fn(), | ||
| displayIncomingCall: jest.fn(), | ||
| endCall: jest.fn(), | ||
| setCurrentCallActive: jest.fn(), | ||
| addEventListener: jest.fn((event, callback) => ({ | ||
| remove: jest.fn() | ||
| })) | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { StyleSheet, View } from 'react-native'; | ||
| import { useSafeAreaInsets } from 'react-native-safe-area-context'; | ||
|
|
||
| import { useTheme } from '../../theme'; | ||
| import Collapse from './components/Collapse'; | ||
| import Title from './components/Title'; | ||
| import EndCall from './components/EndCall'; | ||
| import { useCallStore } from '../../lib/services/voip/useCallStore'; | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| header: { | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-between', | ||
| alignItems: 'center', | ||
| paddingHorizontal: 12, | ||
| paddingBottom: 4, | ||
| borderBottomWidth: StyleSheet.hairlineWidth | ||
| } | ||
| }); | ||
|
|
||
| const CallHeader = () => { | ||
| 'use memo'; | ||
|
|
||
| const { colors } = useTheme(); | ||
| const insets = useSafeAreaInsets(); | ||
|
|
||
| const defaultHeaderStyle = { | ||
| backgroundColor: colors.surfaceNeutral, | ||
| paddingTop: insets.top | ||
| }; | ||
|
|
||
| const call = useCallStore(state => state.call); | ||
| if (!call) { | ||
| return <View style={defaultHeaderStyle} />; | ||
| } | ||
|
|
||
| return ( | ||
| <View style={[styles.header, { ...defaultHeaderStyle, borderBottomColor: colors.strokeLight }]}> | ||
| <Collapse /> | ||
| <Title /> | ||
| <EndCall /> | ||
| </View> | ||
| ); | ||
| }; | ||
|
|
||
| export default CallHeader; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import I18n from '../../../i18n'; | ||
| import { useTheme } from '../../../theme'; | ||
| import { useCallStore } from '../../../lib/services/voip/useCallStore'; | ||
| import * as HeaderButton from '../../Header/components/HeaderButton'; | ||
|
|
||
| const Collapse = () => { | ||
| 'use memo'; | ||
|
|
||
| const { colors } = useTheme(); | ||
| const focused = useCallStore(state => state.focused); | ||
| const toggleFocus = useCallStore(state => state.toggleFocus); | ||
| return ( | ||
| <HeaderButton.Container left> | ||
| <HeaderButton.Item | ||
| accessibilityLabel={I18n.t('Minimize')} | ||
| onPress={toggleFocus} | ||
| iconName={focused ? 'arrow-collapse' : 'arrow-expand'} | ||
| color={colors.fontDefault} | ||
| /> | ||
| </HeaderButton.Container> | ||
| ); | ||
| }; | ||
|
|
||
| export default Collapse; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import I18n from '../../../i18n'; | ||
| import { useTheme } from '../../../theme'; | ||
| import { useCallStore } from '../../../lib/services/voip/useCallStore'; | ||
| import * as HeaderButton from '../../Header/components/HeaderButton'; | ||
|
|
||
| const EndCall = () => { | ||
| 'use memo'; | ||
|
|
||
| const { colors } = useTheme(); | ||
| const endCall = useCallStore(state => state.endCall); | ||
| return ( | ||
| <HeaderButton.Container> | ||
| <HeaderButton.Item accessibilityLabel={I18n.t('End')} onPress={endCall} iconName='phone-end' color={colors.fontDanger} /> | ||
| </HeaderButton.Container> | ||
| ); | ||
| }; | ||
|
|
||
| export default EndCall; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { useEffect, useState } from 'react'; | ||
| import { Text } from 'react-native'; | ||
|
|
||
| import { useCallStore } from '../../../lib/services/voip/useCallStore'; | ||
|
|
||
| const formatDuration = (seconds: number): string => { | ||
| const hours = Math.floor(seconds / 3600); | ||
| const mins = Math.floor((seconds % 3600) / 60); | ||
| const secs = seconds % 60; | ||
| const hoursStr = hours > 0 ? `${hours.toString().padStart(2, '0')}:` : ''; | ||
| return `${hoursStr}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | ||
| }; | ||
|
|
||
| const Timer = () => { | ||
| 'use memo'; | ||
|
|
||
| const callStartTime = useCallStore(state => state.callStartTime); | ||
| const [duration, setDuration] = useState(callStartTime ? Math.floor((new Date().getTime() - callStartTime) / 1000) : 0); | ||
|
|
||
| useEffect(() => { | ||
| if (!callStartTime) { | ||
| return; | ||
| } | ||
| const updateDuration = () => { | ||
| setDuration(Math.floor((Date.now() - callStartTime) / 1000)); | ||
| }; | ||
| updateDuration(); | ||
| const interval = setInterval(updateDuration, 1000); | ||
| return () => clearInterval(interval); | ||
| }, [callStartTime]); | ||
|
|
||
| return <Text>{formatDuration(duration)}</Text>; | ||
| }; | ||
|
|
||
| export default Timer; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { StyleSheet, Text } from 'react-native'; | ||
|
|
||
| import { useTheme } from '../../../theme'; | ||
| import { useCallStore } from '../../../lib/services/voip/useCallStore'; | ||
| import I18n from '../../../i18n'; | ||
| import sharedStyles from '../../../views/Styles'; | ||
| import Timer from './Timer'; | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| headerTitle: { | ||
| ...sharedStyles.textSemibold, | ||
| fontSize: 16, | ||
| lineHeight: 24 | ||
| } | ||
| }); | ||
|
|
||
| const Title = () => { | ||
| 'use memo'; | ||
|
|
||
| const { colors } = useTheme(); | ||
| const callState = useCallStore(state => state.callState); | ||
| const callStartTime = useCallStore(state => state.callStartTime); | ||
| const contact = useCallStore(state => state.contact); | ||
|
|
||
| const callerName = contact.displayName || contact.username || I18n.t('Unknown'); | ||
| const isConnecting = callState === 'none' || callState === 'ringing' || callState === 'accepted'; | ||
| const isConnected = callState === 'active'; | ||
|
|
||
| const getHeaderTitle = () => { | ||
| if (isConnecting) { | ||
| return I18n.t('Connecting'); | ||
| } | ||
| if (isConnected && callStartTime) { | ||
| return `${callerName} – `; | ||
| } | ||
| return callerName; | ||
| }; | ||
|
|
||
| return ( | ||
| <Text style={[styles.headerTitle, { color: colors.fontDefault }]} testID='call-view-header-title'> | ||
| {getHeaderTitle()} | ||
| <Timer /> | ||
| </Text> | ||
| ); | ||
|
Comment on lines
+39
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Timer renders unconditionally, even during "Connecting" state. The return (
<Text style={[styles.headerTitle, { color: colors.fontDefault }]} testID='call-view-header-title'>
{getHeaderTitle()}
- <Timer />
+ {isConnected && callStartTime ? <Timer /> : null}
</Text>
);🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| export default Title; | ||
Large diffs are not rendered by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add
android:required="false"to avoid filtering devices.Without
required="false", Play Store will filter out devices that lack these hardware features. Most VoIP apps should work on devices without dedicated audio output or microphone hardware (e.g., tablets using Bluetooth).Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents