diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt index 9d41dcca2c6..566e3d5b736 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt @@ -817,9 +817,11 @@ public class MessageListController( val groupedMessages = mutableListOf() val membersMap = members.associateBy { it.user.id } val sortedReads = reads - .filter { it.user.id != currentUser?.id && !it.belongsToFreshlyAddedMember(membersMap) } + .filter { + it.user.id != currentUser?.id && membersMap.contains(it.user.id) && + !it.belongsToFreshlyAddedMember(membersMap) + } .sortedBy { it.lastRead } - val lastRead = sortedReads.lastOrNull()?.lastRead val isThreadWithNoReplies = isInThread && messages.size == 1 val isThreadWithReplies = isInThread && messages.size > 1 @@ -874,14 +876,15 @@ public class MessageListController( if (message.isSystem() || (message.isError() && !message.isModerationBounce())) { groupedMessages.add(SystemMessageItemState(message = message)) } else { - val isMessageRead = message.createdAt - ?.let { lastRead != null && it <= lastRead } - ?: false - val messageReadBy = message.createdAt?.let { messageCreatedAt -> - sortedReads.filter { it.lastRead.after(messageCreatedAt) ?: false } + sortedReads.filter { + it.lastRead.after(messageCreatedAt) && + membersMap[it.user.id]?.createdAt?.before(messageCreatedAt) == true + } } ?: emptyList() + val isMessageRead = messageReadBy.isNotEmpty() + val isMessageFocused = message.id == focusedMessage?.id if (isMessageFocused) removeMessageFocus(message.id) @@ -939,6 +942,12 @@ public class MessageListController( ): Boolean { val member = membersMap[user.id] val membershipAndLastReadDiff = member?.createdAt?.diff(lastRead)?.millis ?: Long.MAX_VALUE + if (member?.createdAt?.after(lastRead) == true) { + // If the member was added after the last read, we consider it as a freshly added member. + return true + } + // If the difference between the member's creation and the last read is less than the threshold, we consider it + // as a freshly added member. return membershipAndLastReadDiff < MEMBERSHIP_AND_LAST_READ_THRESHOLD_MS } diff --git a/stream-chat-android-ui-components-sample/detekt-baseline.xml b/stream-chat-android-ui-components-sample/detekt-baseline.xml index 2044a043bf7..fbdce19c757 100644 --- a/stream-chat-android-ui-components-sample/detekt-baseline.xml +++ b/stream-chat-android-ui-components-sample/detekt-baseline.xml @@ -4,6 +4,8 @@ ComplexCondition:UserRepository.kt$UserRepository$apiKey != null && id != null && name != null && token != null && image != null ForbiddenComment:CustomChatClientDebugger.kt$CustomChatClientDebugger$// TODO: Implement your custom logic here + ForbiddenComment:MessageDetailsFragment.kt$MessageDetailsFragment$// TODO: Navigate up + ForbiddenComment:MessageDetailsFragment.kt$MessageDetailsFragment$// TODO: Show error LongMethod:ChatFragment.kt$ChatFragment$private fun initMessageListViewModel() LongMethod:ChatInitializer.kt$ChatInitializer$@Suppress("UNUSED_VARIABLE") fun init(apiKey: String) LongMethod:ComponentBrowserAvatarViewFragment.kt$ComponentBrowserAvatarViewFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?) @@ -50,6 +52,7 @@ MaxLineLength:GroupChatInfoMemberOptionsDialogFragment.kt$GroupChatInfoMemberOptionsDialogFragment$is GroupChatInfoMemberOptionsViewModel.ErrorEvent.UnbanMemberError -> R.string.chat_group_info_error_unban_member MaxLineLength:GroupChatInfoMemberOptionsViewModel.kt$GroupChatInfoMemberOptionsViewModelFactory$"GroupChatInfoMemberOptionsViewModelFactory can only create instances of GroupChatInfoMemberOptionsViewModel" MaxLineLength:GroupChatInfoViewModel.kt$GroupChatInfoViewModel$shouldExpandMembers = currentState.shouldExpandMembers ?: false || members.size <= COLLAPSED_MEMBERS_COUNT + MaxLineLength:MessageDetailsFragment.kt$MessageDetailsFragment$val MaxLineLength:MessageListComponentBrowserFragment.kt$MessageListComponentBrowserFragment$findNavController().navigateSafely(R.id.action_componentBrowserMessageList_to_componentBrowserDateDividerFragment) MaxLineLength:MessageListComponentBrowserFragment.kt$MessageListComponentBrowserFragment$findNavController().navigateSafely(R.id.action_componentBrowserMessageList_to_componentBrowserDeletedMessages) MaxLineLength:MessageListComponentBrowserFragment.kt$MessageListComponentBrowserFragment$findNavController().navigateSafely(R.id.action_componentBrowserMessageList_to_componentBrowserOnlyFileAttachmentsMessages) @@ -65,6 +68,9 @@ MaxLineLength:SharedAttachment.kt$SharedAttachment$AttachmentItem : SharedAttachment SerialVersionUIDInSerializableClass:UserData.kt$MemberData : Serializable SerialVersionUIDInSerializableClass:UserData.kt$UserData : Serializable + TooGenericExceptionCaught:MessageDetailsViewModel.kt$MessageDetailsViewModel$e: Exception + UnusedPrivateMember:MessageDetailsFragment.kt$MessageDetailsFragment$private fun observeMessage() VariableNaming:ComponentBrowserSearchViewFragment.kt$ComponentBrowserSearchViewFragment$private val TAG = ComponentBrowserSearchViewFragment::class.simpleName + VariableNaming:MessageDetailsViewModel.kt$MessageDetailsViewModel$val _state = MutableStateFlow<MessageDetailsViewState>(MessageDetailsViewState.Empty) diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/ChatFragment.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/ChatFragment.kt index 93771c1fc63..aacc1090b4d 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/ChatFragment.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/ChatFragment.kt @@ -43,6 +43,8 @@ import io.getstream.chat.android.ui.common.state.messages.list.DeleteMessage import io.getstream.chat.android.ui.common.state.messages.list.DeletedMessageVisibility import io.getstream.chat.android.ui.common.state.messages.list.EditMessage import io.getstream.chat.android.ui.common.state.messages.list.SendAnyway +import io.getstream.chat.android.ui.feature.messages.list.options.message.MessageOptionItemsFactory +import io.getstream.chat.android.ui.feature.messages.list.options.message.plus import io.getstream.chat.android.ui.utils.extensions.getCreatedAtOrThrow import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModel import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModel @@ -52,6 +54,8 @@ import io.getstream.chat.android.ui.viewmodel.messages.bindView import io.getstream.chat.ui.sample.common.navigateSafely import io.getstream.chat.ui.sample.databinding.FragmentChatBinding import io.getstream.chat.ui.sample.feature.chat.composer.CustomMessageComposerLeadingContent +import io.getstream.chat.ui.sample.feature.chat.messagelist.options.CustomMessageOption +import io.getstream.chat.ui.sample.feature.chat.messagelist.options.CustomMessageOptionItemsFactory import io.getstream.chat.ui.sample.feature.common.ConfirmationDialogFragment import io.getstream.chat.ui.sample.util.extensions.useAdjustResize import io.getstream.log.taggedLogger @@ -303,6 +307,24 @@ class ChatFragment : Fragment() { else -> Unit } } + + setMessageOptionItemsFactory( + CustomMessageOptionItemsFactory(requireContext()) + + MessageOptionItemsFactory.defaultFactory(requireContext()), + ) + + setCustomActionHandler { message, extra -> + when (extra[CustomMessageOption.TYPE]) { + CustomMessageOption.TYPE_MESSAGE_DETAILS -> { + findNavController().navigateSafely( + ChatFragmentDirections.actionChatFragmentToMessageDetailsFragment( + args.cid, + message.id, + ), + ) + } + } + } } } diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/details/MessageDetailsAdapter.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/details/MessageDetailsAdapter.kt new file mode 100644 index 00000000000..8f698e07160 --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/details/MessageDetailsAdapter.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2014-2024 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.ui.sample.feature.chat.messagelist.details + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import io.getstream.chat.android.models.User +import io.getstream.chat.ui.sample.common.appThemeContext +import io.getstream.chat.ui.sample.databinding.AdapterMessageDetailsReadByBinding +import java.util.Date + +class MessageDetailsAdapter : ListAdapter>(MessageDetailsItemDiff) { + + override fun getItemCount(): Int { + return super.getItemCount() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageDetailsViewHolder<*> { + return AdapterMessageDetailsReadByBinding + .inflate(LayoutInflater.from(parent.context.appThemeContext), parent, false) + .let(::ReadByViewHolder) + } + + override fun onBindViewHolder(holder: MessageDetailsViewHolder<*>, position: Int) { + when (holder) { + is ReadByViewHolder -> holder.bind(getItem(position) as ReadByItem) + } + } +} + +sealed class MessageDetailsItem { + abstract val id: String +} + +sealed class MessageDetailsViewHolder( + itemView: View, +) : RecyclerView.ViewHolder(itemView) { + abstract fun bind(item: T) +} + +class ReadByViewHolder( + private val binding: AdapterMessageDetailsReadByBinding, +) : MessageDetailsViewHolder(binding.root) { + override fun bind(item: ReadByItem) { + binding.userAvatarView.setUser(item.user) + binding.nameTextView.text = item.user.name + binding.readAtTextView.text = item.lastReadAt.toString() + } +} + +data class ReadByItem( + val user: User, + val lastReadAt: Date, + val lastReadMessageId: String, +) : MessageDetailsItem() { + override val id: String get() = user.id +} + +private object MessageDetailsItemDiff : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MessageDetailsItem, newItem: MessageDetailsItem): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: MessageDetailsItem, newItem: MessageDetailsItem): Boolean { + return oldItem == newItem + } +} diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/details/MessageDetailsFragment.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/details/MessageDetailsFragment.kt new file mode 100644 index 00000000000..363f5a11f36 --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/details/MessageDetailsFragment.kt @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2014-2024 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.ui.sample.feature.chat.messagelist.details + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.navArgs +import io.getstream.chat.android.client.extensions.getCreatedAtOrNull +import io.getstream.chat.android.models.ChannelUserRead +import io.getstream.chat.android.ui.feature.messages.list.adapter.MessageListItem +import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel +import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelFactory +import io.getstream.chat.ui.sample.R +import io.getstream.chat.ui.sample.databinding.FragmentMessageDetailsBinding +import io.getstream.log.taggedLogger +import kotlinx.coroutines.launch + +class MessageDetailsFragment : Fragment() { + + private val logger by taggedLogger("ChatFragment") + + private val args: MessageDetailsFragmentArgs by navArgs() + + private val viewModel: MessageDetailsViewModel by viewModels { + MessageDetailsViewModelFactory(args.cid, args.messageId) + } + + private val messageListViewModel: MessageListViewModel by viewModels { + MessageListViewModelFactory( + context = requireContext().applicationContext, + cid = args.cid, + messageId = args.messageId, + ) + } + + private var _binding: FragmentMessageDetailsBinding? = null + private val binding get() = _binding!! + + private val adapter: MessageDetailsAdapter = MessageDetailsAdapter() + + override fun onAttach(context: Context) { + super.onAttach(context) + logger.i { "[onAttach] context: $context, targetFragment: $targetFragment" } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + logger.i { "[onCreate] args: $args" } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentMessageDetailsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.initToolbar() + + binding.readByValue.itemAnimator = null + binding.readByValue.adapter = adapter + + observeMessageList() + // observeMessage() + } + + private fun observeMessageList() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + messageListViewModel.state.observe(viewLifecycleOwner) { state -> + logger.i { "[onViewCreated] state: $state" } + when (state) { + is MessageListViewModel.State.Loading -> { + binding.progressBar.isVisible = true + } + is MessageListViewModel.State.Result -> { + binding.progressBar.isVisible = false + val messageItem = state.messageListItem.items.filterIsInstance() + .firstOrNull { it.message.id == args.messageId } + if (messageItem == null) { + logger.w { "[observeMessageList] was unable to find messageId: ${args.messageId}" } + return@observe + } + binding.sentByValue.text = messageItem.message.user.name + binding.createAtValue.text = messageItem.message.getCreatedAtOrNull()?.toString() ?: "N/A" + binding.readByValueEmpty.isVisible = messageItem.messageReadBy.isEmpty() + binding.readByValue.isVisible = messageItem.messageReadBy.isNotEmpty() + + val items = messageItem.messageReadBy.toItems() + adapter.submitList(items) + } + MessageListViewModel.State.NavigateUp -> { + // TODO: Navigate up + } + } + } + } + } + } + + private fun observeMessage() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.state.collect { state -> + logger.i { "[onViewCreated] state: $state" } + when (state) { + MessageDetailsViewState.Empty -> { + binding.progressBar.isVisible = false + } + + MessageDetailsViewState.Loading -> { + binding.progressBar.isVisible = true + } + + is MessageDetailsViewState.Failed -> { + binding.progressBar.isVisible = false + // TODO: Show error + } + + is MessageDetailsViewState.Loaded -> { + binding.progressBar.isVisible = false + binding.sentByValue.text = state.sentBy + binding.createAtValue.text = state.createdAt + binding.readByValueEmpty.isVisible = state.readBy.isEmpty() + binding.readByValue.isVisible = state.readBy.isNotEmpty() + adapter.submitList(state.readBy.toItems()) + } + } + } + } + } + } + + private fun FragmentMessageDetailsBinding.initToolbar() { + (requireActivity() as AppCompatActivity).run { + setSupportActionBar(toolbar) + supportActionBar?.run { + setDisplayShowTitleEnabled(false) + setDisplayShowHomeEnabled(true) + setDisplayHomeAsUpEnabled(true) + + ContextCompat.getDrawable(requireContext(), R.drawable.ic_icon_left)?.apply { + setTint(ContextCompat.getColor(requireContext(), R.color.stream_ui_black)) + }?.let(toolbar::setNavigationIcon) + + toolbar.setNavigationOnClickListener { + onBackPressed() + } + } + } + toolbar.setTitle(R.string.message_details) + } +} + +private fun List.toItems(): List { + return map(ChannelUserRead::toItem) +} + +private fun ChannelUserRead.toItem(): MessageDetailsItem { + return ReadByItem( + user = user, + lastReadAt = lastRead, + lastReadMessageId = lastReadMessageId.orEmpty(), + ) +} diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/details/MessageDetailsViewModel.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/details/MessageDetailsViewModel.kt new file mode 100644 index 00000000000..3a65ff7098e --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/details/MessageDetailsViewModel.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2014-2024 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.ui.sample.feature.chat.messagelist.details + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.client.api.models.QueryChannelRequest +import io.getstream.chat.android.client.channel.ChannelClient +import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.ChannelUserRead +import io.getstream.chat.android.ui.utils.extensions.getCreatedAtOrNull +import io.getstream.log.taggedLogger +import io.getstream.result.call.Call +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class MessageDetailsViewModel( + private val cid: String, + private val messageId: String, + private val chatClient: ChatClient, +) : ViewModel() { + + private val logger by taggedLogger("MessageDetails-VM") + + private val channel by lazy { chatClient.channel(cid) } + + val _state = MutableStateFlow(MessageDetailsViewState.Empty) + val state: StateFlow = _state + + init { + iniState() + } + + private fun iniState() { + viewModelScope.launch { + _state.value = MessageDetailsViewState.Loading + try { + val deferredChannel = async { channel.getChannelState().await() } + val deferredMessage = async { channel.getMessage(messageId).await() } + val channel = deferredChannel.await().getOrThrow() + val message = deferredMessage.await().getOrThrow() + + val sentBy = message.user.name + val createdAt = message.getCreatedAtOrNull() ?: error("Message created at is null") + val readBy = channel.read + .filter { it.lastRead > createdAt } + + _state.value = MessageDetailsViewState.Loaded( + sentBy = sentBy, + createdAt = createdAt.toString(), + readBy = readBy, + ) + } catch (e: Exception) { + logger.e(e) { "Failed to get message with id: $messageId" } + _state.value = MessageDetailsViewState.Failed(e.message ?: "Failed to get message: $messageId") + } + } + } +} + +sealed class MessageDetailsViewState { + data object Empty : MessageDetailsViewState() + data object Loading : MessageDetailsViewState() + data class Loaded( + val sentBy: String, + val createdAt: String, + val readBy: List, + ) : MessageDetailsViewState() + data class Failed(val error: String) : MessageDetailsViewState() +} + +class MessageDetailsViewModelFactory( + private val cid: String, + private val messageId: String, +) : ViewModelProvider.Factory { + private val factories: Map, () -> ViewModel> = mapOf( + MessageDetailsViewModel::class.java to { MessageDetailsViewModel(cid, messageId, ChatClient.instance()) }, + ) + + override fun create(modelClass: Class): T { + val viewModel: ViewModel = factories[modelClass]?.invoke() + ?: throw IllegalArgumentException( + "MessageDetailsViewModelFactory can only create instances " + + "of the following classes: ${factories.keys.joinToString { it.simpleName }}", + ) + + @Suppress("UNCHECKED_CAST") + return viewModel as T + } +} + +private fun ChannelClient.getChannelState(): Call { + return query( + QueryChannelRequest().apply { + state = true + }, + ) +} diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/options/CustomMessageOption.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/options/CustomMessageOption.kt new file mode 100644 index 00000000000..f321de05c62 --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/options/CustomMessageOption.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2014-2024 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.ui.sample.feature.chat.messagelist.options + +object CustomMessageOption { + + const val TYPE = "custom:option_type" + const val TYPE_MESSAGE_DETAILS = "type:option_message_details" +} diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/options/CustomMessageOptionItemsFactory.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/options/CustomMessageOptionItemsFactory.kt new file mode 100644 index 00000000000..69de0a5086f --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/messagelist/options/CustomMessageOptionItemsFactory.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2014-2024 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.ui.sample.feature.chat.messagelist.options + +import android.content.Context +import androidx.core.content.ContextCompat +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.User +import io.getstream.chat.android.ui.common.state.messages.CustomAction +import io.getstream.chat.android.ui.feature.messages.list.MessageListViewStyle +import io.getstream.chat.android.ui.feature.messages.list.options.message.MessageOptionItem +import io.getstream.chat.android.ui.feature.messages.list.options.message.MessageOptionItemsFactory +import io.getstream.chat.ui.sample.R + +class CustomMessageOptionItemsFactory( + private val context: Context, +) : MessageOptionItemsFactory { + + override fun createMessageOptionItems( + selectedMessage: Message, + currentUser: User?, + isInThread: Boolean, + ownCapabilities: Set, + style: MessageListViewStyle, + ): List { + return listOf( + MessageOptionItem( + optionText = context.getString(R.string.message_details), + optionIcon = ContextCompat.getDrawable(context, R.drawable.ic_message_details)!!, + messageAction = CustomAction( + selectedMessage, + mapOf( + CustomMessageOption.TYPE to CustomMessageOption.TYPE_MESSAGE_DETAILS, + ), + ), + ), + ) + } +} diff --git a/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_message_details.xml b/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_message_details.xml new file mode 100644 index 00000000000..31940ab9119 --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_message_details.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/stream-chat-android-ui-components-sample/src/main/res/layout/adapter_message_details_read_by.xml b/stream-chat-android-ui-components-sample/src/main/res/layout/adapter_message_details_read_by.xml new file mode 100644 index 00000000000..881ca7aef2a --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/res/layout/adapter_message_details_read_by.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/stream-chat-android-ui-components-sample/src/main/res/layout/fragment_message_details.xml b/stream-chat-android-ui-components-sample/src/main/res/layout/fragment_message_details.xml new file mode 100644 index 00000000000..cec221ce0e0 --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/res/layout/fragment_message_details.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/stream-chat-android-ui-components-sample/src/main/res/navigation/main_nav_graph.xml b/stream-chat-android-ui-components-sample/src/main/res/navigation/main_nav_graph.xml index 24d70ab1e75..07139b8e50c 100644 --- a/stream-chat-android-ui-components-sample/src/main/res/navigation/main_nav_graph.xml +++ b/stream-chat-android-ui-components-sample/src/main/res/navigation/main_nav_graph.xml @@ -224,6 +224,12 @@ app:destination="@id/groupChatInfoFragment" app:launchSingleTop="true" /> + + + + + + + + + Forwarded + Message details Message Flagged The message has been reported to a moderator. + + Hello blank fragment diff --git a/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api index 5f88a08f7c4..a0c944f1a57 100644 --- a/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api +++ b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api @@ -3324,6 +3324,10 @@ public final class io/getstream/chat/android/ui/feature/messages/list/options/me public final fun defaultFactory (Landroid/content/Context;)Lio/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory; } +public final class io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactoryKt { + public static final fun plus (Lio/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory;Lio/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory;)Lio/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory; +} + public final class io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionsDialogFragment : io/getstream/chat/android/ui/widgets/FullScreenDialogFragment { public static final field Companion Lio/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionsDialogFragment$Companion; public static final field TAG Ljava/lang/String; diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory.kt index 6630d75dce7..971eb081885 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory.kt @@ -73,6 +73,35 @@ public interface MessageOptionItemsFactory { } } +/** + * A factory that combines multiple [MessageOptionItemsFactory]s. + */ +private class CombinedMessageOptionItemsFactory( + private val factories: List, +) : MessageOptionItemsFactory { + + constructor(vararg factories: MessageOptionItemsFactory) : this(factories.toList()) + + override fun createMessageOptionItems( + selectedMessage: Message, + currentUser: User?, + isInThread: Boolean, + ownCapabilities: Set, + style: MessageListViewStyle, + ): List { + return factories.flatMap { + it.createMessageOptionItems(selectedMessage, currentUser, isInThread, ownCapabilities, style) + } + } +} + +/** + * Combines two [MessageOptionItemsFactory]s into a single [MessageOptionItemsFactory]. + */ +public operator fun MessageOptionItemsFactory.plus(that: MessageOptionItemsFactory): MessageOptionItemsFactory { + return CombinedMessageOptionItemsFactory(this, that) +} + /** * The default implementation of [MessageOptionItemsFactory]. *