Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Fix `Message.channelInfo` not populated when parsing `message.new` event. [#5953

### ✅ Added
- Add `ChatTheme.reactionPushEmojiFactory` for customizing the emoji codes for reaction push notifications. [#5935](https://github.com/GetStream/stream-chat-android/pull/5935)
- Add `rememberCaptureMediaLauncher` for registering an activity result launcher to capture media using the device camera. [#5955](https://github.com/GetStream/stream-chat-android/pull/5955)

### ⚠️ Changed
- Change `AttachmentPickerAction` from `sealed interface` to `interface` to allow extension outside of the SDK. [#5943](https://github.com/GetStream/stream-chat-android/pull/5943)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2318,6 +2318,10 @@ public final class io/getstream/chat/android/compose/ui/messages/attachments/fac
public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3;
}

public final class io/getstream/chat/android/compose/ui/messages/attachments/media/CaptureMediaLauncherKt {
public static final fun rememberCaptureMediaLauncher (ZZLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Landroidx/activity/compose/ManagedActivityResultLauncher;
}

public final class io/getstream/chat/android/compose/ui/messages/attachments/poll/ComposableSingletons$PollCreationDiscardDialogKt {
public static final field INSTANCE Lio/getstream/chat/android/compose/ui/messages/attachments/poll/ComposableSingletons$PollCreationDiscardDialogKt;
public static field lambda-1 Lkotlin/jvm/functions/Function3;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,16 @@ import io.getstream.chat.android.compose.state.messages.attachments.AttachmentsP
import io.getstream.chat.android.compose.state.messages.attachments.MediaCapture
import io.getstream.chat.android.compose.state.messages.attachments.Poll
import io.getstream.chat.android.compose.state.messages.attachments.System
import io.getstream.chat.android.compose.ui.messages.attachments.media.rememberCaptureMediaLauncher
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.ui.util.StorageHelperWrapper
import io.getstream.chat.android.compose.ui.util.clickable
import io.getstream.chat.android.ui.common.contract.internal.CaptureMediaContract
import io.getstream.chat.android.ui.common.helper.internal.AttachmentFilter
import io.getstream.chat.android.ui.common.helper.internal.StorageHelper
import io.getstream.chat.android.ui.common.permissions.SystemAttachmentsPickerConfig
import io.getstream.chat.android.ui.common.permissions.toContractVisualMediaType
import io.getstream.chat.android.ui.common.state.messages.composer.AttachmentMetaData
import io.getstream.chat.android.ui.common.utils.isPermissionDeclared
import java.io.File

/**
* Holds the information required to add support for "files" tab in the attachment picker.
Expand Down Expand Up @@ -163,9 +162,6 @@ public class AttachmentsPickerSystemTabFactory(
@Deprecated(message = "Use config.pollAllowed instead.", level = DeprecationLevel.WARNING)
public val pollAllowed: Boolean = config.pollAllowed

private val mediaPickerContract = resolveMediaPickerMode(config.captureImageAllowed, config.captureVideoAllowed)
?.let(::CaptureMediaContract)

private val pollFactory by lazy { AttachmentsPickerPollTabFactory() }

/**
Expand Down Expand Up @@ -236,7 +232,10 @@ public class AttachmentsPickerSystemTabFactory(
onAttachmentsSubmitted(storageHelper.getAttachmentsMetadataFromUris(uris))
}

val captureLauncher = rememberCaptureMediaLauncher { file ->
val captureLauncher = rememberCaptureMediaLauncher(
photo = config.captureImageAllowed,
video = config.captureVideoAllowed,
) { file ->
onAttachmentsSubmitted(listOf(AttachmentMetaData(context, file)))
}
var cameraRationaleShown by remember { mutableStateOf(false) }
Expand All @@ -254,7 +253,7 @@ public class AttachmentsPickerSystemTabFactory(
val buttonsConfig = ButtonsConfig(
filesAllowed = config.filesAllowed,
mediaAllowed = config.visualMediaAllowed,
captureAllowed = mediaPickerContract != null,
captureAllowed = captureLauncher != null,
pollAllowed = config.pollAllowed,
)
val visualMediaType = config.visualMediaType.toContractVisualMediaType()
Expand Down Expand Up @@ -327,21 +326,6 @@ public class AttachmentsPickerSystemTabFactory(
}
}

@Composable
private fun rememberCaptureMediaLauncher(onResult: (File) -> Unit) =
mediaPickerContract?.let {
rememberLauncherForActivityResult(mediaPickerContract) { file ->
file?.let(onResult)
}
}

private fun resolveMediaPickerMode(captureImageAllowed: Boolean, captureVideoAllowed: Boolean) = when {
captureImageAllowed && captureVideoAllowed -> CaptureMediaContract.Mode.PHOTO_AND_VIDEO
captureImageAllowed -> CaptureMediaContract.Mode.PHOTO
captureVideoAllowed -> CaptureMediaContract.Mode.VIDEO
else -> null
}

private fun filePickerIntent(): Intent {
val attachmentFilter = AttachmentFilter()
return Intent(Intent.ACTION_GET_CONTENT).apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.compose.ui.messages.attachments.media

import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.runtime.Composable
import io.getstream.chat.android.ui.common.contract.internal.CaptureMediaContract
import java.io.File

/**
* Creates and remembers a launcher for capturing media (photo and/or video) using the device camera.
*
* @param photo If `true`, enables photo capture capability. When both [photo] and [video] are
* `true`, the user will be able to capture both types of media.
* @param video If `true`, enables video capture capability. When both [photo] and [video] are
* `true`, the user will be able to capture both types of media.
* @param onResult Callback invoked when media capture completes successfully. Receives a [File]
* representing the captured media. This callback is only invoked if the user successfully captures
* media; it is not called if the user cancels the capture.
*
* @return A [ManagedActivityResultLauncher] that can be used to launch the media capture activity,
* or `null` if both [photo] and [video] are `false` (no valid capture mode). The launcher accepts
* `Unit` as input and produces a nullable [File] as output.
*
* @see CaptureMediaContract
*
* Example usage:
* ```kotlin
* val captureMediaLauncher = rememberCaptureMediaLauncher(
* photo = true,
* video = true,
* onResult = { file ->
* // Handle captured media file
* }
* )
*
* // Launch the camera
* captureMediaLauncher?.launch(Unit)
* ```
*
* Note: This function doesn't check for camera permissions. Ensure that the necessary permissions
* are granted before invoking the launcher.
*/
@Composable
public fun rememberCaptureMediaLauncher(
photo: Boolean,
video: Boolean,
onResult: (File) -> Unit,
): ManagedActivityResultLauncher<Unit, File?>? {
val mode = resolveMediaPickerMode(photo, video) ?: return null
return rememberLauncherForActivityResult(CaptureMediaContract(mode)) { file ->
file?.let(onResult)
}
}

private fun resolveMediaPickerMode(photo: Boolean, video: Boolean): CaptureMediaContract.Mode? =
when {
photo && video -> CaptureMediaContract.Mode.PHOTO_AND_VIDEO
photo -> CaptureMediaContract.Mode.PHOTO
video -> CaptureMediaContract.Mode.VIDEO
else -> null
}
Loading