@stream-io/video-react-native-sdk 1.29.4-beta.0 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/CHANGELOG.md +3162 -0
  2. package/android/src/main/AndroidManifest.xml +1 -8
  3. package/android/src/main/AndroidManifestNew.xml +0 -11
  4. package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +5 -42
  5. package/android/src/main/java/com/streamvideo/reactnative/audio/utils/WebRtcAudioUtils.kt +6 -70
  6. package/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt +4 -6
  7. package/android/src/main/java/com/streamvideo/reactnative/util/CallAliveServiceChecker.kt +95 -0
  8. package/dist/commonjs/components/Call/CallContent/CallContent.js +13 -7
  9. package/dist/commonjs/components/Call/CallContent/CallContent.js.map +1 -1
  10. package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js +50 -14
  11. package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
  12. package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js +27 -0
  13. package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
  14. package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
  15. package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
  16. package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js +12 -9
  17. package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
  18. package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js +19 -4
  19. package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
  20. package/dist/commonjs/hooks/push/index.js +2 -0
  21. package/dist/commonjs/hooks/push/index.js.map +1 -1
  22. package/dist/commonjs/hooks/push/useIosCallkeepWithCallingStateEffect.js +160 -0
  23. package/dist/commonjs/hooks/push/useIosCallkeepWithCallingStateEffect.js.map +1 -0
  24. package/dist/commonjs/hooks/push/useIosVoipPushEventsSetupEffect.js +31 -18
  25. package/dist/commonjs/hooks/push/useIosVoipPushEventsSetupEffect.js.map +1 -1
  26. package/dist/commonjs/hooks/push/useProcessPushCallEffect.js +67 -0
  27. package/dist/commonjs/hooks/push/useProcessPushCallEffect.js.map +1 -0
  28. package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js +97 -64
  29. package/dist/commonjs/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
  30. package/dist/commonjs/index.js +0 -1
  31. package/dist/commonjs/index.js.map +1 -1
  32. package/dist/commonjs/modules/call-manager/CallManager.js +0 -26
  33. package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
  34. package/dist/commonjs/providers/StreamCall/index.js +6 -6
  35. package/dist/commonjs/providers/StreamCall/index.js.map +1 -1
  36. package/dist/commonjs/utils/StreamVideoRN/index.js +21 -33
  37. package/dist/commonjs/utils/StreamVideoRN/index.js.map +1 -1
  38. package/dist/commonjs/utils/hooks/index.js +0 -11
  39. package/dist/commonjs/utils/hooks/index.js.map +1 -1
  40. package/dist/commonjs/utils/internal/registerSDKGlobals.js +3 -52
  41. package/dist/commonjs/utils/internal/registerSDKGlobals.js.map +1 -1
  42. package/dist/commonjs/utils/push/android.js +202 -151
  43. package/dist/commonjs/utils/push/android.js.map +1 -1
  44. package/dist/commonjs/utils/push/internal/ios.js +34 -17
  45. package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
  46. package/dist/commonjs/utils/push/internal/rxSubjects.js +45 -1
  47. package/dist/commonjs/utils/push/internal/rxSubjects.js.map +1 -1
  48. package/dist/commonjs/utils/push/internal/utils.js +20 -32
  49. package/dist/commonjs/utils/push/internal/utils.js.map +1 -1
  50. package/dist/commonjs/utils/push/ios.js.map +1 -1
  51. package/dist/commonjs/utils/push/libs/callkeep.js +17 -0
  52. package/dist/commonjs/utils/push/libs/callkeep.js.map +1 -0
  53. package/dist/commonjs/utils/push/libs/index.js +19 -8
  54. package/dist/commonjs/utils/push/libs/index.js.map +1 -1
  55. package/dist/commonjs/utils/push/libs/notifee/index.js +19 -0
  56. package/dist/commonjs/utils/push/libs/notifee/index.js.map +1 -1
  57. package/dist/commonjs/utils/push/libs/voipPushNotification.js +17 -0
  58. package/dist/commonjs/utils/push/libs/voipPushNotification.js.map +1 -0
  59. package/dist/commonjs/utils/push/setupIosCallKeepEvents.js +205 -0
  60. package/dist/commonjs/utils/push/setupIosCallKeepEvents.js.map +1 -0
  61. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js +6 -7
  62. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js.map +1 -1
  63. package/dist/commonjs/version.js +1 -1
  64. package/dist/commonjs/version.js.map +1 -1
  65. package/dist/module/components/Call/CallContent/CallContent.js +10 -4
  66. package/dist/module/components/Call/CallContent/CallContent.js.map +1 -1
  67. package/dist/module/components/Call/CallContent/RTCViewPipIOS.js +52 -16
  68. package/dist/module/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
  69. package/dist/module/components/Call/CallContent/RTCViewPipNative.js +27 -0
  70. package/dist/module/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
  71. package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
  72. package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
  73. package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js +15 -12
  74. package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
  75. package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js +20 -5
  76. package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
  77. package/dist/module/hooks/push/index.js +2 -0
  78. package/dist/module/hooks/push/index.js.map +1 -1
  79. package/dist/module/hooks/push/useIosCallkeepWithCallingStateEffect.js +153 -0
  80. package/dist/module/hooks/push/useIosCallkeepWithCallingStateEffect.js.map +1 -0
  81. package/dist/module/hooks/push/useIosVoipPushEventsSetupEffect.js +31 -18
  82. package/dist/module/hooks/push/useIosVoipPushEventsSetupEffect.js.map +1 -1
  83. package/dist/module/hooks/push/useProcessPushCallEffect.js +60 -0
  84. package/dist/module/hooks/push/useProcessPushCallEffect.js.map +1 -0
  85. package/dist/module/hooks/useAndroidKeepCallAliveEffect.js +99 -66
  86. package/dist/module/hooks/useAndroidKeepCallAliveEffect.js.map +1 -1
  87. package/dist/module/index.js +0 -1
  88. package/dist/module/index.js.map +1 -1
  89. package/dist/module/modules/call-manager/CallManager.js +0 -26
  90. package/dist/module/modules/call-manager/CallManager.js.map +1 -1
  91. package/dist/module/providers/StreamCall/index.js +6 -6
  92. package/dist/module/providers/StreamCall/index.js.map +1 -1
  93. package/dist/module/utils/StreamVideoRN/index.js +21 -33
  94. package/dist/module/utils/StreamVideoRN/index.js.map +1 -1
  95. package/dist/module/utils/hooks/index.js +0 -1
  96. package/dist/module/utils/hooks/index.js.map +1 -1
  97. package/dist/module/utils/internal/registerSDKGlobals.js +3 -52
  98. package/dist/module/utils/internal/registerSDKGlobals.js.map +1 -1
  99. package/dist/module/utils/push/android.js +204 -153
  100. package/dist/module/utils/push/android.js.map +1 -1
  101. package/dist/module/utils/push/internal/ios.js +34 -17
  102. package/dist/module/utils/push/internal/ios.js.map +1 -1
  103. package/dist/module/utils/push/internal/rxSubjects.js +44 -0
  104. package/dist/module/utils/push/internal/rxSubjects.js.map +1 -1
  105. package/dist/module/utils/push/internal/utils.js +19 -29
  106. package/dist/module/utils/push/internal/utils.js.map +1 -1
  107. package/dist/module/utils/push/ios.js.map +1 -1
  108. package/dist/module/utils/push/libs/callkeep.js +11 -0
  109. package/dist/module/utils/push/libs/callkeep.js.map +1 -0
  110. package/dist/module/utils/push/libs/index.js +2 -1
  111. package/dist/module/utils/push/libs/index.js.map +1 -1
  112. package/dist/module/utils/push/libs/notifee/index.js +18 -0
  113. package/dist/module/utils/push/libs/notifee/index.js.map +1 -1
  114. package/dist/module/utils/push/libs/voipPushNotification.js +11 -0
  115. package/dist/module/utils/push/libs/voipPushNotification.js.map +1 -0
  116. package/dist/module/utils/push/setupIosCallKeepEvents.js +199 -0
  117. package/dist/module/utils/push/setupIosCallKeepEvents.js.map +1 -0
  118. package/dist/module/utils/push/setupIosVoipPushEvents.js +6 -7
  119. package/dist/module/utils/push/setupIosVoipPushEvents.js.map +1 -1
  120. package/dist/module/version.js +1 -1
  121. package/dist/module/version.js.map +1 -1
  122. package/dist/typescript/components/Call/CallContent/CallContent.d.ts.map +1 -1
  123. package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts.map +1 -1
  124. package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts +18 -0
  125. package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts.map +1 -1
  126. package/dist/typescript/components/Call/CallLayout/CallParticipantsGrid.d.ts.map +1 -1
  127. package/dist/typescript/components/Call/CallLayout/CallParticipantsSpotlight.d.ts.map +1 -1
  128. package/dist/typescript/components/Call/CallParticipantsList/CallParticipantsList.d.ts.map +1 -1
  129. package/dist/typescript/hooks/push/index.d.ts.map +1 -1
  130. package/dist/typescript/hooks/push/useIosCallkeepWithCallingStateEffect.d.ts +5 -0
  131. package/dist/typescript/hooks/push/useIosCallkeepWithCallingStateEffect.d.ts.map +1 -0
  132. package/dist/typescript/hooks/push/useIosVoipPushEventsSetupEffect.d.ts.map +1 -1
  133. package/dist/typescript/hooks/push/useProcessPushCallEffect.d.ts +8 -0
  134. package/dist/typescript/hooks/push/useProcessPushCallEffect.d.ts.map +1 -0
  135. package/dist/typescript/hooks/useAndroidKeepCallAliveEffect.d.ts.map +1 -1
  136. package/dist/typescript/index.d.ts +0 -1
  137. package/dist/typescript/index.d.ts.map +1 -1
  138. package/dist/typescript/modules/call-manager/CallManager.d.ts +0 -5
  139. package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
  140. package/dist/typescript/utils/StreamVideoRN/index.d.ts +2 -20
  141. package/dist/typescript/utils/StreamVideoRN/index.d.ts.map +1 -1
  142. package/dist/typescript/utils/StreamVideoRN/types.d.ts +29 -54
  143. package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
  144. package/dist/typescript/utils/hooks/index.d.ts +0 -1
  145. package/dist/typescript/utils/hooks/index.d.ts.map +1 -1
  146. package/dist/typescript/utils/internal/registerSDKGlobals.d.ts.map +1 -1
  147. package/dist/typescript/utils/push/android.d.ts +2 -1
  148. package/dist/typescript/utils/push/android.d.ts.map +1 -1
  149. package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
  150. package/dist/typescript/utils/push/internal/rxSubjects.d.ts +33 -0
  151. package/dist/typescript/utils/push/internal/rxSubjects.d.ts.map +1 -1
  152. package/dist/typescript/utils/push/internal/utils.d.ts +1 -8
  153. package/dist/typescript/utils/push/internal/utils.d.ts.map +1 -1
  154. package/dist/typescript/utils/push/ios.d.ts +2 -1
  155. package/dist/typescript/utils/push/ios.d.ts.map +1 -1
  156. package/dist/typescript/utils/push/libs/callkeep.d.ts +3 -0
  157. package/dist/typescript/utils/push/libs/callkeep.d.ts.map +1 -0
  158. package/dist/typescript/utils/push/libs/index.d.ts +2 -1
  159. package/dist/typescript/utils/push/libs/index.d.ts.map +1 -1
  160. package/dist/typescript/utils/push/libs/notifee/index.d.ts +1 -0
  161. package/dist/typescript/utils/push/libs/notifee/index.d.ts.map +1 -1
  162. package/dist/typescript/utils/push/libs/voipPushNotification.d.ts +3 -0
  163. package/dist/typescript/utils/push/libs/voipPushNotification.d.ts.map +1 -0
  164. package/dist/typescript/utils/push/setupIosCallKeepEvents.d.ts +6 -0
  165. package/dist/typescript/utils/push/setupIosCallKeepEvents.d.ts.map +1 -0
  166. package/dist/typescript/utils/push/setupIosVoipPushEvents.d.ts.map +1 -1
  167. package/dist/typescript/version.d.ts +1 -1
  168. package/dist/typescript/version.d.ts.map +1 -1
  169. package/expo-config-plugin/dist/withAndroidManifest.js +33 -1
  170. package/expo-config-plugin/dist/withAndroidPermissions.js +7 -2
  171. package/expo-config-plugin/dist/withAppDelegate.js +197 -19
  172. package/expo-config-plugin/dist/withMainActivity.js +1 -1
  173. package/expo-config-plugin/dist/withiOSInfoPlist.js +3 -2
  174. package/ios/PictureInPicture/PictureInPictureAvatarView.swift +273 -0
  175. package/ios/PictureInPicture/PictureInPictureConnectionQualityIndicator.swift +162 -0
  176. package/ios/PictureInPicture/PictureInPictureContent.swift +173 -0
  177. package/ios/PictureInPicture/PictureInPictureContentState.swift +123 -0
  178. package/ios/PictureInPicture/PictureInPictureDelegateProxy.swift +89 -0
  179. package/ios/PictureInPicture/PictureInPictureEnforcedStopAdapter.swift +166 -0
  180. package/ios/PictureInPicture/PictureInPictureLogger.swift +16 -0
  181. package/ios/PictureInPicture/PictureInPictureParticipantOverlayView.swift +217 -0
  182. package/ios/PictureInPicture/PictureInPictureReconnectionView.swift +193 -0
  183. package/ios/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +125 -7
  184. package/ios/PictureInPicture/StreamPictureInPictureController.swift +237 -63
  185. package/ios/PictureInPicture/StreamPictureInPictureControllerProtocol.swift +30 -0
  186. package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +384 -12
  187. package/ios/RTCViewPip.swift +187 -21
  188. package/ios/RTCViewPipManager.mm +9 -0
  189. package/ios/RTCViewPipManager.swift +3 -3
  190. package/ios/StreamInCallManager.m +0 -2
  191. package/ios/StreamInCallManager.swift +7 -19
  192. package/ios/StreamVideoReactNative.h +4 -7
  193. package/ios/StreamVideoReactNative.m +82 -189
  194. package/package.json +19 -14
  195. package/src/components/Call/CallContent/CallContent.tsx +16 -8
  196. package/src/components/Call/CallContent/RTCViewPipIOS.tsx +81 -15
  197. package/src/components/Call/CallContent/RTCViewPipNative.tsx +36 -0
  198. package/src/components/Call/CallLayout/CallParticipantsGrid.tsx +28 -14
  199. package/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +19 -10
  200. package/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +20 -5
  201. package/src/hooks/push/index.ts +2 -0
  202. package/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts +235 -0
  203. package/src/hooks/push/useIosVoipPushEventsSetupEffect.ts +34 -21
  204. package/src/hooks/push/useProcessPushCallEffect.ts +108 -0
  205. package/src/hooks/useAndroidKeepCallAliveEffect.ts +120 -94
  206. package/src/index.ts +0 -1
  207. package/src/modules/call-manager/CallManager.ts +0 -36
  208. package/src/modules/call-manager/native-module.d.ts +0 -7
  209. package/src/providers/StreamCall/index.tsx +6 -6
  210. package/src/utils/StreamVideoRN/index.ts +30 -40
  211. package/src/utils/StreamVideoRN/types.ts +29 -56
  212. package/src/utils/hooks/index.ts +0 -1
  213. package/src/utils/internal/registerSDKGlobals.ts +4 -47
  214. package/src/utils/push/android.ts +309 -227
  215. package/src/utils/push/internal/ios.ts +44 -28
  216. package/src/utils/push/internal/rxSubjects.ts +61 -0
  217. package/src/utils/push/internal/utils.ts +26 -45
  218. package/src/utils/push/ios.ts +6 -1
  219. package/src/utils/push/libs/callkeep.ts +16 -0
  220. package/src/utils/push/libs/index.ts +2 -1
  221. package/src/utils/push/libs/notifee/index.ts +27 -0
  222. package/src/utils/push/libs/voipPushNotification.ts +17 -0
  223. package/src/utils/push/setupIosCallKeepEvents.ts +252 -0
  224. package/src/utils/push/setupIosVoipPushEvents.ts +7 -11
  225. package/src/version.ts +1 -1
  226. package/android/src/main/java/com/streamvideo/reactnative/keepalive/KeepAliveNotification.kt +0 -83
  227. package/android/src/main/java/com/streamvideo/reactnative/keepalive/StreamCallKeepAliveHeadlessService.kt +0 -149
  228. package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js +0 -121
  229. package/dist/commonjs/hooks/push/useCallingExpWithCallingStateEffect.js.map +0 -1
  230. package/dist/commonjs/utils/hooks/useDebouncedValue.js +0 -24
  231. package/dist/commonjs/utils/hooks/useDebouncedValue.js.map +0 -1
  232. package/dist/commonjs/utils/internal/callingx/audioSessionPromise.js +0 -58
  233. package/dist/commonjs/utils/internal/callingx/audioSessionPromise.js.map +0 -1
  234. package/dist/commonjs/utils/internal/callingx/callingx.js +0 -109
  235. package/dist/commonjs/utils/internal/callingx/callingx.js.map +0 -1
  236. package/dist/commonjs/utils/keepCallAliveHeadlessTask.js +0 -48
  237. package/dist/commonjs/utils/keepCallAliveHeadlessTask.js.map +0 -1
  238. package/dist/commonjs/utils/push/libs/callingx.js +0 -75
  239. package/dist/commonjs/utils/push/libs/callingx.js.map +0 -1
  240. package/dist/commonjs/utils/push/setupCallingExpEvents.js +0 -108
  241. package/dist/commonjs/utils/push/setupCallingExpEvents.js.map +0 -1
  242. package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js +0 -114
  243. package/dist/module/hooks/push/useCallingExpWithCallingStateEffect.js.map +0 -1
  244. package/dist/module/utils/hooks/useDebouncedValue.js +0 -19
  245. package/dist/module/utils/hooks/useDebouncedValue.js.map +0 -1
  246. package/dist/module/utils/internal/callingx/audioSessionPromise.js +0 -51
  247. package/dist/module/utils/internal/callingx/audioSessionPromise.js.map +0 -1
  248. package/dist/module/utils/internal/callingx/callingx.js +0 -100
  249. package/dist/module/utils/internal/callingx/callingx.js.map +0 -1
  250. package/dist/module/utils/keepCallAliveHeadlessTask.js +0 -42
  251. package/dist/module/utils/keepCallAliveHeadlessTask.js.map +0 -1
  252. package/dist/module/utils/push/libs/callingx.js +0 -67
  253. package/dist/module/utils/push/libs/callingx.js.map +0 -1
  254. package/dist/module/utils/push/setupCallingExpEvents.js +0 -102
  255. package/dist/module/utils/push/setupCallingExpEvents.js.map +0 -1
  256. package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts +0 -5
  257. package/dist/typescript/hooks/push/useCallingExpWithCallingStateEffect.d.ts.map +0 -1
  258. package/dist/typescript/utils/hooks/useDebouncedValue.d.ts +0 -8
  259. package/dist/typescript/utils/hooks/useDebouncedValue.d.ts.map +0 -1
  260. package/dist/typescript/utils/internal/callingx/audioSessionPromise.d.ts +0 -16
  261. package/dist/typescript/utils/internal/callingx/audioSessionPromise.d.ts.map +0 -1
  262. package/dist/typescript/utils/internal/callingx/callingx.d.ts +0 -14
  263. package/dist/typescript/utils/internal/callingx/callingx.d.ts.map +0 -1
  264. package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts +0 -10
  265. package/dist/typescript/utils/keepCallAliveHeadlessTask.d.ts.map +0 -1
  266. package/dist/typescript/utils/push/libs/callingx.d.ts +0 -9
  267. package/dist/typescript/utils/push/libs/callingx.d.ts.map +0 -1
  268. package/dist/typescript/utils/push/setupCallingExpEvents.d.ts +0 -8
  269. package/dist/typescript/utils/push/setupCallingExpEvents.d.ts.map +0 -1
  270. package/src/hooks/push/useCallingExpWithCallingStateEffect.ts +0 -147
  271. package/src/utils/hooks/useDebouncedValue.ts +0 -21
  272. package/src/utils/internal/callingx/audioSessionPromise.ts +0 -53
  273. package/src/utils/internal/callingx/callingx.ts +0 -146
  274. package/src/utils/keepCallAliveHeadlessTask.ts +0 -54
  275. package/src/utils/push/libs/callingx.ts +0 -90
  276. package/src/utils/push/setupCallingExpEvents.ts +0 -130
