diff --git a/app/components/channel_actions/copy_channel_link_option/copy_channel_link_option.tsx b/app/components/channel_actions/copy_channel_link_option/copy_channel_link_option.tsx index 2331b143c..997f7bb44 100644 --- a/app/components/channel_actions/copy_channel_link_option/copy_channel_link_option.tsx +++ b/app/components/channel_actions/copy_channel_link_option/copy_channel_link_option.tsx @@ -35,7 +35,7 @@ const CopyChannelLinkOption = ({channelName, teamName, showAsLabel, testID}: Pro ); diff --git a/app/components/channel_actions/info_box/index.tsx b/app/components/channel_actions/info_box/index.tsx index 036e4a2b6..f55fbd320 100644 --- a/app/components/channel_actions/info_box/index.tsx +++ b/app/components/channel_actions/info_box/index.tsx @@ -45,7 +45,7 @@ const InfoBox = ({channelId, containerStyle, showAsLabel = false, testID}: Props if (showAsLabel) { return ( { dismissBottomSheet(); let username = mentionName; @@ -182,7 +182,7 @@ const AtMention = ({ /> { dismissBottomSheet(); }} diff --git a/app/components/markdown/markdown_code_block/index.tsx b/app/components/markdown/markdown_code_block/index.tsx index 903504d44..a9d799b96 100644 --- a/app/components/markdown/markdown_code_block/index.tsx +++ b/app/components/markdown/markdown_code_block/index.tsx @@ -123,7 +123,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc style={style.bottomSheet} > { dismissBottomSheet(); Clipboard.setString(content); @@ -133,7 +133,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc /> { dismissBottomSheet(); }} diff --git a/app/components/markdown/markdown_image/index.tsx b/app/components/markdown/markdown_image/index.tsx index 5cc5a62ef..5cb5f6a8a 100644 --- a/app/components/markdown/markdown_image/index.tsx +++ b/app/components/markdown/markdown_image/index.tsx @@ -157,7 +157,7 @@ const MarkdownImage = ({ style={style.bottomSheet} > { dismissBottomSheet(); Clipboard.setString(linkDestination || source); @@ -167,7 +167,7 @@ const MarkdownImage = ({ /> { dismissBottomSheet(); }} diff --git a/app/components/markdown/markdown_latex_block/index.tsx b/app/components/markdown/markdown_latex_block/index.tsx index 3b7dd644c..65437cb9e 100644 --- a/app/components/markdown/markdown_latex_block/index.tsx +++ b/app/components/markdown/markdown_latex_block/index.tsx @@ -139,7 +139,7 @@ const LatexCodeBlock = ({content, theme}: Props) => { style={styles.bottomSheet} > { dismissBottomSheet(); Clipboard.setString(content); @@ -149,7 +149,7 @@ const LatexCodeBlock = ({content, theme}: Props) => { /> { dismissBottomSheet(); Clipboard.setString(href); @@ -126,7 +126,7 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU /> { dismissBottomSheet(); }} diff --git a/app/components/post_draft/quick_actions/camera_quick_action/camera_type.tsx b/app/components/post_draft/quick_actions/camera_quick_action/camera_type.tsx index e681c7a56..c5198299a 100644 --- a/app/components/post_draft/quick_actions/camera_quick_action/camera_type.tsx +++ b/app/components/post_draft/quick_actions/camera_quick_action/camera_type.tsx @@ -66,13 +66,13 @@ const CameraType = ({onPress}: Props) => { /> } { style={styles.bottomSheet} > { dismissBottomSheet(); retryFailedPost(serverUrl, post); @@ -54,7 +54,7 @@ const Failed = ({post, theme}: FailedProps) => { /> { dismissBottomSheet(); removePost(serverUrl, post); diff --git a/app/components/slide_up_panel_item/index.tsx b/app/components/slide_up_panel_item/index.tsx index ce02022c5..37a4c4adf 100644 --- a/app/components/slide_up_panel_item/index.tsx +++ b/app/components/slide_up_panel_item/index.tsx @@ -14,10 +14,12 @@ import {isValidUrl} from '@utils/url'; type SlideUpPanelProps = { destructive?: boolean; - icon?: string | Source; - rightIcon?: boolean; - imageStyles?: StyleProp; - iconStyles?: StyleProp; + leftIcon?: string | Source; + leftImageStyles?: StyleProp; + leftIconStyles?: StyleProp; + rightIcon?: string | Source; + rightImageStyles?: StyleProp; + rightIconStyles?: StyleProp; onPress: () => void; textStyles?: TextStyle; testID?: string; @@ -65,13 +67,55 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }; }); -const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text, textStyles, iconStyles, rightIcon = false}: SlideUpPanelProps) => { +const SlideUpPanelItem = ({ + destructive = false, + leftIcon, + leftImageStyles, + leftIconStyles, + rightIcon, + rightImageStyles, + rightIconStyles, + onPress, + testID, + text, + textStyles, +}: SlideUpPanelProps) => { const theme = useTheme(); + const style = getStyleSheet(theme); + + const {image: leftImage, iconStyle: leftIconStyle} = useImageAndStyle(leftIcon, leftImageStyles, leftIconStyles, destructive); + const {image: rightImage, iconStyle: rightIconStyle} = useImageAndStyle(rightIcon, rightImageStyles, rightIconStyles, destructive); + const handleOnPress = useCallback(preventDoubleTap(onPress, 500), []); + + return ( + + + {Boolean(leftImage) && + {leftImage} + } + + {text} + + {Boolean(rightImage) && + {rightImage} + } + + + ); +}; + +const useImageAndStyle = (icon: string | Source | undefined, imageStyles: StyleProp, iconStyles: StyleProp, destructive: boolean) => { + const theme = useTheme(); const style = getStyleSheet(theme); let image; - let iconStyle: StyleProp = [style.iconContainer]; + let iconStyle: Array> = [style.iconContainer]; if (icon) { if (typeof icon === 'object') { if (icon.uri && isValidUrl(icon.uri)) { @@ -101,26 +145,7 @@ const SlideUpPanelItem = ({destructive, icon, imageStyles, onPress, testID, text } } - return ( - - - {Boolean(image) && !rightIcon && - {image} - } - - {text} - - {Boolean(image) && rightIcon && - {image} - } - - - ); + return {image, iconStyle}; }; export default SlideUpPanelItem; diff --git a/app/products/calls/components/audio_device_button.tsx b/app/products/calls/components/audio_device_button.tsx index 47c339bec..a62fdbaf6 100644 --- a/app/products/calls/components/audio_device_button.tsx +++ b/app/products/calls/components/audio_device_button.tsx @@ -14,7 +14,7 @@ import {Device} from '@constants'; import {useTheme} from '@context/theme'; import {bottomSheet, dismissBottomSheet} from '@screens/navigation'; import {bottomSheetSnapPoint} from '@utils/helpers'; -import {typography} from '@utils/typography'; +import {makeStyleSheetFromTheme} from '@utils/theme'; type Props = { pressableStyle: StyleProp; @@ -23,13 +23,16 @@ type Props = { currentCall: CurrentCall; } -const style = { - bold: typography('Body', 200, 'SemiBold'), -}; +const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => ({ + checkIcon: { + color: theme.buttonBg, + }, +})); export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, currentCall}: Props) => { const intl = useIntl(); const theme = useTheme(); + const style = getStyleFromTheme(theme); const {bottom} = useSafeAreaInsets(); const isTablet = Device.IS_TABLET; // not `useIsTablet` because even if we're in splitView, we're still using a tablet. const color = theme.awayIndicator; @@ -38,10 +41,14 @@ export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, c const tabletLabel = intl.formatMessage({id: 'mobile.calls_tablet', defaultMessage: 'Tablet'}); const speakerLabel = intl.formatMessage({id: 'mobile.calls_speaker', defaultMessage: 'SpeakerPhone'}); const bluetoothLabel = intl.formatMessage({id: 'mobile.calls_bluetooth', defaultMessage: 'Bluetooth'}); + const headsetLabel = intl.formatMessage({id: 'mobile.calls_headset', defaultMessage: 'Headset'}); const deviceSelector = useCallback(async () => { const currentDevice = audioDeviceInfo.selectedAudioDevice; - const available = audioDeviceInfo.availableAudioDeviceList; + let available = audioDeviceInfo.availableAudioDeviceList; + if (available.includes(AudioDevice.WiredHeadset)) { + available = available.filter((d) => d !== AudioDevice.Earpiece); + } const selectDevice = (device: AudioDevice) => { setPreferredAudioRoute(device); dismissBottomSheet(); @@ -52,34 +59,47 @@ export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, c {available.includes(AudioDevice.Earpiece) && isTablet && selectDevice(AudioDevice.Earpiece)} text={tabletLabel} - textStyles={currentDevice === AudioDevice.Earpiece ? {...style.bold, color} : {}} + rightIcon={currentDevice === AudioDevice.Earpiece ? 'check' : undefined} + rightIconStyles={currentDevice === AudioDevice.Earpiece ? style.checkIcon : {}} /> } {available.includes(AudioDevice.Earpiece) && !isTablet && selectDevice(AudioDevice.Earpiece)} text={phoneLabel} - textStyles={currentDevice === AudioDevice.Earpiece ? {...style.bold, color} : {}} + rightIcon={currentDevice === AudioDevice.Earpiece ? 'check' : undefined} + rightIconStyles={currentDevice === AudioDevice.Earpiece ? style.checkIcon : {}} /> } {available.includes(AudioDevice.Speakerphone) && selectDevice(AudioDevice.Speakerphone)} text={speakerLabel} - textStyles={currentDevice === AudioDevice.Speakerphone ? {...style.bold, color} : {}} + rightIcon={currentDevice === AudioDevice.Speakerphone ? 'check' : undefined} + rightIconStyles={currentDevice === AudioDevice.Speakerphone ? style.checkIcon : {}} /> } {available.includes(AudioDevice.Bluetooth) && selectDevice(AudioDevice.Bluetooth)} text={bluetoothLabel} - textStyles={currentDevice === AudioDevice.Bluetooth ? {...style.bold, color} : {}} + rightIcon={currentDevice === AudioDevice.Bluetooth ? 'check' : undefined} + rightIconStyles={currentDevice === AudioDevice.Bluetooth ? style.checkIcon : {}} + /> + } + {available.includes(AudioDevice.WiredHeadset) && + selectDevice(AudioDevice.WiredHeadset)} + text={headsetLabel} + rightIcon={currentDevice === AudioDevice.WiredHeadset ? 'check' : undefined} + rightIconStyles={currentDevice === AudioDevice.WiredHeadset ? style.checkIcon : {}} /> } @@ -89,7 +109,7 @@ export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, c await bottomSheet({ closeButtonId: 'close-other-actions', renderContent, - snapPoints: [1, bottomSheetSnapPoint(audioDeviceInfo.availableAudioDeviceList.length + 1, ITEM_HEIGHT, bottom)], + snapPoints: [1, bottomSheetSnapPoint(available.length + 1, ITEM_HEIGHT, bottom)], title: intl.formatMessage({id: 'mobile.calls_audio_device', defaultMessage: 'Select audio device'}), theme, }); @@ -106,6 +126,10 @@ export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, c icon = 'bluetooth'; label = bluetoothLabel; break; + case AudioDevice.WiredHeadset: + icon = 'headphones'; + label = headsetLabel; + break; } return ( diff --git a/app/products/calls/connection/connection.ts b/app/products/calls/connection/connection.ts index 0dd69a3d2..6d736e3ff 100644 --- a/app/products/calls/connection/connection.ts +++ b/app/products/calls/connection/connection.ts @@ -3,7 +3,7 @@ import {RTCMonitor, RTCPeer} from '@mattermost/calls/lib'; import {deflate} from 'pako'; -import {DeviceEventEmitter, type EmitterSubscription, Platform} from 'react-native'; +import {DeviceEventEmitter, type EmitterSubscription, NativeEventEmitter, NativeModules, Platform} from 'react-native'; import InCallManager from 'react-native-incall-manager'; import {mediaDevices, MediaStream, MediaStreamTrack, RTCPeerConnection} from 'react-native-webrtc'; @@ -24,6 +24,8 @@ import type {EmojiData} from '@mattermost/calls/lib/types'; const peerConnectTimeout = 5000; const rtcMonitorInterval = 4000; +const InCallManagerEmitter = new NativeEventEmitter(NativeModules.InCallManager); + export async function newConnection( serverUrl: string, channelID: string, @@ -40,6 +42,7 @@ export async function newConnection( let isClosed = false; let onCallEnd: EmitterSubscription | null = null; let audioDeviceChanged: EmitterSubscription | null = null; + let wiredHeadsetEvent: EmitterSubscription | null = null; const streams: MediaStream[] = []; let rtcMonitor: RTCMonitor | null = null; const logger = { @@ -106,6 +109,7 @@ export async function newConnection( peer = null; InCallManager.stop(); audioDeviceChanged?.remove(); + wiredHeadsetEvent?.remove(); if (closeCb) { closeCb(); @@ -224,29 +228,50 @@ export async function newConnection( let btInitialized = false; let speakerInitialized = false; - audioDeviceChanged = DeviceEventEmitter.addListener('onAudioDeviceChanged', (data: AudioDeviceInfoRaw) => { - const info: AudioDeviceInfo = { - availableAudioDeviceList: JSON.parse(data.availableAudioDeviceList), - selectedAudioDevice: data.selectedAudioDevice, - }; - setAudioDeviceInfo(info); + if (Platform.OS === 'android') { + audioDeviceChanged = DeviceEventEmitter.addListener('onAudioDeviceChanged', (data: AudioDeviceInfoRaw) => { + const info: AudioDeviceInfo = { + availableAudioDeviceList: JSON.parse(data.availableAudioDeviceList), + selectedAudioDevice: data.selectedAudioDevice, + }; + setAudioDeviceInfo(info); + logDebug('AudioDeviceChanged. info:', info); - // Auto switch to bluetooth the first time we connect to bluetooth, but not after. - if (!btInitialized) { - if (info.availableAudioDeviceList.includes(AudioDevice.Bluetooth)) { - setPreferredAudioRoute(AudioDevice.Bluetooth); - btInitialized = true; - } else if (!speakerInitialized) { - // If we don't have bluetooth available, default to speakerphone on. - setPreferredAudioRoute(AudioDevice.Speakerphone); - speakerInitialized = true; + // Auto switch to bluetooth the first time we connect to bluetooth, but not after. + if (!btInitialized) { + if (info.availableAudioDeviceList.includes(AudioDevice.Bluetooth)) { + setPreferredAudioRoute(AudioDevice.Bluetooth); + btInitialized = true; + } else if (!speakerInitialized) { + // If we don't have bluetooth available, default to speakerphone on. + setPreferredAudioRoute(AudioDevice.Speakerphone); + speakerInitialized = true; + } } - } - }); + }); + } - // We default to speakerphone (Android is handled above in the onAudioDeviceChanged handler above). + // We default to speakerphone, but not if the WiredHeadset is plugged in. if (Platform.OS === 'ios') { - setSpeakerphoneOn(true); + wiredHeadsetEvent = InCallManagerEmitter.addListener('WiredHeadset', (data) => { + // Log for customer debugging. For the moment we're not changing output labels because of incall-manager iOS + // limitations with how it reports Bluetooth -- namely that it doesn't, so we don't know when Bluetooth is + // overriding the earpiece and/or headset. + logDebug('WiredHeadset plugged in. Data:', data); + + // iOS switches to the headset when we connect it, so turn off speakerphone to keep UI in sync. + if (data.isPlugged) { + setSpeakerphoneOn(false); + } + }); + + // If headset is plugged in when the call starts, use it. + const report = await InCallManager.getIsWiredHeadsetPluggedIn(); + if (report.isWiredHeadsetPluggedIn) { + setSpeakerphoneOn(false); + } else { + setSpeakerphoneOn(true); + } } peer = new RTCPeer({ diff --git a/app/products/calls/screens/call_screen/call_screen.tsx b/app/products/calls/screens/call_screen/call_screen.tsx index 8deaad047..103ced171 100644 --- a/app/products/calls/screens/call_screen/call_screen.tsx +++ b/app/products/calls/screens/call_screen/call_screen.tsx @@ -477,7 +477,7 @@ const CallScreen = ({ { showStartRecording && @@ -485,14 +485,14 @@ const CallScreen = ({ { showStopRecording && } @@ -764,7 +764,6 @@ const CallScreen = ({ iconStyle={[ style.buttonIcon, isLandscape && style.buttonIconLandscape, - style.speakerphoneIcon, currentCall.speakerphoneOn && style.buttonOn, ]} buttonTextStyle={style.buttonText} diff --git a/app/screens/browse_channels/dropdown_slideup.tsx b/app/screens/browse_channels/dropdown_slideup.tsx index f9f8e51c1..c1ae0b44a 100644 --- a/app/screens/browse_channels/dropdown_slideup.tsx +++ b/app/screens/browse_channels/dropdown_slideup.tsx @@ -42,11 +42,6 @@ export default function DropdownSlideup({ const style = getStyleFromTheme(theme); const isTablet = useIsTablet(); - const commonProps = { - rightIcon: true, - iconStyles: style.checkIcon, - }; - const handlePublicPress = useCallback(() => { dismissBottomSheet(); onPress(PUBLIC); @@ -73,16 +68,16 @@ export default function DropdownSlideup({ onPress={handlePublicPress} testID='browse_channels.dropdown_slideup_item.public_channels' text={intl.formatMessage({id: 'browse_channels.publicChannels', defaultMessage: 'Public Channels'})} - icon={selected === PUBLIC ? 'check' : undefined} - {...commonProps} + rightIcon={selected === PUBLIC ? 'check' : undefined} + rightIconStyles={style.checkIcon} /> {canShowArchivedChannels && ( )} {sharedChannelsEnabled && ( @@ -90,8 +85,8 @@ export default function DropdownSlideup({ onPress={handleSharedPress} testID='browse_channels.dropdown_slideup_item.shared_channels' text={intl.formatMessage({id: 'browse_channels.sharedChannels', defaultMessage: 'Shared Channels'})} - icon={selected === SHARED ? 'check' : undefined} - {...commonProps} + rightIcon={selected === SHARED ? 'check' : undefined} + rightIconStyles={style.checkIcon} /> )} diff --git a/app/screens/edit_profile/components/panel_item.tsx b/app/screens/edit_profile/components/panel_item.tsx index 9bd3025a1..5cb414e9a 100644 --- a/app/screens/edit_profile/components/panel_item.tsx +++ b/app/screens/edit_profile/components/panel_item.tsx @@ -71,7 +71,7 @@ const PanelItem = ({pickerAction, pictureUtils, onRemoveProfileImage}: PanelItem return ( { )} setUserStatus(ONLINE)} testID='user_status.online.option' text={intl.formatMessage({ @@ -89,8 +89,8 @@ const UserStatus = ({currentUser}: Props) => { textStyles={styles.label} /> setUserStatus(AWAY)} testID='user_status.away.option' text={intl.formatMessage({ @@ -100,8 +100,8 @@ const UserStatus = ({currentUser}: Props) => { textStyles={styles.label} /> setUserStatus(DND)} testID='user_status.dnd.option' text={intl.formatMessage({ @@ -111,8 +111,8 @@ const UserStatus = ({currentUser}: Props) => { textStyles={styles.label} /> setUserStatus(OFFLINE)} testID='user_status.offline.option' text={intl.formatMessage({ diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 15707f980..4b8a02ce2 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -435,6 +435,7 @@ "mobile.calls_ended_at": "Ended at", "mobile.calls_error_message": "Error: {error}", "mobile.calls_error_title": "Error", + "mobile.calls_headset": "Headset", "mobile.calls_host": "host", "mobile.calls_host_rec": "You are recording this meeting. Consider letting everyone know that this meeting is being recorded.", "mobile.calls_host_rec_error": "Please try to record again. You can also contact your system admin for troubleshooting help.", diff --git a/package-lock.json b/package-lock.json index 057f7c33e..7f7928ca5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@formatjs/intl-relativetimeformat": "11.2.4", "@gorhom/bottom-sheet": "4.4.7", "@mattermost/calls": "github:mattermost/calls-common#v0.17.0", - "@mattermost/compass-icons": "0.1.37", + "@mattermost/compass-icons": "0.1.38", "@mattermost/react-native-emm": "1.3.5", "@mattermost/react-native-network-client": "1.4.1", "@mattermost/react-native-paste-input": "0.6.4", @@ -3436,9 +3436,9 @@ } }, "node_modules/@mattermost/compass-icons": { - "version": "0.1.37", - "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.37.tgz", - "integrity": "sha512-4me1W0hj1nu8A1gpdQA6cij/hyb2P7uIYMJQ+xrNvn5ImTRfQ67LEdyNOa/LY+oT1NO2ui6kKOs8oLUehIaneg==" + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.38.tgz", + "integrity": "sha512-JfLQJtvxD++7lw+jiLWs55+SOhF8wPQgfTCOEi/uyl9bRxfY736a9fPzRcRJwF80se7lPMxvAx8itwvTz0o7CQ==" }, "node_modules/@mattermost/react-native-emm": { "version": "1.3.5", @@ -25415,9 +25415,9 @@ } }, "@mattermost/compass-icons": { - "version": "0.1.37", - "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.37.tgz", - "integrity": "sha512-4me1W0hj1nu8A1gpdQA6cij/hyb2P7uIYMJQ+xrNvn5ImTRfQ67LEdyNOa/LY+oT1NO2ui6kKOs8oLUehIaneg==" + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.38.tgz", + "integrity": "sha512-JfLQJtvxD++7lw+jiLWs55+SOhF8wPQgfTCOEi/uyl9bRxfY736a9fPzRcRJwF80se7lPMxvAx8itwvTz0o7CQ==" }, "@mattermost/react-native-emm": { "version": "1.3.5", diff --git a/package.json b/package.json index fa464345b..5f5ea59c9 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@formatjs/intl-relativetimeformat": "11.2.4", "@gorhom/bottom-sheet": "4.4.7", "@mattermost/calls": "github:mattermost/calls-common#v0.17.0", - "@mattermost/compass-icons": "0.1.37", + "@mattermost/compass-icons": "0.1.38", "@mattermost/react-native-emm": "1.3.5", "@mattermost/react-native-network-client": "1.4.1", "@mattermost/react-native-paste-input": "0.6.4",