@developer_tribe/react-native-comnyx 0.13.11 → 0.14.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 (121) hide show
  1. package/android/src/main/AndroidManifest.xml +15 -0
  2. package/android/src/main/AndroidManifestNew.xml +4 -0
  3. package/android/src/main/java/com/comnyx/ComnyxMediaPickerModule.kt +301 -0
  4. package/android/src/main/java/com/comnyx/ComnyxPackage.kt +1 -1
  5. package/android/src/main/java/com/comnyx/VideoPlayerActivity.kt +91 -0
  6. package/android/src/main/java/com/comnyx/src/messaging/notifications/NotificationsService.kt +12 -1
  7. package/ios/ComnyxMediaPicker.m +23 -0
  8. package/ios/ComnyxMediaPicker.swift +377 -0
  9. package/lib/commonjs/NativeComnyxMediaPicker.js +62 -0
  10. package/lib/commonjs/NativeComnyxMediaPicker.js.map +1 -0
  11. package/lib/commonjs/api/index.js +19 -0
  12. package/lib/commonjs/api/index.js.map +1 -1
  13. package/lib/commonjs/api/media.js +76 -0
  14. package/lib/commonjs/api/media.js.map +1 -0
  15. package/lib/commonjs/assets/attachment-01.png +0 -0
  16. package/lib/commonjs/assets/x-circle.png +0 -0
  17. package/lib/commonjs/components/ChatList.js +48 -22
  18. package/lib/commonjs/components/ChatList.js.map +1 -1
  19. package/lib/commonjs/components/LinkifyText.js +5 -1
  20. package/lib/commonjs/components/LinkifyText.js.map +1 -1
  21. package/lib/commonjs/components/MediaMessageItem.js +333 -0
  22. package/lib/commonjs/components/MediaMessageItem.js.map +1 -0
  23. package/lib/commonjs/components/MediaPickerButton.js +47 -0
  24. package/lib/commonjs/components/MediaPickerButton.js.map +1 -0
  25. package/lib/commonjs/components/MediaViewerModal.js +157 -0
  26. package/lib/commonjs/components/MediaViewerModal.js.map +1 -0
  27. package/lib/commonjs/components/MessageInput.js +344 -73
  28. package/lib/commonjs/components/MessageInput.js.map +1 -1
  29. package/lib/commonjs/components/MessageItem.js +17 -7
  30. package/lib/commonjs/components/MessageItem.js.map +1 -1
  31. package/lib/commonjs/constants/translations.js +203 -29
  32. package/lib/commonjs/constants/translations.js.map +1 -1
  33. package/lib/commonjs/data/fake/media.js +105 -0
  34. package/lib/commonjs/data/fake/media.js.map +1 -0
  35. package/lib/commonjs/notifications/initializeNotifications.js +1 -0
  36. package/lib/commonjs/notifications/initializeNotifications.js.map +1 -1
  37. package/lib/commonjs/types/MediaTypes.js +2 -0
  38. package/lib/commonjs/types/MediaTypes.js.map +1 -0
  39. package/lib/commonjs/version.js +1 -1
  40. package/lib/commonjs/version.js.map +1 -1
  41. package/lib/module/NativeComnyxMediaPicker.js +54 -0
  42. package/lib/module/NativeComnyxMediaPicker.js.map +1 -0
  43. package/lib/module/api/index.js +1 -0
  44. package/lib/module/api/index.js.map +1 -1
  45. package/lib/module/api/media.js +70 -0
  46. package/lib/module/api/media.js.map +1 -0
  47. package/lib/module/assets/attachment-01.png +0 -0
  48. package/lib/module/assets/x-circle.png +0 -0
  49. package/lib/module/components/ChatList.js +48 -22
  50. package/lib/module/components/ChatList.js.map +1 -1
  51. package/lib/module/components/LinkifyText.js +5 -1
  52. package/lib/module/components/LinkifyText.js.map +1 -1
  53. package/lib/module/components/MediaMessageItem.js +330 -0
  54. package/lib/module/components/MediaMessageItem.js.map +1 -0
  55. package/lib/module/components/MediaPickerButton.js +43 -0
  56. package/lib/module/components/MediaPickerButton.js.map +1 -0
  57. package/lib/module/components/MediaViewerModal.js +153 -0
  58. package/lib/module/components/MediaViewerModal.js.map +1 -0
  59. package/lib/module/components/MessageInput.js +347 -75
  60. package/lib/module/components/MessageInput.js.map +1 -1
  61. package/lib/module/components/MessageItem.js +17 -7
  62. package/lib/module/components/MessageItem.js.map +1 -1
  63. package/lib/module/constants/translations.js +203 -29
  64. package/lib/module/constants/translations.js.map +1 -1
  65. package/lib/module/data/fake/media.js +99 -0
  66. package/lib/module/data/fake/media.js.map +1 -0
  67. package/lib/module/notifications/initializeNotifications.js +1 -0
  68. package/lib/module/notifications/initializeNotifications.js.map +1 -1
  69. package/lib/module/types/MediaTypes.js +2 -0
  70. package/lib/module/types/MediaTypes.js.map +1 -0
  71. package/lib/module/version.js +1 -1
  72. package/lib/module/version.js.map +1 -1
  73. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts +7 -0
  74. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts.map +1 -0
  75. package/lib/typescript/src/api/index.d.ts +1 -0
  76. package/lib/typescript/src/api/index.d.ts.map +1 -1
  77. package/lib/typescript/src/api/media.d.ts +7 -0
  78. package/lib/typescript/src/api/media.d.ts.map +1 -0
  79. package/lib/typescript/src/components/ChatList.d.ts.map +1 -1
  80. package/lib/typescript/src/components/LinkifyText.d.ts.map +1 -1
  81. package/lib/typescript/src/components/MediaMessageItem.d.ts +6 -0
  82. package/lib/typescript/src/components/MediaMessageItem.d.ts.map +1 -0
  83. package/lib/typescript/src/components/MediaPickerButton.d.ts +5 -0
  84. package/lib/typescript/src/components/MediaPickerButton.d.ts.map +1 -0
  85. package/lib/typescript/src/components/MediaViewerModal.d.ts +8 -0
  86. package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -0
  87. package/lib/typescript/src/components/MessageInput.d.ts.map +1 -1
  88. package/lib/typescript/src/components/MessageItem.d.ts.map +1 -1
  89. package/lib/typescript/src/constants/translations.d.ts.map +1 -1
  90. package/lib/typescript/src/data/fake/media.d.ts +6 -0
  91. package/lib/typescript/src/data/fake/media.d.ts.map +1 -0
  92. package/lib/typescript/src/notifications/initializeNotifications.d.ts.map +1 -1
  93. package/lib/typescript/src/types/Conversation.d.ts +19 -0
  94. package/lib/typescript/src/types/Conversation.d.ts.map +1 -1
  95. package/lib/typescript/src/types/LocalizationKeys.d.ts +6 -0
  96. package/lib/typescript/src/types/LocalizationKeys.d.ts.map +1 -1
  97. package/lib/typescript/src/types/MediaTypes.d.ts +26 -0
  98. package/lib/typescript/src/types/MediaTypes.d.ts.map +1 -0
  99. package/lib/typescript/src/version.d.ts +1 -1
  100. package/lib/typescript/src/version.d.ts.map +1 -1
  101. package/package.json +1 -1
  102. package/src/NativeComnyxMediaPicker.ts +62 -0
  103. package/src/api/index.ts +1 -0
  104. package/src/api/media.ts +116 -0
  105. package/src/assets/attachment-01.png +0 -0
  106. package/src/assets/x-circle.png +0 -0
  107. package/src/components/ChatList.tsx +81 -24
  108. package/src/components/CustomerForm.tsx +1 -1
  109. package/src/components/LinkifyText.tsx +3 -2
  110. package/src/components/MediaMessageItem.tsx +390 -0
  111. package/src/components/MediaPickerButton.tsx +48 -0
  112. package/src/components/MediaViewerModal.tsx +161 -0
  113. package/src/components/MessageInput.tsx +396 -84
  114. package/src/components/MessageItem.tsx +19 -4
  115. package/src/constants/translations.ts +174 -0
  116. package/src/data/fake/media.ts +110 -0
  117. package/src/notifications/initializeNotifications.ts +1 -0
  118. package/src/types/Conversation.ts +20 -0
  119. package/src/types/LocalizationKeys.ts +6 -0
  120. package/src/types/MediaTypes.ts +27 -0
  121. package/src/version.ts +1 -1