@@ -9,7 +9,7 @@ const addNewLinesToAppDelegateObjc_1 = __importDefault(require("./common/addNewL
9
9
  const addToSwiftBridgingHeaderFile_1 = require("./common/addToSwiftBridgingHeaderFile");
10
10
  const withAppDelegate = (configuration, props) => {
11
11
  return (0, config_plugins_1.withAppDelegate)(configuration, (config) => {
12
- if (!props?.ringing &&
12
+ if (!props?.ringingPushNotifications &&
13
13
  !props?.iOSEnableMultitaskingCameraAccess &&
14
14
  !props?.addNoiseCancellation) {
15
15
  // quit early if no change is necessary
@@ -21,12 +21,19 @@ const withAppDelegate = (configuration, props) => {
21
21
  config.modResults.contents = (0, codeMod_1.addObjcImports)(config.modResults.contents, ['"NoiseCancellationManagerObjc.h"']);
22
22
  }
23
23
  config.modResults.contents = addDidFinishLaunchingWithOptionsObjc(config.modResults.contents, props.iOSEnableMultitaskingCameraAccess, props.addNoiseCancellation);
24
- if (props?.ringing) {
25
- config.modResults.contents = (0, codeMod_1.addObjcImports)(config.modResults.contents, ['<PushKit/PushKit.h>', '"StreamVideoReactNative.h"']);
24
+ if (props?.ringingPushNotifications) {
25
+ config.modResults.contents = (0, codeMod_1.addObjcImports)(config.modResults.contents, [
26
+ '"RNCallKeep.h"',
27
+ '<PushKit/PushKit.h>',
28
+ '"RNVoipPushNotificationManager.h"',
29
+ '"StreamVideoReactNative.h"',
30
+ '<WebRTC/RTCAudioSession.h>',
31
+ ]);
26
32
  config.modResults.contents =
27
- addDidFinishLaunchingWithOptionsRingingObjc(config.modResults.contents);
33
+ addDidFinishLaunchingWithOptionsRingingObjc(config.modResults.contents, props.ringingPushNotifications);
28
34
  config.modResults.contents = addDidUpdatePushCredentialsObjc(config.modResults.contents);
29
35
  config.modResults.contents = addDidReceiveIncomingPushCallbackObjc(config.modResults.contents);
36
+ config.modResults.contents = addAudioSessionMethodsObjc(config.modResults.contents);
30
37
  }
31
38
  return config;
32
39
  }
@@ -36,7 +43,7 @@ const withAppDelegate = (configuration, props) => {
36
43
  }
37
44
  else {
38
45
  try {
39
- if (props?.ringing) {
46
+ if (props?.ringingPushNotifications) {
40
47
  // make it public class AppDelegate: ExpoAppDelegate, PKPushRegistryDelegate {
41
48
  const regex = /(class\s+AppDelegate[^{]*)(\s*\{)/;
42
49
  config.modResults.contents = config.modResults.contents.replace(regex, (match, declarationPart, openBrace) => {
@@ -69,12 +76,13 @@ const withAppDelegate = (configuration, props) => {
69
76
  config.modResults.contents = (0, codeMod_1.addSwiftImports)(config.modResults.contents, ['stream_io_noise_cancellation_react_native']);
70
77
  }
71
78
  config.modResults.contents = addDidFinishLaunchingWithOptionsSwift(config.modResults.contents, props.iOSEnableMultitaskingCameraAccess, props.addNoiseCancellation);
72
- if (props?.ringing) {
73
- config.modResults.contents = (0, codeMod_1.addSwiftImports)(config.modResults.contents, ['PushKit', 'stream_video_react_native']);
79
+ if (props?.ringingPushNotifications) {
80
+ config.modResults.contents = (0, codeMod_1.addSwiftImports)(config.modResults.contents, ['RNCallKeep', 'PushKit', 'RNVoipPushNotification']);
74
81
  config.modResults.contents =
75
- addDidFinishLaunchingWithOptionsRingingSwift(config.modResults.contents);
82
+ addDidFinishLaunchingWithOptionsRingingSwift(config.modResults.contents, props.ringingPushNotifications);
76
83
  config.modResults.contents = addDidUpdatePushCredentialsSwift(config.modResults.contents);
77
84
  config.modResults.contents = addDidReceiveIncomingPushCallbackSwift(config.modResults.contents);
85
+ config.modResults.contents = addAudioSessionMethodsSwift(config.modResults.contents);
78
86
  }
79
87
  return config;
80
88
  }
@@ -119,26 +127,55 @@ function addDidFinishLaunchingWithOptionsObjc(contents, iOSEnableMultitaskingCam
119
127
  }
120
128
  return contents;
121
129
  }
122
- function addDidFinishLaunchingWithOptionsRingingSwift(contents) {
130
+ function addDidFinishLaunchingWithOptionsRingingSwift(contents, ringingPushNotifications) {
123
131
  const functionSelector = 'application(_:didFinishLaunchingWithOptions:)';
132
+ const supportsVideoString = ringingPushNotifications.disableVideoIos
133
+ ? 'false'
134
+ : 'true';
135
+ const includesCallsInRecents = ringingPushNotifications.includesCallsInRecentsIos ? 'false' : 'true';
136
+ const setupCallKeep = ` let localizedAppName = Bundle.main.localizedInfoDictionary?["CFBundleDisplayName"] as? String
137
+ let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String
138
+ RNCallKeep.setup([
139
+ "appName": localizedAppName != nil ? localizedAppName! : appName as Any,
140
+ "supportsVideo": ${supportsVideoString},
141
+ "includesCallsInRecents": ${includesCallsInRecents},
142
+ ])`;
143
+ if (!contents.includes('RNCallKeep.setup')) {
144
+ contents = (0, codeMod_1.insertContentsInsideSwiftFunctionBlock)(contents, functionSelector, setupCallKeep, { position: 'head' });
145
+ }
124
146
  // call the setup of voip push notification
125
- const voipSetupMethod = 'StreamVideoReactNative.voipRegistration()';
147
+ const voipSetupMethod = 'RNVoipPushNotificationManager.voipRegistration()';
126
148
  if (!contents.includes(voipSetupMethod)) {
127
149
  contents = (0, codeMod_1.insertContentsInsideSwiftFunctionBlock)(contents, functionSelector, ' ' /* indentation */ + voipSetupMethod, { position: 'head' });
128
150
  }
129
151
  return contents;
130
152
  }
131
- function addDidFinishLaunchingWithOptionsRingingObjc(contents) {
153
+ function addDidFinishLaunchingWithOptionsRingingObjc(contents, ringingPushNotifications) {
132
154
  const functionSelector = 'application:didFinishLaunchingWithOptions:';
155
+ // call the setup RNCallKeep
156
+ const supportsVideoString = ringingPushNotifications.disableVideoIos
157
+ ? '@NO'
158
+ : '@YES';
159
+ const includesCallsInRecents = ringingPushNotifications.includesCallsInRecentsIos ? '@YES' : '@NO';
160
+ const setupCallKeep = `NSString *localizedAppName = [[[NSBundle mainBundle] localizedInfoDictionary] objectForKey:@"CFBundleDisplayName"];
161
+ NSString *appName = [[[NSBundle mainBundle] infoDictionary]objectForKey :@"CFBundleDisplayName"];
162
+ [RNCallKeep setup:@{
163
+ @"appName": localizedAppName != nil ? localizedAppName : appName,
164
+ @"supportsVideo": ${supportsVideoString},
165
+ @"includesCallsInRecents": ${includesCallsInRecents},
166
+ }];`;
167
+ if (!contents.includes('[RNCallKeep setup:@')) {
168
+ contents = (0, codeMod_1.insertContentsInsideObjcFunctionBlock)(contents, functionSelector, setupCallKeep, { position: 'head' });
169
+ }
133
170
  // call the setup of voip push notification
134
- const voipSetupMethod = '[StreamVideoReactNative voipRegistration];';
171
+ const voipSetupMethod = '[RNVoipPushNotificationManager voipRegistration];';
135
172
  if (!contents.includes(voipSetupMethod)) {
136
173
  contents = (0, codeMod_1.insertContentsInsideObjcFunctionBlock)(contents, functionSelector, voipSetupMethod, { position: 'head' });
137
174
  }
138
175
  return contents;
139
176
  }
140
177
  function addDidUpdatePushCredentialsSwift(contents) {
141
- const updatedPushCredentialsMethod = 'StreamVideoReactNative.didUpdate(credentials, forType: type.rawValue)';
178
+ const updatedPushCredentialsMethod = 'RNVoipPushNotificationManager.didUpdate(credentials, forType: type.rawValue)';
142
179
  if (!contents.includes(updatedPushCredentialsMethod)) {
143
180
  const functionSelector = 'pushRegistry(_:didUpdate:for:)';
144
181
  const codeblock = (0, codeMod_1.findSwiftFunctionCodeBlock)(contents, functionSelector);
@@ -160,7 +197,7 @@ function addDidUpdatePushCredentialsSwift(contents) {
160
197
  return contents;
161
198
  }
162
199
  function addDidUpdatePushCredentialsObjc(contents) {
163
- const updatedPushCredentialsMethod = '[StreamVideoReactNative didUpdatePushCredentials:credentials forType: (NSString *) type];';
200
+ const updatedPushCredentialsMethod = '[RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:(NSString *)type];';
164
201
  if (!contents.includes(updatedPushCredentialsMethod)) {
165
202
  const functionSelector = 'pushRegistry:didUpdatePushCredentials:forType:';
166
203
  const codeblock = (0, codeMod_1.findObjcFunctionCodeBlock)(contents, functionSelector);
@@ -177,10 +214,112 @@ function addDidUpdatePushCredentialsObjc(contents) {
177
214
  }
178
215
  return contents;
179
216
  }
217
+ function addAudioSessionMethodsSwift(contents) {
218
+ const audioSessionDidActivateMethod = 'RTCAudioSession.sharedInstance().audioSessionDidActivate(AVAudioSession.sharedInstance())';
219
+ if (!contents.includes(audioSessionDidActivateMethod)) {
220
+ const functionSelector = 'provider(_:didActivate:)';
221
+ if (!contents.includes('didActivateAudioSession')) {
222
+ contents = (0, codeMod_1.insertContentsInsideSwiftClassBlock)(contents, 'class AppDelegate', `
223
+ func provider(_ provider: CXProvider, didActivateAudioSession audioSession: AVAudioSession) {
224
+ ${audioSessionDidActivateMethod}
225
+ }
226
+ `, { position: 'tail' });
227
+ }
228
+ else {
229
+ contents = (0, codeMod_1.insertContentsInsideSwiftFunctionBlock)(contents, functionSelector, audioSessionDidActivateMethod, { position: 'tail' });
230
+ }
231
+ }
232
+ const audioSessionDidDeactivateMethod = 'RTCAudioSession.sharedInstance().audioSessionDidDeactivate(AVAudioSession.sharedInstance())';
233
+ if (!contents.includes(audioSessionDidDeactivateMethod)) {
234
+ const functionSelector = 'provider(_:didDeactivate:)';
235
+ if (!contents.includes('didDeactivateAudioSession')) {
236
+ contents = (0, codeMod_1.insertContentsInsideSwiftClassBlock)(contents, 'class AppDelegate', `
237
+ func provider(_ provider: CXProvider, didDeactivateAudioSession audioSession: AVAudioSession) {
238
+ ${audioSessionDidDeactivateMethod}
239
+ }
240
+ `, { position: 'tail' });
241
+ }
242
+ else {
243
+ contents = (0, codeMod_1.insertContentsInsideSwiftFunctionBlock)(contents, functionSelector, audioSessionDidDeactivateMethod, { position: 'tail' });
244
+ }
245
+ }
246
+ return contents;
247
+ }
248
+ function addAudioSessionMethodsObjc(contents) {
249
+ const audioSessionDidActivateMethod = '[[RTCAudioSession sharedInstance] audioSessionDidActivate:[AVAudioSession sharedInstance]];';
250
+ if (!contents.includes(audioSessionDidActivateMethod)) {
251
+ const functionSelector = 'provider:didActivateAudioSession:audioSession:';
252
+ const codeblock = (0, codeMod_1.findObjcFunctionCodeBlock)(contents, functionSelector);
253
+ if (!codeblock) {
254
+ contents = (0, addNewLinesToAppDelegateObjc_1.default)(contents, [
255
+ '- (void) provider:(CXProvider *) provider didActivateAudioSession:(AVAudioSession *) audioSession {',
256
+ ' ' /* indentation */ + audioSessionDidActivateMethod,
257
+ '}',
258
+ ]);
259
+ }
260
+ else {
261
+ contents = (0, codeMod_1.insertContentsInsideObjcFunctionBlock)(contents, functionSelector, audioSessionDidActivateMethod, { position: 'tail' });
262
+ }
263
+ }
264
+ const audioSessionDidDeactivateMethod = '[[RTCAudioSession sharedInstance] audioSessionDidDeactivate:[AVAudioSession sharedInstance]];';
265
+ if (!contents.includes(audioSessionDidDeactivateMethod)) {
266
+ const functionSelector = 'provider:didDeactivateAudioSession:audioSession:';
267
+ const codeblock = (0, codeMod_1.findObjcFunctionCodeBlock)(contents, functionSelector);
268
+ if (!codeblock) {
269
+ contents = (0, addNewLinesToAppDelegateObjc_1.default)(contents, [
270
+ '- (void) provider:(CXProvider *) provider didDeactivateAudioSession:(AVAudioSession *) audioSession {',
271
+ ' ' /* indentation */ + audioSessionDidDeactivateMethod,
272
+ '}',
273
+ ]);
274
+ }
275
+ else {
276
+ contents = (0, codeMod_1.insertContentsInsideObjcFunctionBlock)(contents, functionSelector, audioSessionDidDeactivateMethod, { position: 'tail' });
277
+ }
278
+ }
279
+ return contents;
280
+ }
180
281
  function addDidReceiveIncomingPushCallbackSwift(contents) {
181
282
  const onIncomingPush = `
182
- StreamVideoReactNative.didReceiveIncomingPush(payload, forType: type.rawValue, completionHandler: completion)`;
183
- if (!contents.includes('StreamVideoReactNative.didReceiveIncomingPush')) {
283
+ guard let stream = payload.dictionaryPayload["stream"] as? [String: Any],
284
+ let createdCallerName = stream["created_by_display_name"] as? String,
285
+ let cid = stream["call_cid"] as? String else {
286
+ completion()
287
+ return
288
+ }
289
+
290
+ // Check if user is busy BEFORE registering the call
291
+ let shouldReject = StreamVideoReactNative.shouldRejectCallWhenBusy()
292
+ let hasAnyActiveCall = StreamVideoReactNative.hasAnyActiveCall()
293
+
294
+ if shouldReject && hasAnyActiveCall {
295
+ // Complete the VoIP notification without showing CallKit UI
296
+ completion()
297
+ return
298
+ }
299
+
300
+ let uuid = UUID().uuidString
301
+ let videoIncluded = stream["video"] as? String
302
+ let hasVideo = videoIncluded == "false" ? false : true
303
+
304
+ StreamVideoReactNative.registerIncomingCall(cid, uuid: uuid)
305
+
306
+ RNVoipPushNotificationManager.addCompletionHandler(uuid, completionHandler: completion)
307
+
308
+ RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue)
309
+
310
+ RNCallKeep.reportNewIncomingCall(uuid,
311
+ handle: createdCallerName,
312
+ handleType: "generic",
313
+ hasVideo: hasVideo,
314
+ localizedCallerName: createdCallerName,
315
+ supportsHolding: false,
316
+ supportsDTMF: false,
317
+ supportsGrouping: false,
318
+ supportsUngrouping: false,
319
+ fromPushKit: true,
320
+ payload: stream,
321
+ withCompletionHandler: nil)`;
322
+ if (!contents.includes('RNVoipPushNotificationManager.didReceiveIncomingPush')) {
184
323
  const functionSelector = 'pushRegistry(_:didReceiveIncomingPushWith:for:completion:)';
185
324
  const codeblock = (0, codeMod_1.findSwiftFunctionCodeBlock)(contents, functionSelector);
186
325
  if (!codeblock) {
@@ -203,10 +342,49 @@ function addDidReceiveIncomingPushCallbackSwift(contents) {
203
342
  }
204
343
  function addDidReceiveIncomingPushCallbackObjc(contents) {
205
344
  const onIncomingPush = `
206
- // process the payload and display the incoming call notification
207
- [StreamVideoReactNative didReceiveIncomingPush:payload forType: (NSString *)type completionHandler:completion];
345
+ // process the payload and store it in the native module's cache
346
+ NSDictionary *stream = payload.dictionaryPayload[@"stream"];
347
+ NSString *uuid = [[NSUUID UUID] UUIDString];
348
+ NSString *createdCallerName = stream[@"created_by_display_name"];
349
+ NSString *cid = stream[@"call_cid"];
350
+
351
+ // Check if user is busy BEFORE registering the call
352
+ BOOL shouldReject = [StreamVideoReactNative shouldRejectCallWhenBusy];
353
+ BOOL hasAnyActiveCall = [StreamVideoReactNative hasAnyActiveCall];
354
+
355
+ if (shouldReject && hasAnyActiveCall) {
356
+ // Complete the VoIP notification without showing CallKit UI
357
+ completion();
358
+ return;
359
+ }
360
+
361
+ NSString *videoIncluded = stream[@"video"];
362
+ BOOL hasVideo = [videoIncluded isEqualToString:@"false"] ? NO : YES;
363
+
364
+ // store the call cid and uuid in the native module's cache
365
+ [StreamVideoReactNative registerIncomingCall:cid uuid:uuid];
366
+
367
+ // set the completion handler - this one is called by the JS SDK
368
+ [RNVoipPushNotificationManager addCompletionHandler:uuid completionHandler:completion];
369
+
370
+ // send event to JS - the JS SDK will handle the rest and call the 'completionHandler'
371
+ [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];
372
+
373
+ // display the incoming call notification
374
+ [RNCallKeep reportNewIncomingCall: uuid
375
+ handle: createdCallerName
376
+ handleType: @"generic"
377
+ hasVideo: hasVideo
378
+ localizedCallerName: createdCallerName
379
+ supportsHolding: NO
380
+ supportsDTMF: NO
381
+ supportsGrouping: NO
382
+ supportsUngrouping: NO
383
+ fromPushKit: YES
384
+ payload: stream
385
+ withCompletionHandler: nil];
208
386
  `;
209
- if (!contents.includes('[StreamVideoReactNative didReceiveIncomingPush')) {
387
+ if (!contents.includes('[RNVoipPushNotificationManager didReceiveIncomingPushWithPayload')) {
210
388
  const functionSelector = 'pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:';
211
389
  const codeblock = (0, codeMod_1.findObjcFunctionCodeBlock)(contents, functionSelector);
212
390
  if (!codeblock) {
@@ -23,7 +23,7 @@ const withStreamVideoReactNativeSDKMainActivity = (configuration, props) => {
23
23
  if (props?.enableScreenshare) {
24
24
  config.modResults.contents = addInsideOnCreateScreenshare(config.modResults.contents, isMainActivityJava);
25
25
  }
26
- if (props?.ringing) {
26
+ if (props?.ringingPushNotifications?.showWhenLockedAndroid) {
27
27
  config.modResults.contents = addInsideOnCreateLockscreen(config.modResults.contents, isMainActivityJava);
28
28
  }
29
29
  return config;
@@ -12,7 +12,7 @@ const withStreamVideoReactNativeSDKiOSInfoPList = (configuration, props) => {
12
12
  }
13
13
  }
14
14
  addBackgroundMode('audio');
15
- if (props?.ringing) {
15
+ if (props?.ringingPushNotifications) {
16
16
  addBackgroundMode('voip');
17
17
  addBackgroundMode('fetch');
18
18
  addBackgroundMode('processing');
@@ -20,7 +20,8 @@ const withStreamVideoReactNativeSDKiOSInfoPList = (configuration, props) => {
20
20
  '$(PRODUCT_BUNDLE_IDENTIFIER)',
21
21
  ];
22
22
  }
23
- if (props?.enableNonRingingPushNotifications || props?.ringing) {
23
+ if (props?.enableNonRingingPushNotifications ||
24
+ props?.ringingPushNotifications) {
24
25
  addBackgroundMode('remote-notification');
25
26
  }
26
27
  return config;
@@ -0,0 +1,273 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import UIKit
6
+
7
+ /// A view that displays an avatar placeholder when video is disabled in PiP mode.
8
+ /// Shows either a loaded image from URL, initials, or a default person icon.
9
+ final class PictureInPictureAvatarView: UIView {
10
+
11
+ // MARK: - Properties
12
+
13
+ /// The participant's name, used to generate initials
14
+ var participantName: String? {
15
+ didSet {
16
+ PictureInPictureLogger.log("AvatarView.participantName didSet: '\(participantName ?? "nil")'")
17
+ updateInitials()
18
+ }
19
+ }
20
+
21
+ /// The URL string for the participant's profile image
22
+ var imageURL: String? {
23
+ didSet {
24
+ loadImage()
25
+ }
26
+ }
27
+
28
+ /// Whether video is enabled - when true, the avatar should be hidden (alpha = 0)
29
+ /// Note: We use alpha instead of isHidden to match upstream SwiftUI behavior.
30
+ /// Using isHidden can cause layout issues because iOS may skip layoutSubviews for hidden views.
31
+ var isVideoEnabled: Bool = true {
32
+ didSet {
33
+ updateVisibility()
34
+ // When becoming visible (video disabled), refresh content to ensure initials are shown
35
+ // This is needed when the same avatarView instance is reused across PiP sessions
36
+ if !isVideoEnabled {
37
+ PictureInPictureLogger.log("AvatarView isVideoEnabled=false, refreshing content")
38
+ updateInitials()
39
+ }
40
+ }
41
+ }
42
+
43
+ // MARK: - Private Properties
44
+
45
+ private let containerView: UIView = {
46
+ let view = UIView()
47
+ view.translatesAutoresizingMaskIntoConstraints = false
48
+ view.backgroundColor = UIColor(red: 0.12, green: 0.13, blue: 0.15, alpha: 1.0) // Dark background
49
+ return view
50
+ }()
51
+
52
+ private let avatarContainerView: UIView = {
53
+ let view = UIView()
54
+ view.translatesAutoresizingMaskIntoConstraints = false
55
+ view.backgroundColor = UIColor(red: 0.0, green: 0.47, blue: 1.0, alpha: 1.0) // Stream blue
56
+ view.clipsToBounds = true
57
+ return view
58
+ }()
59
+
60
+ private let initialsLabel: UILabel = {
61
+ let label = UILabel()
62
+ label.translatesAutoresizingMaskIntoConstraints = false
63
+ label.textColor = .white
64
+ label.textAlignment = .center
65
+ label.font = UIFont.systemFont(ofSize: 32, weight: .semibold)
66
+ label.adjustsFontSizeToFitWidth = true
67
+ label.minimumScaleFactor = 0.5
68
+ return label
69
+ }()
70
+
71
+ private let imageView: UIImageView = {
72
+ let imageView = UIImageView()
73
+ imageView.translatesAutoresizingMaskIntoConstraints = false
74
+ imageView.contentMode = .scaleAspectFill
75
+ imageView.clipsToBounds = true
76
+ imageView.isHidden = true
77
+ return imageView
78
+ }()
79
+
80
+ private let placeholderImageView: UIImageView = {
81
+ let imageView = UIImageView()
82
+ imageView.translatesAutoresizingMaskIntoConstraints = false
83
+ imageView.contentMode = .scaleAspectFit
84
+ imageView.tintColor = .white
85
+ // Use SF Symbol for person icon
86
+ if let personImage = UIImage(systemName: "person.fill") {
87
+ imageView.image = personImage
88
+ }
89
+ imageView.isHidden = true
90
+ return imageView
91
+ }()
92
+
93
+ private var currentImageLoadTask: URLSessionDataTask?
94
+ private var avatarSizeConstraints: [NSLayoutConstraint] = []
95
+
96
+ // MARK: - Lifecycle
97
+
98
+ override init(frame: CGRect) {
99
+ super.init(frame: frame)
100
+ setUp()
101
+ }
102
+
103
+ required init?(coder: NSCoder) {
104
+ fatalError("init(coder:) has not been implemented")
105
+ }
106
+
107
+ override func layoutSubviews() {
108
+ super.layoutSubviews()
109
+ PictureInPictureLogger.log("AvatarView layoutSubviews: bounds=\(bounds), isHidden=\(isHidden)")
110
+ updateAvatarSize()
111
+ }
112
+
113
+ // MARK: - Private Helpers
114
+
115
+ private func setUp() {
116
+ addSubview(containerView)
117
+ containerView.addSubview(avatarContainerView)
118
+ avatarContainerView.addSubview(initialsLabel)
119
+ avatarContainerView.addSubview(imageView)
120
+ avatarContainerView.addSubview(placeholderImageView)
121
+
122
+ NSLayoutConstraint.activate([
123
+ containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
124
+ containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
125
+ containerView.topAnchor.constraint(equalTo: topAnchor),
126
+ containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
127
+
128
+ avatarContainerView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
129
+ avatarContainerView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
130
+
131
+ initialsLabel.leadingAnchor.constraint(equalTo: avatarContainerView.leadingAnchor, constant: 4),
132
+ initialsLabel.trailingAnchor.constraint(equalTo: avatarContainerView.trailingAnchor, constant: -4),
133
+ initialsLabel.topAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: 4),
134
+ initialsLabel.bottomAnchor.constraint(equalTo: avatarContainerView.bottomAnchor, constant: -4),
135
+
136
+ imageView.leadingAnchor.constraint(equalTo: avatarContainerView.leadingAnchor),
137
+ imageView.trailingAnchor.constraint(equalTo: avatarContainerView.trailingAnchor),
138
+ imageView.topAnchor.constraint(equalTo: avatarContainerView.topAnchor),
139
+ imageView.bottomAnchor.constraint(equalTo: avatarContainerView.bottomAnchor),
140
+
141
+ placeholderImageView.centerXAnchor.constraint(equalTo: avatarContainerView.centerXAnchor),
142
+ placeholderImageView.centerYAnchor.constraint(equalTo: avatarContainerView.centerYAnchor),
143
+ placeholderImageView.widthAnchor.constraint(equalTo: avatarContainerView.widthAnchor, multiplier: 0.5),
144
+ placeholderImageView.heightAnchor.constraint(equalTo: avatarContainerView.heightAnchor, multiplier: 0.5)
145
+ ])
146
+
147
+ updateAvatarSize()
148
+ updateVisibility()
149
+ // Ensure initial content state is correct (show placeholder when no name/image)
150
+ updateInitials()
151
+ }
152
+
153
+ private func updateAvatarSize() {
154
+ // Remove old constraints
155
+ NSLayoutConstraint.deactivate(avatarSizeConstraints)
156
+
157
+ // Avatar size should be about 40% of the smaller dimension
158
+ let minDimension = min(bounds.width, bounds.height)
159
+ let avatarSize = max(minDimension * 0.4, 60) // Minimum 60pt
160
+
161
+ PictureInPictureLogger.log("AvatarView updateAvatarSize: bounds=\(bounds), minDimension=\(minDimension), avatarSize=\(avatarSize)")
162
+
163
+ avatarSizeConstraints = [
164
+ avatarContainerView.widthAnchor.constraint(equalToConstant: avatarSize),
165
+ avatarContainerView.heightAnchor.constraint(equalToConstant: avatarSize)
166
+ ]
167
+ NSLayoutConstraint.activate(avatarSizeConstraints)
168
+
169
+ // Force immediate layout to apply the new constraints
170
+ // This is needed because constraints set during layoutSubviews
171
+ // won't be resolved until the next layout pass otherwise
172
+ containerView.setNeedsLayout()
173
+ containerView.layoutIfNeeded()
174
+
175
+ // Update corner radius after layout is complete
176
+ avatarContainerView.layer.cornerRadius = avatarContainerView.bounds.width / 2
177
+
178
+ PictureInPictureLogger.log("AvatarView updateAvatarSize FINAL: avatarContainer.frame=\(avatarContainerView.frame)")
179
+ }
180
+
181
+ private func updateVisibility() {
182
+ // Hide avatar when video is enabled using alpha (not isHidden)
183
+ // Using alpha instead of isHidden ensures layoutSubviews is always called,
184
+ // which is critical for proper constraint-based layout. This matches
185
+ // upstream SwiftUI's opacity-based visibility switching.
186
+ let newAlpha: CGFloat = isVideoEnabled ? 0 : 1
187
+ PictureInPictureLogger.log("AvatarView updateVisibility: isVideoEnabled=\(isVideoEnabled), setting alpha=\(newAlpha)")
188
+ alpha = newAlpha
189
+
190
+ // Force layout update when becoming visible to ensure proper sizing
191
+ if !isVideoEnabled {
192
+ PictureInPictureLogger.log("AvatarView updateVisibility: becoming visible, forcing layout")
193
+ setNeedsLayout()
194
+ layoutIfNeeded()
195
+ }
196
+ }
197
+
198
+ private func updateInitials() {
199
+ guard let name = participantName, !name.isEmpty else {
200
+ PictureInPictureLogger.log("AvatarView updateInitials: no name, showing placeholder. avatarContainer.frame=\(avatarContainerView.frame)")
201
+ initialsLabel.text = nil
202
+ initialsLabel.isHidden = true
203
+ // Show placeholder when there's no image loaded
204
+ placeholderImageView.isHidden = imageView.image != nil
205
+ return
206
+ }
207
+
208
+ let initials = generateInitials(from: name)
209
+ PictureInPictureLogger.log("AvatarView updateInitials: name=\(name), initials=\(initials), imageView.image=\(imageView.image != nil ? "loaded" : "nil"), avatarContainer.frame=\(avatarContainerView.frame)")
210
+ initialsLabel.text = initials
211
+ initialsLabel.isHidden = imageView.image != nil
212
+ placeholderImageView.isHidden = true
213
+ }
214
+
215
+ private func generateInitials(from name: String) -> String {
216
+ let components = name.split(separator: " ")
217
+ if components.count >= 2 {
218
+ let first = components[0].prefix(1)
219
+ let last = components[1].prefix(1)
220
+ return "\(first)\(last)".uppercased()
221
+ } else if let first = components.first {
222
+ return String(first.prefix(2)).uppercased()
223
+ }
224
+ return ""
225
+ }
226
+
227
+ private func loadImage() {
228
+ // Cancel any existing task
229
+ currentImageLoadTask?.cancel()
230
+ currentImageLoadTask = nil
231
+
232
+ guard let urlString = imageURL, !urlString.isEmpty, let url = URL(string: urlString) else {
233
+ imageView.image = nil
234
+ imageView.isHidden = true
235
+ updateInitials()
236
+ return
237
+ }
238
+
239
+ let requestURLString = urlString
240
+
241
+ // Load image asynchronously
242
+ var requestTask: URLSessionDataTask?
243
+ let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
244
+ DispatchQueue.main.async { [weak self] in
245
+ guard let self = self else { return }
246
+ guard let requestTask else { return }
247
+ guard self.currentImageLoadTask === requestTask else { return }
248
+ defer { self.currentImageLoadTask = nil }
249
+
250
+ // Ignore stale/cancelled responses so only the latest request can mutate UI.
251
+ if let nsError = error as NSError?, nsError.code == NSURLErrorCancelled {
252
+ return
253
+ }
254
+ guard self.imageURL == requestURLString else { return }
255
+
256
+ guard error == nil, let data = data, let image = UIImage(data: data) else {
257
+ self.imageView.image = nil
258
+ self.imageView.isHidden = true
259
+ self.updateInitials()
260
+ return
261
+ }
262
+
263
+ self.imageView.image = image
264
+ self.imageView.isHidden = false
265
+ self.initialsLabel.isHidden = true
266
+ self.placeholderImageView.isHidden = true
267
+ }
268
+ }
269
+ requestTask = task
270
+ currentImageLoadTask = task
271
+ task.resume()
272
+ }
273
+ }