@Composable
private fun CustomAttachmentsPicker(
attachmentsPickerViewModel: AttachmentsPickerViewModel,
onAttachmentsSelected: (List<Attachment>) -> Unit,
onDismiss: () -> Unit,
tabFactories: List<AttachmentsPickerTabFactory> = ChatTheme.attachmentsPickerTabFactories,
) {
var shouldShowMenu by remember { mutableStateOf(true) }
var selectedOptionIndex by remember { mutableStateOf(-1) }
Box( // Gray overlay
modifier = Modifier
.fillMaxSize()
.background(ChatTheme.colors.overlay)
.clickable(
onClick = onDismiss,
indication = null,
interactionSource = remember { MutableInteractionSource() },
),
) {
Card(
modifier = Modifier
.heightIn(max = 350.dp)
.align(Alignment.BottomCenter)
.clickable(
indication = null,
onClick = {},
interactionSource = remember { MutableInteractionSource() },
),
elevation = 4.dp,
shape = ChatTheme.shapes.bottomSheet,
backgroundColor = ChatTheme.colors.inputBackground,
) {
Box(modifier = Modifier.padding(vertical = 24.dp)) {
if (shouldShowMenu) {
// Show the menu with Images, Files, Camera options
// See implementation in dedicated section below
AttachmentsTypeMenu(
tabFactories = tabFactories,
onClick = {
selectedOptionIndex = it
shouldShowMenu = false
},
)
} else {
// Show the selected tabFactory content, with a back and submit buttons toolbar
Column(
modifier = Modifier.padding(horizontal = 8.dp),
) {
// See implementation in dedicated section below
AttachmentsPickerToolbar(
onBackClick = {
shouldShowMenu = true
selectedOptionIndex = -1
},
isSubmitEnabled = attachmentsPickerViewModel.hasPickedAttachments,
onSubmitClick = {
onAttachmentsSelected(attachmentsPickerViewModel.getSelectedAttachments())
},
)
tabFactories.getOrNull(selectedOptionIndex)
?.PickerTabContent(
onAttachmentPickerAction = { pickerAction ->
when (pickerAction) {
AttachmentPickerBack -> onDismiss.invoke()
is AttachmentPickerPollCreation -> Unit
}
},
attachments = attachmentsPickerViewModel.attachments,
onAttachmentItemSelected = attachmentsPickerViewModel::changeSelectedAttachments,
onAttachmentsChanged = { attachmentsPickerViewModel.attachments = it },
onAttachmentsSubmitted = {
onAttachmentsSelected(attachmentsPickerViewModel.getAttachmentsFromMetaData(it))
},
)
}
}
}
}
}
}
Custom Attachments Picker
The AttachmentsPicker
component allows users to pick media, files or capture media attachments. You can find more info about it here.
By default, it looks like below:
Default - Images tab selected | Default - Files tab selected |
---|---|
When we’re done, our custom attachments picker will look this this:
Custom - Attachment type menu | Custom - File picker with Back and Submit |
---|---|
Although AttachmentsPicker
can be customized extensively, as you can read here, for this example we’ll create our own component. We’ll mostly keep the signature of the standard AttachmentsPicker
and rely on the default tabFactories
parameter value to show pickers for several attachments types (Images, Files, Camera).
Main Composable
Let’s define the main composable for our custom attachment picker. Notice that it expects an AttachmentsPickerViewModel
, which is part of our SDK. Also, notice the usage of ChatTheme.attachmentsPickerTabFactories
as a default value for the tabFactories
parameter. It provides the standard selector content for Images, Files and Camera attachment type.
Attachment Type Menu
The attachment type menu (Images, Files, Camera) is drawn by a simple composable that shows a menu item for each tabFactory
.
@Composable
private fun AttachmentsTypeMenu(
tabFactories: List<AttachmentsPickerTabFactory>,
onClick: (Int) -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
tabFactories.forEachIndexed { index, tabFactory ->
AttachmentsTypeMenuItem(
tabFactory = tabFactory,
isEnabled = tabFactory.isPickerTabEnabled(),
index = index,
onClick = onClick,
)
}
}
}
Each menu item is represented by a circle with a label underneath. So we need a Column
, a circle-shaped Box
with a certain background color, and a Text
.
@Composable
private fun AttachmentsTypeMenuItem(
tabFactory: AttachmentsPickerTabFactory,
isEnabled: Boolean,
index: Int,
onClick: (Int) -> Unit,
) {
Column(
modifier = Modifier.clickable(enabled = isEnabled) { onClick(index) },
horizontalAlignment = Alignment.CenterHorizontally,
) {
val backgroundColor: Color
val label: String
when (tabFactory.attachmentsPickerMode) {
is Images -> {
backgroundColor = Color(0xFFCCCCFF)
label = "Images"
}
is Files -> {
backgroundColor = Color(0xFFFFCCCC)
label = "Files"
}
is MediaCapture -> {
backgroundColor = Color(0xFFFFCC99)
label = "Camera"
}
else -> {
backgroundColor = Color.LightGray
label = "Other"
}
}
Box(
modifier = Modifier
.padding(8.dp)
.size(48.dp)
.background(backgroundColor, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
tabFactory.PickerTabIcon(isEnabled, isSelected = false)
}
Text(text = label)
}
}
Picker Toolbar
Above each picker, we draw a toolbar with Back and Submit buttons. Back navigates to the menu and Submit attaches the selected file to the message contents.
@Composable
private fun AttachmentsPickerToolbar(
onBackClick: () -> Unit,
isSubmitEnabled: Boolean,
onSubmitClick: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
IconButton(onClick = onBackClick) {
Icon(
painter = painterResource(id = R.drawable.ic_back),
contentDescription = "Back",
modifier = Modifier.size(24.dp),
)
}
IconButton(
enabled = isSubmitEnabled,
onClick = onSubmitClick
) {
Icon(
painter = painterResource(id = R.drawable.ic_check),
contentDescription = "Submit Attachments",
modifier = Modifier.size(24.dp),
tint = if (isSubmitEnabled) {
ChatTheme.colors.primaryAccent
} else {
ChatTheme.colors.textLowEmphasis
},
)
}
}
}
Usage
We’ll place our custom attachments picker in a screen that contains other components, like a header, a messages list and a message composer. We’ll also use the BackHandler
standard component.
In order to show the attachments picker, we’ll use an isShowingAttachments
flag. We’ll wrap all components in a Box
, so that the attachments picker appears on top of other content.
fun CustomScreen(cid: String, onBackClick: () -> Unit = {}) {
val viewModelFactory = MessagesViewModelFactory(LocalContext.current, channelId = cid)
val listViewModel = viewModel(modelClass = MessageListViewModel::class.java, factory = viewModelFactory)
val composerViewModel = viewModel(modelClass = MessageComposerViewModel::class.java, factory = viewModelFactory)
val attachmentsPickerViewModel = viewModel(modelClass = AttachmentsPickerViewModel::class.java, factory = viewModelFactory)
val isShowingAttachments = attachmentsPickerViewModel.isShowingAttachments
val backAction = remember(composerViewModel, attachmentsPickerViewModel) {
{
// First close the attachments picker, if visible, then call onBackClick
when {
attachmentsPickerViewModel.isShowingAttachments -> {
attachmentsPickerViewModel.changeAttachmentState(false)
}
else -> onBackClick()
}
}
}
BackHandler(enabled = true, onBack = backAction) // Standard SDK component
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
// Screen content
Scaffold(
topBar = {
// MessageListHeader
},
bottomBar = {
// MessageComposer
},
content = {
// MessageList
}
)
// Attachments picker
if (isShowingAttachments) {
CustomAttachmentsPicker(
attachmentsPickerViewModel = attachmentsPickerViewModel,
onAttachmentsSelected = { attachments ->
attachmentsPickerViewModel.changeAttachmentState(false)
composerViewModel.addSelectedAttachments(attachments)
},
onDismiss = {
attachmentsPickerViewModel.changeAttachmentState(false)
attachmentsPickerViewModel.dismissAttachments()
}
)
}
}
}
More Resources
If you want to learn how to use our Compose UI Components, see this page.
Also, check the other pages in this Cookbook to find out how to create custom versions of our components.