@@ -1,3 +1,18 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
2
  package="com.comnyx">
3
+
4
+ <application>
5
+ <service
6
+ android:name="com.comnyx.messaging.ComnyxFirebaseMessagingService"
7
+ android:priority="-1500"
8
+ android:exported="true">
9
+ <intent-filter>
10
+ <action android:name="com.google.firebase.MESSAGING_EVENT" />
11
+ </intent-filter>
12
+ </service>
13
+ <activity
14
+ android:name="com.comnyx.VideoPlayerActivity"
15
+ android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
16
+ android:exported="false" />
17
+ </application>
3
18
  </manifest>
@@ -8,5 +8,9 @@
8
8
  <action android:name="com.google.firebase.MESSAGING_EVENT" />
9
9
  </intent-filter>
10
10
  </service>
11
+ <activity
12
+ android:name="com.comnyx.VideoPlayerActivity"
13
+ android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
14
+ android:exported="false" />
11
15
  </application>
12
16
  </manifest>
@@ -0,0 +1,301 @@
1
+ package com.comnyx
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.graphics.Bitmap
6
+ import android.graphics.BitmapFactory
7
+ import android.net.Uri
8
+ import android.provider.OpenableColumns
9
+ import android.webkit.MimeTypeMap
10
+ import com.facebook.react.bridge.*
11
+ import java.io.File
12
+ import java.io.FileOutputStream
13
+ import java.io.InputStream
14
+ import android.media.MediaMetadataRetriever
15
+
16
+ class ComnyxMediaPickerModule(reactContext: ReactApplicationContext) :
17
+ ReactContextBaseJavaModule(reactContext), ActivityEventListener {
18
+
19
+ companion object {
20
+ const val NAME = "ComnyxMediaPicker"
21
+ private const val REQUEST_MEDIA = 2003
22
+ private const val COMPRESS_THRESHOLD = 500 * 1024 // 500KB
23
+ private const val COMPRESS_QUALITY = 80
24
+ }
25
+
26
+ private var promise: Promise? = null
27
+
28
+ init {
29
+ reactContext.addActivityEventListener(this)
30
+ }
31
+
32
+ override fun getName(): String = NAME
33
+
34
+
35
+
36
+ // MARK: - Pick Media (both images and videos)
37
+
38
+ @ReactMethod
39
+ fun pickMedia(promise: Promise) {
40
+ this.promise = promise
41
+ val activity = reactApplicationContext.currentActivity
42
+ if (activity == null) {
43
+ promise.reject("NO_ACTIVITY", "Activity is not available")
44
+ return
45
+ }
46
+ val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
47
+ type = "*/*"
48
+ putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
49
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
50
+ addCategory(Intent.CATEGORY_OPENABLE)
51
+ }
52
+ activity.startActivityForResult(intent, REQUEST_MEDIA)
53
+ }
54
+
55
+ // MARK: - Generate Thumbnail from Video URL
56
+
57
+ @ReactMethod
58
+ fun generateThumbnail(videoUrl: String, promise: Promise) {
59
+ android.util.Log.d("ComnyxMedia", "generateThumbnail called with: $videoUrl")
60
+ Thread {
61
+ try {
62
+ val retriever = MediaMetadataRetriever()
63
+ if (videoUrl.startsWith("http://") || videoUrl.startsWith("https://")) {
64
+ android.util.Log.d("ComnyxMedia", "Setting remote data source")
65
+ retriever.setDataSource(videoUrl, HashMap<String, String>())
66
+ } else {
67
+ android.util.Log.d("ComnyxMedia", "Setting local data source")
68
+ retriever.setDataSource(reactApplicationContext, Uri.parse(videoUrl))
69
+ }
70
+ val bitmap = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
71
+ retriever.release()
72
+
73
+ if (bitmap != null) {
74
+ android.util.Log.d("ComnyxMedia", "Thumbnail bitmap generated: ${bitmap.width}x${bitmap.height}")
75
+ val thumbFile = File(reactApplicationContext.cacheDir, "thumb_${System.currentTimeMillis()}.jpg")
76
+ FileOutputStream(thumbFile).use { fos ->
77
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos)
78
+ }
79
+ bitmap.recycle()
80
+ promise.resolve("file://" + thumbFile.absolutePath)
81
+ } else {
82
+ android.util.Log.d("ComnyxMedia", "Thumbnail bitmap is null")
83
+ promise.resolve(null)
84
+ }
85
+ } catch (e: Exception) {
86
+ android.util.Log.e("ComnyxMedia", "generateThumbnail error: ${e.message}", e)
87
+ promise.resolve(null)
88
+ }
89
+ }.start()
90
+ }
91
+
92
+ // MARK: - Open Video
93
+
94
+ @ReactMethod
95
+ fun openVideo(uri: String, promise: Promise) {
96
+ try {
97
+ val activity = reactApplicationContext.currentActivity
98
+ if (activity == null) {
99
+ promise.reject("NO_ACTIVITY", "Activity is not available")
100
+ return
101
+ }
102
+
103
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
104
+ // Remote URL — use system player for faster streaming
105
+ val intent = Intent(Intent.ACTION_VIEW).apply {
106
+ setDataAndType(Uri.parse(uri), "video/*")
107
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
108
+ }
109
+ activity.startActivity(intent)
110
+ promise.resolve(null)
111
+ } else {
112
+ // Local file — use custom VideoPlayerActivity
113
+ val intent = Intent(activity, VideoPlayerActivity::class.java).apply {
114
+ putExtra("video_uri", uri)
115
+ }
116
+ activity.startActivity(intent)
117
+ promise.resolve(null)
118
+ }
119
+ } catch (e: Exception) {
120
+ promise.reject("OPEN_ERROR", e.message, e)
121
+ }
122
+ }
123
+
124
+ // MARK: - Activity Result
125
+
126
+ override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
127
+ if (requestCode != REQUEST_MEDIA) return
128
+
129
+ if (resultCode != Activity.RESULT_OK || data == null) {
130
+ promise?.resolve(null)
131
+ promise = null
132
+ return
133
+ }
134
+
135
+ // Collect all selected URIs
136
+ val uris = mutableListOf<Uri>()
137
+ val clipData = data.clipData
138
+ if (clipData != null) {
139
+ for (i in 0 until clipData.itemCount) {
140
+ uris.add(clipData.getItemAt(i).uri)
141
+ }
142
+ } else if (data.data != null) {
143
+ uris.add(data.data!!)
144
+ }
145
+
146
+ if (uris.isEmpty()) {
147
+ promise?.resolve(null)
148
+ promise = null
149
+ return
150
+ }
151
+
152
+ // Capture and clear promise to prevent double-resolve races
153
+ val currentPromise = promise
154
+ promise = null
155
+
156
+ // Move all heavy I/O to a background thread to prevent ANR
157
+ Thread {
158
+ try {
159
+ val context = reactApplicationContext
160
+ val contentResolver = context.contentResolver
161
+ val resultsArray = Arguments.createArray()
162
+
163
+ for (uri in uris) {
164
+ val mimeTypeStr = contentResolver.getType(uri) ?: ""
165
+ val isImage = mimeTypeStr.startsWith("image")
166
+ val mimeType = contentResolver.getType(uri) ?: if (isImage) "image/jpeg" else "video/mp4"
167
+
168
+ // Get file name
169
+ var fileName = "file_${System.currentTimeMillis()}"
170
+ val cursor = contentResolver.query(uri, null, null, null, null)
171
+ cursor?.use {
172
+ if (it.moveToFirst()) {
173
+ val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
174
+ if (nameIndex >= 0) {
175
+ fileName = it.getString(nameIndex) ?: fileName
176
+ }
177
+ }
178
+ }
179
+
180
+ // Copy to temp file
181
+ val tempFile = File(context.cacheDir, "${System.currentTimeMillis()}_$fileName")
182
+ contentResolver.openInputStream(uri)?.use { inputStream ->
183
+ FileOutputStream(tempFile).use { outputStream ->
184
+ inputStream.copyTo(outputStream)
185
+ }
186
+ }
187
+
188
+ var finalFile = tempFile
189
+ var finalMimeType = mimeType
190
+ var finalFileSize = tempFile.length()
191
+
192
+ // Compress image if >500KB
193
+ if (isImage && finalFileSize > COMPRESS_THRESHOLD) {
194
+ val compressed = compressImage(tempFile)
195
+ if (compressed != null) {
196
+ tempFile.delete()
197
+ finalFile = compressed
198
+ finalMimeType = "image/jpeg"
199
+ finalFileSize = compressed.length()
200
+ }
201
+ }
202
+
203
+ val result = Arguments.createMap().apply {
204
+ putString("uri", Uri.fromFile(finalFile).toString())
205
+ putString("type", if (isImage) "image" else "video")
206
+ putString("fileName", finalFile.name)
207
+ putDouble("fileSize", finalFileSize.toDouble())
208
+ putString("mimeType", finalMimeType)
209
+ if (!isImage) {
210
+ val thumbnailUri = generateVideoThumbnail(finalFile)
211
+ if (thumbnailUri != null) {
212
+ putString("thumbnailUri", thumbnailUri)
213
+ }
214
+ }
215
+ }
216
+ resultsArray.pushMap(result)
217
+ }
218
+
219
+ currentPromise?.resolve(resultsArray)
220
+ } catch (e: Exception) {
221
+ currentPromise?.reject("PICK_ERROR", e.message, e)
222
+ }
223
+ }.start()
224
+ }
225
+
226
+ override fun onNewIntent(intent: Intent) {
227
+ // Not needed
228
+ }
229
+
230
+ // MARK: - Image Compression
231
+
232
+ private fun compressImage(file: File): File? {
233
+ return try {
234
+ val bitmap = BitmapFactory.decodeFile(file.absolutePath) ?: return null
235
+ val compressedFile = File(reactApplicationContext.cacheDir, "compressed_${System.currentTimeMillis()}.jpg")
236
+ FileOutputStream(compressedFile).use { outputStream ->
237
+ bitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESS_QUALITY, outputStream)
238
+ }
239
+ bitmap.recycle()
240
+ compressedFile
241
+ } catch (e: Exception) {
242
+ null
243
+ }
244
+ }
245
+
246
+ private fun generateVideoThumbnail(videoFile: File): String? {
247
+ return try {
248
+ val retriever = MediaMetadataRetriever()
249
+ retriever.setDataSource(videoFile.absolutePath)
250
+ val bitmap = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
251
+ retriever.release()
252
+ if (bitmap != null) {
253
+ val thumbFile = File(reactApplicationContext.cacheDir, "thumb_${System.currentTimeMillis()}.jpg")
254
+ FileOutputStream(thumbFile).use { outputStream ->
255
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 70, outputStream)
256
+ }
257
+ bitmap.recycle()
258
+ Uri.fromFile(thumbFile).toString()
259
+ } else null
260
+ } catch (e: Exception) {
261
+ null
262
+ }
263
+ }
264
+
265
+ @ReactMethod
266
+ fun deleteTempFile(uri: String, promise: Promise) {
267
+ Thread {
268
+ try {
269
+ val path = uri.removePrefix("file://")
270
+ val file = File(path)
271
+ if (file.exists()) {
272
+ file.delete()
273
+ }
274
+ promise.resolve(null)
275
+ } catch (e: Exception) {
276
+ promise.resolve(null)
277
+ }
278
+ }.start()
279
+ }
280
+
281
+ @ReactMethod
282
+ fun cleanupTempFiles(promise: Promise) {
283
+ Thread {
284
+ try {
285
+ val cacheDir = reactApplicationContext.cacheDir
286
+ val files = cacheDir.listFiles() ?: emptyArray()
287
+ for (file in files) {
288
+ val name = file.name.lowercase()
289
+ if (name.startsWith("thumb_") || name.startsWith("compressed_") ||
290
+ (name.contains("_") && (name.endsWith(".jpg") || name.endsWith(".jpeg") ||
291
+ name.endsWith(".png") || name.endsWith(".mp4") || name.endsWith(".mov")))) {
292
+ file.delete()
293
+ }
294
+ }
295
+ promise.resolve(null)
296
+ } catch (e: Exception) {
297
+ promise.resolve(null)
298
+ }
299
+ }.start()
300
+ }
301
+ }
@@ -7,7 +7,7 @@ import com.facebook.react.uimanager.ViewManager
7
7
 
8
8
  class ComnyxPackage : ReactPackage {
9
9
  override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
- return listOf(ComnyxModule(reactContext))
10
+ return listOf(ComnyxModule(reactContext), ComnyxMediaPickerModule(reactContext))
11
11
  }
12
12
 
13
13
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
@@ -0,0 +1,91 @@
1
+ package com.comnyx
2
+
3
+ import android.net.Uri
4
+ import android.os.Bundle
5
+ import android.view.Gravity
6
+ import android.view.Window
7
+ import android.view.WindowManager
8
+ import android.widget.FrameLayout
9
+ import android.widget.ImageButton
10
+ import android.widget.ProgressBar
11
+ import android.widget.VideoView
12
+ import android.app.Activity as AndroidActivity
13
+ import android.widget.MediaController
14
+
15
+ class VideoPlayerActivity : AndroidActivity() {
16
+ override fun onCreate(savedInstanceState: Bundle?) {
17
+ super.onCreate(savedInstanceState)
18
+
19
+ // Full screen
20
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
21
+ window.setFlags(
22
+ WindowManager.LayoutParams.FLAG_FULLSCREEN,
23
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
24
+ )
25
+
26
+ val container = FrameLayout(this)
27
+ container.setBackgroundColor(android.graphics.Color.BLACK)
28
+
29
+ val videoView = VideoView(this)
30
+ val videoParams = FrameLayout.LayoutParams(
31
+ FrameLayout.LayoutParams.MATCH_PARENT,
32
+ FrameLayout.LayoutParams.MATCH_PARENT,
33
+ Gravity.CENTER
34
+ )
35
+ container.addView(videoView, videoParams)
36
+
37
+ // Loading spinner
38
+ val progressBar = ProgressBar(this)
39
+ val progressParams = FrameLayout.LayoutParams(
40
+ FrameLayout.LayoutParams.WRAP_CONTENT,
41
+ FrameLayout.LayoutParams.WRAP_CONTENT,
42
+ Gravity.CENTER
43
+ )
44
+ container.addView(progressBar, progressParams)
45
+
46
+ // Close button (X) top-right
47
+ val closeButton = ImageButton(this)
48
+ closeButton.setBackgroundColor(android.graphics.Color.TRANSPARENT)
49
+ closeButton.setImageResource(android.R.drawable.ic_menu_close_clear_cancel)
50
+ closeButton.setColorFilter(android.graphics.Color.WHITE)
51
+ closeButton.setPadding(24, 24, 24, 24)
52
+ val closeParams = FrameLayout.LayoutParams(
53
+ FrameLayout.LayoutParams.WRAP_CONTENT,
54
+ FrameLayout.LayoutParams.WRAP_CONTENT,
55
+ Gravity.TOP or Gravity.END
56
+ )
57
+ closeParams.topMargin = 60
58
+ closeParams.rightMargin = 24
59
+ container.addView(closeButton, closeParams)
60
+
61
+ closeButton.setOnClickListener {
62
+ finish()
63
+ }
64
+
65
+ setContentView(container)
66
+
67
+ val videoUri = intent.getStringExtra("video_uri") ?: run {
68
+ finish()
69
+ return
70
+ }
71
+
72
+ // MediaController for play/pause/seek controls
73
+ val mediaController = MediaController(this)
74
+ mediaController.setAnchorView(videoView)
75
+ videoView.setMediaController(mediaController)
76
+
77
+ videoView.setVideoURI(Uri.parse(videoUri))
78
+ videoView.setOnPreparedListener { mp ->
79
+ progressBar.visibility = android.view.View.GONE
80
+ mp.isLooping = false
81
+ videoView.start()
82
+ }
83
+ videoView.setOnCompletionListener {
84
+ finish()
85
+ }
86
+ videoView.setOnErrorListener { _, _, _ ->
87
+ finish()
88
+ true
89
+ }
90
+ }
91
+ }
@@ -15,6 +15,7 @@ import com.facebook.react.bridge.Arguments
15
15
  import com.facebook.react.bridge.WritableMap
16
16
  import com.google.firebase.messaging.RemoteMessage
17
17
  import com.comnyx.Logger
18
+ import kotlin.math.abs
18
19
 
19
20
  class NotificationsHelper(private val context: Context) {
20
21
 
@@ -149,7 +150,17 @@ class NotificationsHelper(private val context: Context) {
149
150
  .setContentIntent(pendingIntent)
150
151
 
151
152
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
152
- notificationManager.notify(NOTIFICATION_ID, builder.build())
153
+ // Use a unique notification id per message so notifications don't overwrite each other.
154
+ // Prefer FCM's messageId when available; fall back to sentTime/currentTimeMillis.
155
+ val notificationId: Int = remoteMessage.messageId
156
+ ?.hashCode()
157
+ ?.let { if (it == Int.MIN_VALUE) Int.MAX_VALUE else abs(it) }
158
+ ?: run {
159
+ val basis = if (remoteMessage.sentTime > 0) remoteMessage.sentTime else System.currentTimeMillis()
160
+ (basis % Int.MAX_VALUE).toInt()
161
+ }
162
+
163
+ notificationManager.notify(notificationId, builder.build())
153
164
  Logger.i("Notification displayed successfully", TAG)
154
165
 
155
166
  val writableMap: WritableMap = Arguments.createMap()
@@ -0,0 +1,23 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(ComnyxMediaPicker, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(pickMedia:(RCTPromiseResolveBlock)resolve
6
+ reject:(RCTPromiseRejectBlock)reject)
7
+
8
+ RCT_EXTERN_METHOD(generateThumbnail:(NSString *)videoUrl
9
+ resolve:(RCTPromiseResolveBlock)resolve
10
+ reject:(RCTPromiseRejectBlock)reject)
11
+
12
+ RCT_EXTERN_METHOD(openVideo:(NSString *)uri
13
+ resolve:(RCTPromiseResolveBlock)resolve
14
+ reject:(RCTPromiseRejectBlock)reject)
15
+
16
+ RCT_EXTERN_METHOD(deleteTempFile:(NSString *)uri
17
+ resolve:(RCTPromiseResolveBlock)resolve
18
+ reject:(RCTPromiseRejectBlock)reject)
19
+
20
+ RCT_EXTERN_METHOD(cleanupTempFiles:(RCTPromiseResolveBlock)resolve
21
+ reject:(RCTPromiseRejectBlock)reject)
22
+
23
+ @end