Merge branch 'develop' into feature/beta_release_merge
This commit is contained in:
Коммит
063b0550d8
10
README.md
10
README.md
|
@ -34,13 +34,11 @@ android {
|
|||
```groovy
|
||||
dependencies {
|
||||
...
|
||||
implementation 'com.azure.android:azure-communication-ui-calling:<latest stable release version>'
|
||||
implementation 'com.azure.android:azure-communication-ui-calling:1.1.0'
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Please make sure to pick latest stable release version from our [Github Releases](https://github.com/Azure/communication-ui-library-android/releases)
|
||||
|
||||
In your project gradle scripts add following lines to `repositories`. For `Android Studio (2020.*)` the `repositories` are in `settings.gradle` `dependencyResolutionManagement(Gradle version 6.8 or greater)`. If you are using old versions of `Android Studio (4.*)` then the `repositories` will be in project level `build.gradle` `allprojects{}`.
|
||||
|
||||
```groovy
|
||||
|
@ -113,9 +111,9 @@ The snackbar on setup screen with error message may take more time to show up.
|
|||
## Contributing to the Library
|
||||
|
||||
Before developing and contributing to Communication Mobile UI Library, check out our [making a contribution guide](docs/contributing-guide.md).
|
||||
Included in this repository is a demo of using Mobile UI Library to start a call. You can find the detail of using and developing the UI Library in the [Demo Guide](azure-communication-ui/azure-communication-ui-demo-app).
|
||||
Included in this repository is a demo of using Mobile UI Library to start a call. You can find the detail of using and developing the UI Library in the [Demo Guide](azure-communication-ui/demo-app).
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. Also, please check our [Contribution Policy](CONTRIBUTING.md).
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. Also, please check our [Contribution Policy](docs/contributing-guide.md).
|
||||
|
||||
## Community Help and Support
|
||||
|
||||
|
@ -136,3 +134,5 @@ The chat experience is a work in progress, please be aware that chat and callwit
|
|||
* [Azure Communication Client and Server Architecture](https://docs.microsoft.com/en-us/azure/communication-services/concepts/client-and-server-architecture)
|
||||
* [Azure Communication Authentication](https://docs.microsoft.com/en-us/azure/communication-services/concepts/authentication)
|
||||
* [Azure Communication Service Troubleshooting](https://docs.microsoft.com/en-us/azure/communication-services/concepts/troubleshooting-info)
|
||||
* [Azure Communication Service UI Calling Library Maven Releases](https://search.maven.org/artifact/com.azure.android/azure-communication-ui-calling)
|
||||
* [Azure Communication Service Android Calling Hero Sample](https://github.com/Azure-Samples/communication-services-android-calling-hero)
|
||||
|
|
|
@ -7,7 +7,7 @@ public final class CallWithChatCompositeLocalOptions {
|
|||
private CallWithChatCompositeParticipantViewData participantViewData;
|
||||
|
||||
/**
|
||||
* Create Local Options.
|
||||
* Get {@link CallWithChatCompositeParticipantViewData}.
|
||||
*
|
||||
*/
|
||||
public CallWithChatCompositeLocalOptions() {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
package com.azure.android.communication.ui.callwithchat.models;
|
||||
|
||||
/**
|
||||
* Provides navigation bar view data to Call Composite including title and subtitle.
|
||||
*
|
||||
* Create an instance of {@link CallWithChatCompositeNavigationBarViewData} and pass it to
|
||||
* {@link CallWithChatCompositeLocalOptions} when launching a new call.
|
||||
*
|
||||
*/
|
||||
public final class CallWithChatCompositeNavigationBarViewData {
|
||||
private String callTitle = null;
|
||||
private String callSubtitle = null;
|
||||
|
||||
/**
|
||||
* Get the call title.
|
||||
* @return The title of the call.
|
||||
*/
|
||||
public String getCallTitle() {
|
||||
return callTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the call title of the call setup screen to the supplied String.
|
||||
* @param callTitle Title of the call.
|
||||
* @return The current {@link CallWithChatCompositeNavigationBarViewData}.
|
||||
*/
|
||||
public CallWithChatCompositeNavigationBarViewData setCallTitle(final String callTitle) {
|
||||
this.callTitle = callTitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the call sub title.
|
||||
* @return The subtitle of the call.
|
||||
*/
|
||||
public String getCallSubtitle() {
|
||||
return callSubtitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the subtitle of the call setup screen to the supplied String.
|
||||
* @param callSubtitle Subtitle of the call.
|
||||
* @return The current {@link CallWithChatCompositeNavigationBarViewData}.
|
||||
*/
|
||||
public CallWithChatCompositeNavigationBarViewData setCallSubtitle(final String callSubtitle) {
|
||||
this.callSubtitle = callSubtitle;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1515,7 +1515,6 @@ internal class CallingMiddlewareActionHandlerUnitTest : ACSBaseTestCoroutine() {
|
|||
MutableSharedFlow<MutableMap<String, ParticipantInfoModel>>()
|
||||
val callInfoModelStateFlow =
|
||||
MutableStateFlow(CallInfoModel(CallingStatus.LOCAL_HOLD, null))
|
||||
|
||||
val callIdFlow = MutableStateFlow<String?>(null)
|
||||
val isMutedSharedFlow = MutableSharedFlow<Boolean>()
|
||||
val isRecordingSharedFlow = MutableSharedFlow<Boolean>()
|
||||
|
@ -1526,6 +1525,7 @@ internal class CallingMiddlewareActionHandlerUnitTest : ACSBaseTestCoroutine() {
|
|||
val mockCallingService: CallingService = mock {
|
||||
on { getParticipantsInfoModelSharedFlow() } doReturn callingServiceParticipantsSharedFlow
|
||||
on { startCall(any(), any()) } doReturn CompletableFuture<Void>()
|
||||
on { getCallIdStateFlow() } doReturn callIdFlow
|
||||
on { getCallInfoModelEventSharedFlow() } doReturn callInfoModelStateFlow
|
||||
on { getCallIdStateFlow() } doReturn callIdFlow
|
||||
on { getCallInfoModelEventSharedFlow() } doReturn callInfoModelStateFlow
|
||||
|
|
|
@ -91,6 +91,9 @@ dependencies {
|
|||
debugImplementation "androidx.customview:customview:1.2.0-alpha01"
|
||||
debugImplementation "androidx.customview:customview-poolingcontainer:1.0.0-alpha01"
|
||||
|
||||
api ("com.azure.android:azure-communication-chat:$azure_chat_sdk_version")
|
||||
api ("com.azure.android:azure-communication-common:$azure_common_sdk_version")
|
||||
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
testImplementation "org.mockito:mockito-inline:$mockito_inline_version"
|
||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockito_kotlin_version"
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
android:exported="false"
|
||||
android:label="@string/azure_communication_ui_chat_title_activity_compose_chat"
|
||||
android:theme="@style/AzureCommunicationUIChat.Theme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
/>
|
||||
</application>
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.azure.android.communication.ui.chat.presentation.ChatCompositeActivit
|
|||
import com.azure.android.communication.ui.chat.presentation.ui.container.ChatView;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Azure android communication chat composite component.
|
||||
*
|
||||
|
|
|
@ -54,7 +54,6 @@ internal class ChatContainer(
|
|||
context: Context,
|
||||
remoteOptions: ChatCompositeRemoteOptions,
|
||||
localOptions: ChatCompositeLocalOptions?,
|
||||
|
||||
) {
|
||||
// currently only single instance is supported
|
||||
if (!started) {
|
||||
|
|
|
@ -13,8 +13,8 @@ import org.threeten.bp.OffsetDateTime
|
|||
internal data class MessageInfoModel(
|
||||
val id: String?,
|
||||
val internalId: String? = null,
|
||||
val messageType: ChatMessageType?,
|
||||
val content: String?,
|
||||
val messageType: ChatMessageType? = null,
|
||||
val content: String? = null,
|
||||
val topic: String? = null,
|
||||
val participants: List<String> = emptyList(),
|
||||
val version: String? = null,
|
||||
|
@ -43,7 +43,7 @@ internal fun com.azure.android.communication.chat.models.ChatMessage.into(): Mes
|
|||
)
|
||||
}
|
||||
|
||||
internal fun com.azure.android.communication.chat.models.ChatMessageReceivedEvent.into(): MessageInfoModel {
|
||||
internal fun com.azure.android.communication.chat.models.ChatMessageReceivedEvent.into(localParticipantIdentifier: String): MessageInfoModel {
|
||||
return MessageInfoModel(
|
||||
internalId = null,
|
||||
id = this.id,
|
||||
|
@ -55,11 +55,12 @@ internal fun com.azure.android.communication.chat.models.ChatMessageReceivedEven
|
|||
senderDisplayName = this.senderDisplayName,
|
||||
createdOn = this.createdOn,
|
||||
deletedOn = null,
|
||||
editedOn = null
|
||||
editedOn = null,
|
||||
isCurrentUser = localParticipantIdentifier == this.sender.into().id,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun com.azure.android.communication.chat.models.ChatMessageEditedEvent.into(): MessageInfoModel {
|
||||
internal fun com.azure.android.communication.chat.models.ChatMessageEditedEvent.into(localParticipantIdentifier: String): MessageInfoModel {
|
||||
return MessageInfoModel(
|
||||
internalId = null,
|
||||
id = this.id,
|
||||
|
@ -71,11 +72,12 @@ internal fun com.azure.android.communication.chat.models.ChatMessageEditedEvent.
|
|||
senderDisplayName = this.senderDisplayName,
|
||||
createdOn = this.createdOn,
|
||||
deletedOn = null,
|
||||
editedOn = this.editedOn
|
||||
editedOn = this.editedOn,
|
||||
isCurrentUser = localParticipantIdentifier == this.sender.into().id,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun com.azure.android.communication.chat.models.ChatMessageDeletedEvent.into(): MessageInfoModel {
|
||||
internal fun com.azure.android.communication.chat.models.ChatMessageDeletedEvent.into(localParticipantIdentifier: String): MessageInfoModel {
|
||||
return MessageInfoModel(
|
||||
internalId = null,
|
||||
id = this.id,
|
||||
|
@ -87,7 +89,8 @@ internal fun com.azure.android.communication.chat.models.ChatMessageDeletedEvent
|
|||
senderDisplayName = this.senderDisplayName,
|
||||
createdOn = this.createdOn,
|
||||
deletedOn = this.deletedOn,
|
||||
editedOn = null
|
||||
editedOn = null,
|
||||
isCurrentUser = localParticipantIdentifier == this.sender.into().id,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -103,4 +106,7 @@ internal val EMPTY_MESSAGE_INFO_MODEL = MessageInfoModel(
|
|||
senderCommunicationIdentifier = null,
|
||||
deletedOn = null,
|
||||
editedOn = null,
|
||||
isCurrentUser = false
|
||||
)
|
||||
|
||||
internal const val INVALID_INDEX = -1
|
||||
|
|
|
@ -15,17 +15,22 @@ import androidx.compose.ui.unit.sp
|
|||
@Immutable
|
||||
internal data class ChatCompositeDimensions(
|
||||
@Dimension
|
||||
val messageBubbleLeftSpacing: Dp = 48.dp,
|
||||
val messageAvatarSize: Dp = 24.dp,
|
||||
// Left Rail where Avatar is
|
||||
val messageAvatarRailWidth: Dp = 32.dp,
|
||||
val messageReceiptRailWidth: Dp = 20.dp,
|
||||
val messageUsernamePaddingEnd: Dp = 8.dp,
|
||||
val messagePadding: PaddingValues = PaddingValues(start = 10.dp, end = 10.dp, top = 8.dp, bottom = 8.dp),
|
||||
val systemMessagePadding: PaddingValues = PaddingValues(start = 20.dp, end = 5.dp, top = 10.dp, bottom = 10.dp),
|
||||
val messageOuterPadding: PaddingValues = PaddingValues(start = 0.dp, end = 0.dp, top = 1.dp, bottom = 1.dp),
|
||||
val messageInnerPadding: PaddingValues = PaddingValues(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 8.dp),
|
||||
val systemMessagePadding: PaddingValues = PaddingValues(start = 20.dp, end = 5.dp, top = 8.dp, bottom = 8.dp),
|
||||
val typingIndicatorAreaHeight: Dp = 36.dp,
|
||||
val unreadMessagesIndicatorHeight: Dp = 48.dp,
|
||||
val unreadMessagesIndicatorIconHeight: Dp = 18.dp,
|
||||
val unreadMessagesIndicatorIconPadding: PaddingValues = PaddingValues(start = 10.dp, end = 0.dp, top = 2.dp, bottom = 0.dp),
|
||||
val unreadMessagesIndicatorPadding: PaddingValues = PaddingValues(start = 0.dp, end = 0.dp, top = 0.dp, bottom = 4.dp),
|
||||
val unreadMessagesIndicatorIconPadding: PaddingValues = PaddingValues(start = 10.dp, end = 0.dp, top = 0.dp, bottom = 0.dp),
|
||||
val unreadMessagesIndicatorTextFontSize: TextUnit = 16.sp,
|
||||
val dateHeaderPadding: PaddingValues = PaddingValues(start = 0.dp, end = 0.dp, top = 16.dp, bottom = 0.dp)
|
||||
val dateHeaderPadding: PaddingValues = PaddingValues(start = 0.dp, end = 0.dp, top = 12.dp, bottom = 4.dp),
|
||||
val messageAvatarPadding: PaddingValues = PaddingValues(start = 0.dp, end = 4.dp, top = 0.dp, bottom = 0.dp),
|
||||
val messageRead: PaddingValues = PaddingValues(start = 3.dp, end = 4.99.dp, top = 3.dp, bottom = 3.dp)
|
||||
)
|
||||
|
||||
internal val LocalChatCompositeDimensions = staticCompositionLocalOf {
|
||||
|
|
|
@ -5,6 +5,7 @@ package com.azure.android.communication.ui.chat.presentation.ui.chat.components
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
@ -19,17 +20,21 @@ internal fun AvatarView(
|
|||
avatarSize: AvatarSize = AvatarSize.LARGE,
|
||||
@DrawableRes image: Int = -1,
|
||||
isGrouped: Boolean = false,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
AndroidView(factory = {
|
||||
val view = AvatarView(it)
|
||||
view.name = name ?: ""
|
||||
view.avatarSize = avatarSize
|
||||
color?.apply {
|
||||
view.avatarBackgroundColor = toArgb()
|
||||
}
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = {
|
||||
val view = AvatarView(it)
|
||||
view.name = name ?: ""
|
||||
view.avatarSize = avatarSize
|
||||
color?.apply {
|
||||
view.avatarBackgroundColor = toArgb()
|
||||
}
|
||||
|
||||
view
|
||||
})
|
||||
view
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
|
@ -45,7 +45,8 @@ internal fun BottomBarView(
|
|||
|
||||
SendMessageButtonView(
|
||||
contentDescription = stringResource(R.string.azure_communication_ui_chat_message_send_button_content_description, messageInputTextState.value),
|
||||
chatStatus = chatStatus
|
||||
chatStatus = chatStatus,
|
||||
clickable = messageInputTextState.value.isNotBlank()
|
||||
) {
|
||||
sendButtonOnclick(postAction, messageInputTextState)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
|
@ -26,7 +25,6 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.azure.android.communication.ui.chat.R
|
||||
import com.azure.android.communication.ui.chat.presentation.style.ChatCompositeTheme
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -35,6 +33,8 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.azure.android.communication.ui.chat.R
|
||||
import com.azure.android.communication.ui.chat.presentation.ui.chat.UITestTags
|
||||
import com.azure.android.communication.ui.chat.redux.action.Action
|
||||
import com.azure.android.communication.ui.chat.redux.action.ChatAction
|
||||
|
|
|
@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
|
@ -44,13 +44,15 @@ internal fun MessageListView(
|
|||
requestPages(scrollState, messages, dispatchers)
|
||||
if (messages.isNotEmpty()) {
|
||||
sendReadReceipt(scrollState, messages, dispatchers)
|
||||
autoScrollToBottom(scrollState, messages)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxHeight(),
|
||||
state = scrollState,
|
||||
reverseLayout = true,
|
||||
) {
|
||||
items(messages.asReversed()) { message ->
|
||||
itemsIndexed(messages.asReversed(), key = { index, item -> item.message.id ?: index }) { index, message ->
|
||||
MessageView(message)
|
||||
}
|
||||
if (messages.isNotEmpty() && showLoading) {
|
||||
|
@ -91,6 +93,21 @@ private fun requestPages(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun autoScrollToBottom(
|
||||
scrollState: LazyListState,
|
||||
messages: List<MessageViewModel>
|
||||
) {
|
||||
val wasAtEnd = remember { mutableStateOf(scrollState.firstVisibleItemIndex) }
|
||||
val isAtEnd = scrollState.firstVisibleItemIndex
|
||||
if (wasAtEnd.value == 0 && wasAtEnd.value != isAtEnd) {
|
||||
LaunchedEffect(messages.last()) {
|
||||
scrollState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
wasAtEnd.value = isAtEnd
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun sendReadReceipt(
|
||||
scrollState: LazyListState,
|
||||
|
|
|
@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.width
|
||||
|
||||
|
@ -33,15 +32,20 @@ import com.azure.android.communication.ui.chat.preview.MOCK_LOCAL_USER_ID
|
|||
import com.azure.android.communication.ui.chat.preview.MOCK_MESSAGES
|
||||
import com.azure.android.communication.ui.chat.service.sdk.wrapper.ChatMessageType
|
||||
import com.jakewharton.threetenabp.AndroidThreeTen
|
||||
import com.microsoft.fluentui.persona.AvatarSize
|
||||
import org.threeten.bp.OffsetDateTime
|
||||
import org.threeten.bp.format.DateTimeFormatter
|
||||
|
||||
val timeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("h:m a")
|
||||
val timeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("h:mm a")
|
||||
|
||||
@Composable
|
||||
internal fun MessageView(viewModel: MessageViewModel) {
|
||||
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.padding(ChatCompositeTheme.dimensions.messageOuterPadding),
|
||||
) {
|
||||
|
||||
// Date Header Part
|
||||
if (viewModel.dateHeaderText != null) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
|
@ -64,6 +68,7 @@ internal fun MessageView(viewModel: MessageViewModel) {
|
|||
icon = R.drawable.azure_communication_ui_chat_ic_topic_changed_filled, /* TODO: update icon */
|
||||
stringResource = R.string.azure_communication_ui_chat_topic_updated,
|
||||
substitution = listOf(viewModel.message.topic ?: "Unknown")
|
||||
|
||||
)
|
||||
ChatMessageType.PARTICIPANT_ADDED -> SystemMessage(
|
||||
icon = R.drawable.azure_communication_ui_chat_ic_participant_added_filled,
|
||||
|
@ -88,13 +93,13 @@ internal fun MessageView(viewModel: MessageViewModel) {
|
|||
private fun SystemMessage(icon: Int, stringResource: Int, substitution: List<String>) {
|
||||
|
||||
val text = LocalContext.current.getString(stringResource, substitution.joinToString(", "))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(ChatCompositeTheme.dimensions.systemMessagePadding)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = "Participant Added",
|
||||
modifier = Modifier.padding(
|
||||
ChatCompositeTheme.dimensions.systemMessagePadding
|
||||
),
|
||||
contentDescription = text,
|
||||
tint = ChatCompositeTheme.colors.systemIconColor
|
||||
)
|
||||
BasicText(text = text, style = ChatCompositeTheme.typography.systemMessage)
|
||||
|
@ -103,58 +108,90 @@ private fun SystemMessage(icon: Int, stringResource: Int, substitution: List<Str
|
|||
|
||||
@Composable
|
||||
private fun BasicChatMessage(viewModel: MessageViewModel) {
|
||||
Row(
|
||||
Modifier.padding(2.dp),
|
||||
) {
|
||||
if (viewModel.isLocalUser) {
|
||||
Box(modifier = Modifier.weight(1.0f))
|
||||
}
|
||||
Box(modifier = Modifier.size(ChatCompositeTheme.dimensions.messageBubbleLeftSpacing)) {
|
||||
if (viewModel.showUsername) {
|
||||
AvatarView(name = viewModel.message.senderDisplayName)
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(modifier = Modifier.align(alignment = if (viewModel.isLocalUser) Alignment.TopEnd else Alignment.TopStart)) {
|
||||
// Avatar Rail (Left Padding)
|
||||
Box(modifier = Modifier.width(ChatCompositeTheme.dimensions.messageAvatarRailWidth)) {
|
||||
// Display the Avatar
|
||||
if (viewModel.showUsername) {
|
||||
AvatarView(
|
||||
name = viewModel.message.senderDisplayName,
|
||||
avatarSize = AvatarSize.SMALL,
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.TopEnd)
|
||||
.padding(ChatCompositeTheme.dimensions.messageAvatarPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.weight(1.0f)) {
|
||||
Box(
|
||||
Modifier.background(
|
||||
color = when (viewModel.isLocalUser) {
|
||||
true -> ChatCompositeTheme.colors.messageBackgroundSelf
|
||||
false -> ChatCompositeTheme.colors.messageBackground
|
||||
},
|
||||
shape = ChatCompositeTheme.shapes.messageBubble,
|
||||
).align(alignment = if (viewModel.isLocalUser) Alignment.TopEnd else Alignment.TopStart)
|
||||
) {
|
||||
messageContent(viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(ChatCompositeTheme.dimensions.messageReceiptRailWidth)
|
||||
.align(alignment = Alignment.Bottom)
|
||||
) {
|
||||
// Display the Read Receipt
|
||||
androidx.compose.animation.AnimatedVisibility(visible = viewModel.isRead) {
|
||||
Icon(
|
||||
painter =
|
||||
painterResource(
|
||||
id =
|
||||
R.drawable.azure_communication_ui_chat_ic_fluent_message_read_10_filled
|
||||
),
|
||||
contentDescription = "Message Read",
|
||||
tint = ChatCompositeTheme.colors.unreadMessageIndicatorBackground,
|
||||
modifier = Modifier.padding(start = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier.background(
|
||||
color = when (viewModel.isLocalUser) {
|
||||
true -> ChatCompositeTheme.colors.messageBackgroundSelf
|
||||
false -> ChatCompositeTheme.colors.messageBackground
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
shape = ChatCompositeTheme.shapes.messageBubble
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(ChatCompositeTheme.dimensions.messagePadding)
|
||||
) {
|
||||
Column {
|
||||
if (viewModel.showUsername || viewModel.showTime) {
|
||||
Row {
|
||||
if (viewModel.showUsername) {
|
||||
BasicText(
|
||||
viewModel.message.senderDisplayName ?: "Unknown Sender",
|
||||
style = ChatCompositeTheme.typography.messageHeader,
|
||||
modifier = Modifier.padding(PaddingValues(end = ChatCompositeTheme.dimensions.messageUsernamePaddingEnd))
|
||||
)
|
||||
}
|
||||
if (viewModel.showTime) {
|
||||
BasicText(
|
||||
viewModel.message.createdOn?.format(timeFormat)
|
||||
?: "Unknown Time",
|
||||
style = ChatCompositeTheme.typography.messageHeaderDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (viewModel.message.messageType == ChatMessageType.HTML) {
|
||||
HtmlText(html = viewModel.message.content ?: "Empty")
|
||||
} else {
|
||||
@Composable
|
||||
private fun messageContent(viewModel: MessageViewModel) {
|
||||
Box(
|
||||
modifier = Modifier.padding(ChatCompositeTheme.dimensions.messageInnerPadding)
|
||||
) {
|
||||
Column {
|
||||
if (viewModel.showUsername || viewModel.showTime) {
|
||||
Row {
|
||||
if (viewModel.showUsername) {
|
||||
BasicText(
|
||||
text = viewModel.message.content ?: "Empty"
|
||||
viewModel.message.senderDisplayName ?: "Unknown Sender",
|
||||
style = ChatCompositeTheme.typography.messageHeader,
|
||||
modifier = Modifier.padding(PaddingValues(end = ChatCompositeTheme.dimensions.messageUsernamePaddingEnd))
|
||||
)
|
||||
}
|
||||
if (viewModel.showTime) {
|
||||
BasicText(
|
||||
viewModel.message.createdOn?.format(timeFormat)
|
||||
?: "Unknown Time",
|
||||
style = ChatCompositeTheme.typography.messageHeaderDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (viewModel.message.messageType == ChatMessageType.HTML) {
|
||||
HtmlText(html = viewModel.message.content ?: "Empty")
|
||||
} else {
|
||||
BasicText(
|
||||
text = viewModel.message.content ?: "Empty"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -181,7 +218,11 @@ internal fun PreviewChatCompositeMessage() {
|
|||
.width(500.dp)
|
||||
.background(color = ChatCompositeTheme.colors.background)
|
||||
) {
|
||||
val vms = MOCK_MESSAGES.toViewModelList(LocalContext.current, MOCK_LOCAL_USER_ID)
|
||||
val vms = MOCK_MESSAGES.toViewModelList(
|
||||
LocalContext.current,
|
||||
MOCK_LOCAL_USER_ID,
|
||||
OffsetDateTime.now()
|
||||
)
|
||||
for (a in 0 until vms.size) {
|
||||
MessageView(vms[a])
|
||||
}
|
||||
|
|
|
@ -27,19 +27,20 @@ internal fun SendMessageButtonView(
|
|||
contentDescription: String,
|
||||
modifier: Modifier = Modifier,
|
||||
chatStatus: ChatStatus,
|
||||
clickable: Boolean = false,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
val semantics = Modifier.semantics {
|
||||
this.contentDescription = contentDescription
|
||||
this.role = Role.Image
|
||||
}
|
||||
val painter = if (chatStatus == ChatStatus.INITIALIZED)
|
||||
val painter = if (chatStatus == ChatStatus.INITIALIZED && clickable)
|
||||
painterResource(id = R.drawable.azure_communication_ui_chat_ic_fluent_send_message_button_20_filled_enabled)
|
||||
else
|
||||
painterResource(id = R.drawable.azure_communication_ui_chat_ic_fluent_send_message_button_20_filled_disabled)
|
||||
Box(
|
||||
modifier = Modifier.testTag(UITestTags.MESSAGE_SEND_BUTTON).clickable {
|
||||
if (chatStatus == ChatStatus.INITIALIZED) {
|
||||
if (chatStatus == ChatStatus.INITIALIZED && clickable) {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ internal fun UnreadMessagesIndicatorView(
|
|||
scrollState: LazyListState,
|
||||
visible: Boolean,
|
||||
unreadCount: Int,
|
||||
totalMessages: Int,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val content = LocalContext.current
|
||||
|
@ -39,7 +38,8 @@ internal fun UnreadMessagesIndicatorView(
|
|||
icon = {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.azure_communication_ui_chat_ic_fluent_arrow_down_16_filled),
|
||||
modifier = Modifier.height(ChatCompositeTheme.dimensions.unreadMessagesIndicatorIconHeight)
|
||||
modifier = Modifier
|
||||
.height(ChatCompositeTheme.dimensions.unreadMessagesIndicatorIconHeight)
|
||||
.padding(ChatCompositeTheme.dimensions.unreadMessagesIndicatorIconPadding),
|
||||
contentDescription = null
|
||||
)
|
||||
|
@ -47,15 +47,20 @@ internal fun UnreadMessagesIndicatorView(
|
|||
text = {
|
||||
Text(
|
||||
text = when (unreadCount) {
|
||||
1 -> content.getString(R.string.azure_communication_ui_chat_unread_new_messages)
|
||||
else -> content.getString(R.string.azure_communication_ui_chat_unread_new_messages, unreadCount.toString())
|
||||
in Int.MIN_VALUE..0 -> return@ExtendedFloatingActionButton
|
||||
1 -> content.getString(R.string.azure_communication_ui_chat_unread_new_message)
|
||||
in 2..99 -> content.getString(
|
||||
R.string.azure_communication_ui_chat_unread_new_messages,
|
||||
unreadCount.toString()
|
||||
)
|
||||
else -> content.getString(R.string.azure_communication_ui_chat_many_unread_new_messages)
|
||||
},
|
||||
fontSize = ChatCompositeTheme.dimensions.unreadMessagesIndicatorTextFontSize
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
scrollState.animateScrollToItem(totalMessages)
|
||||
scrollState.animateScrollToItem(0)
|
||||
}
|
||||
},
|
||||
backgroundColor = ChatCompositeTheme.colors.unreadMessageIndicatorBackground,
|
||||
|
@ -74,6 +79,5 @@ internal fun PreviewUnreadMessagesIndicatorView() {
|
|||
rememberLazyListState(),
|
||||
visible = true,
|
||||
unreadCount = 20,
|
||||
totalMessages = 30,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
package com.azure.android.communication.ui.chat.presentation.ui.chat.screens
|
||||
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
|
@ -86,26 +86,36 @@ internal fun ChatScreen(
|
|||
FluentCircularIndicator()
|
||||
}
|
||||
} else {
|
||||
MessageListView(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxWidth(),
|
||||
messages = viewModel.messages,
|
||||
scrollState = listState,
|
||||
showLoading = viewModel.areMessagesLoading,
|
||||
dispatchers = viewModel.postAction
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
MessageListView(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxWidth(),
|
||||
messages = viewModel.messages,
|
||||
scrollState = listState,
|
||||
showLoading = viewModel.areMessagesLoading,
|
||||
dispatchers = viewModel.postAction
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.padding(ChatCompositeTheme.dimensions.unreadMessagesIndicatorPadding)
|
||||
) {
|
||||
UnreadMessagesIndicatorView(
|
||||
scrollState = listState,
|
||||
visible = viewModel.unreadMessagesIndicatorVisibility,
|
||||
unreadCount = viewModel.unreadMessagesCount,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
UnreadMessagesIndicatorView(
|
||||
scrollState = listState,
|
||||
visible = viewModel.unreadMessagesIndicatorVisibility,
|
||||
unreadCount = viewModel.unreadMessagesCount,
|
||||
totalMessages = viewModel.messages.size/* TODO ViewModelLogic */
|
||||
)
|
||||
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
TypingIndicatorView(viewModel.typingParticipants.toList())
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import com.azure.android.communication.ui.chat.redux.action.Action
|
|||
import com.azure.android.communication.ui.chat.redux.state.ChatStatus
|
||||
import com.azure.android.communication.ui.chat.redux.state.NavigationStatus
|
||||
import com.azure.android.communication.ui.chat.redux.state.ReduxState
|
||||
import kotlin.math.max
|
||||
|
||||
// View Model for the Chat Screen
|
||||
internal data class ChatScreenViewModel(
|
||||
|
@ -46,15 +47,12 @@ internal fun buildChatScreenViewModel(
|
|||
dispatch: Dispatch,
|
||||
): ChatScreenViewModel {
|
||||
|
||||
// TODO add logic with last read message
|
||||
var unreadMessagesCount: Int = 0
|
||||
|
||||
return ChatScreenViewModel(
|
||||
messages = messages.toViewModelList(context, localUserIdentifier),
|
||||
messages = messages.toViewModelList(context, localUserIdentifier, store.getCurrentState().participantState.latestReadMessageTimestamp),
|
||||
areMessagesLoading = !store.getCurrentState().chatState.chatInfoModel.allMessagesFetched,
|
||||
chatStatus = store.getCurrentState().chatState.chatStatus,
|
||||
buildCount = buildCount++,
|
||||
unreadMessagesCount = unreadMessagesCount,
|
||||
unreadMessagesCount = getUnReadMessagesCount(store, messages),
|
||||
error = store.getCurrentState().errorState.chatStateError,
|
||||
postAction = dispatch,
|
||||
typingParticipants = store.getCurrentState().participantState.participantTyping.values.toList(),
|
||||
|
@ -63,3 +61,18 @@ internal fun buildChatScreenViewModel(
|
|||
navigationStatus = store.getCurrentState().navigationState.navigationStatus,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getUnReadMessagesCount(
|
||||
store: AppStore<ReduxState>,
|
||||
messages: List<MessageInfoModel>,
|
||||
): Int {
|
||||
val lastReadId = store.getCurrentState().chatState.lastReadMessageId
|
||||
val lastSendId = store.getCurrentState().chatState.lastSendMessageId
|
||||
|
||||
val internalLastReadIndex = messages.indexOf(MessageInfoModel(id = lastReadId))
|
||||
val internalLastSendIndex = messages.indexOf(MessageInfoModel(id = lastSendId))
|
||||
|
||||
val internalLastIndex = max(internalLastReadIndex, internalLastSendIndex)
|
||||
|
||||
return if (internalLastIndex == -1) 0 else messages.size - internalLastIndex - 1
|
||||
}
|
||||
|
|
|
@ -21,15 +21,17 @@ internal class MessageViewModel(
|
|||
val showTime: Boolean,
|
||||
val dateHeaderText: String?,
|
||||
val isLocalUser: Boolean,
|
||||
val isRead: Boolean
|
||||
)
|
||||
|
||||
internal fun List<MessageInfoModel>.toViewModelList(context: Context, localUserIdentifier: String) =
|
||||
InfoModelToViewModelAdapter(context, this, localUserIdentifier) as List<MessageViewModel>
|
||||
internal fun List<MessageInfoModel>.toViewModelList(context: Context, localUserIdentifier: String, latestReadMessageTimestamp: OffsetDateTime = OffsetDateTime.MIN) =
|
||||
InfoModelToViewModelAdapter(context, this, localUserIdentifier, latestReadMessageTimestamp) as List<MessageViewModel>
|
||||
|
||||
private class InfoModelToViewModelAdapter(
|
||||
private val context: Context,
|
||||
private val messages: List<MessageInfoModel>,
|
||||
private val localUserIdentifier: String
|
||||
private val localUserIdentifier: String,
|
||||
private val latestReadMessageTimestamp: OffsetDateTime
|
||||
) :
|
||||
List<MessageViewModel> {
|
||||
|
||||
|
@ -37,8 +39,10 @@ private class InfoModelToViewModelAdapter(
|
|||
// Generate Message View Model here
|
||||
|
||||
val lastMessage = try { messages[index - 1] } catch (e: IndexOutOfBoundsException) { EMPTY_MESSAGE_INFO_MODEL }
|
||||
// val lastLocalUserMessage =
|
||||
val thisMessage = messages[index]
|
||||
val isLocalUser = thisMessage.senderCommunicationIdentifier?.id == localUserIdentifier || thisMessage.isCurrentUser
|
||||
val currentMessageTime = thisMessage.editedOn ?: thisMessage.createdOn
|
||||
return MessageViewModel(
|
||||
|
||||
messages[index],
|
||||
|
@ -55,7 +59,8 @@ private class InfoModelToViewModelAdapter(
|
|||
thisMessage.createdOn ?: OffsetDateTime.now()
|
||||
),
|
||||
|
||||
isLocalUser = isLocalUser
|
||||
isLocalUser = isLocalUser,
|
||||
isRead = isLocalUser && (currentMessageTime != null && currentMessageTime <= latestReadMessageTimestamp)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -45,11 +45,31 @@ internal val MOCK_MESSAGES get(): List<MessageInfoModel> {
|
|||
senderDisplayName = userB_Display,
|
||||
content = "Hi Peter, thanks for following up with me",
|
||||
messageType = ChatMessageType.TEXT,
|
||||
id = null,
|
||||
id = OffsetDateTime.now().toString(),
|
||||
internalId = null,
|
||||
createdOn = OffsetDateTime.now().minusDays(1).minusMinutes(12)
|
||||
),
|
||||
|
||||
MessageInfoModel(
|
||||
senderCommunicationIdentifier = userB_ID,
|
||||
senderDisplayName = userB_Display,
|
||||
content = "I like to type",
|
||||
messageType = ChatMessageType.TEXT,
|
||||
id = null,
|
||||
internalId = null,
|
||||
createdOn = OffsetDateTime.now().minusDays(1).minusMinutes(11)
|
||||
),
|
||||
|
||||
MessageInfoModel(
|
||||
senderCommunicationIdentifier = userB_ID,
|
||||
senderDisplayName = userB_Display,
|
||||
content = "a lot",
|
||||
messageType = ChatMessageType.TEXT,
|
||||
id = null,
|
||||
internalId = null,
|
||||
createdOn = OffsetDateTime.now().minusDays(1).minusMinutes(10)
|
||||
),
|
||||
|
||||
MessageInfoModel(
|
||||
content = null,
|
||||
messageType = ChatMessageType.PARTICIPANT_ADDED,
|
||||
|
|
|
@ -28,8 +28,14 @@ internal class ChatReducerImpl : ChatReducer {
|
|||
is ChatAction.ThreadDeleted -> {
|
||||
state.copy(chatInfoModel = state.chatInfoModel.copy(isThreadDeleted = true))
|
||||
}
|
||||
is ChatAction.MessageSent -> {
|
||||
state.copy(lastSendMessageId = action.messageInfoModel.id ?: "")
|
||||
}
|
||||
is ChatAction.MessageRead -> {
|
||||
state.copy(lastReadMessageId = action.messageId)
|
||||
state.copy(
|
||||
lastReadMessageId = if (state.lastReadMessageId> action.messageId) state.lastReadMessageId
|
||||
else action.messageId
|
||||
)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
|
|
|
@ -24,7 +24,8 @@ internal class AppReduxState(
|
|||
allMessagesFetched = false,
|
||||
isThreadDeleted = false
|
||||
),
|
||||
lastReadMessageId = ""
|
||||
lastReadMessageId = "",
|
||||
lastSendMessageId = "",
|
||||
)
|
||||
|
||||
override var participantState: ParticipantsState = ParticipantsState(
|
||||
|
|
|
@ -19,4 +19,5 @@ internal data class ChatState(
|
|||
val localParticipantInfoModel: LocalParticipantInfoModel,
|
||||
val chatInfoModel: ChatInfoModel,
|
||||
val lastReadMessageId: String,
|
||||
val lastSendMessageId: String,
|
||||
)
|
||||
|
|
|
@ -23,6 +23,7 @@ internal class MessageRepository private constructor(
|
|||
override fun addServerMessage(message: MessageInfoModel) = writerDelegate.addServerMessage(message = message)
|
||||
override fun removeMessage(message: MessageInfoModel) = writerDelegate.removeMessage(message = message)
|
||||
override fun editMessage(message: MessageInfoModel) = writerDelegate.editMessage(message = message)
|
||||
override fun indexOf(element: MessageInfoModel) = readerDelegate.indexOf(element)
|
||||
|
||||
// TODO: We should be using read interface to get last message in list
|
||||
// This isn't a write message
|
||||
|
|
|
@ -18,6 +18,7 @@ internal abstract class MessageRepositoryReader : List<MessageInfoModel> {
|
|||
override fun isEmpty(): Boolean {
|
||||
return size == 0
|
||||
}
|
||||
|
||||
final override fun contains(element: MessageInfoModel): Boolean {
|
||||
throw RuntimeException("Not implemented on the Message Repository")
|
||||
}
|
||||
|
@ -26,10 +27,6 @@ internal abstract class MessageRepositoryReader : List<MessageInfoModel> {
|
|||
throw RuntimeException("Not implemented on the Message Repository")
|
||||
}
|
||||
|
||||
final override fun indexOf(element: MessageInfoModel): Int {
|
||||
throw RuntimeException("Not implemented on the Message Repository")
|
||||
}
|
||||
|
||||
final override fun iterator(): Iterator<MessageInfoModel> {
|
||||
throw RuntimeException("Not implemented on the Message Repository")
|
||||
}
|
||||
|
|
|
@ -80,4 +80,16 @@ internal class MessageRepositoryListReader(private val writer: MessageRepository
|
|||
}
|
||||
|
||||
override val size: Int get() = writer.messages.size
|
||||
|
||||
override fun indexOf(element: MessageInfoModel): Int {
|
||||
val messageId = element.id!!.toLong()
|
||||
var index = 0
|
||||
for (message in writer.messages) {
|
||||
if (messageId == message.id!!.toLong()) {
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package com.azure.android.communication.ui.chat.repository.storage
|
||||
|
||||
import com.azure.android.communication.ui.chat.models.EMPTY_MESSAGE_INFO_MODEL
|
||||
import com.azure.android.communication.ui.chat.models.INVALID_INDEX
|
||||
import com.azure.android.communication.ui.chat.models.MessageInfoModel
|
||||
import com.azure.android.communication.ui.chat.repository.MessageRepositoryReader
|
||||
import com.azure.android.communication.ui.chat.repository.MessageRepositoryWriter
|
||||
|
@ -82,6 +83,25 @@ internal class MessageRepositorySkipListWriter : MessageRepositoryWriter {
|
|||
return skipListStorage.get(key)!!
|
||||
}
|
||||
|
||||
fun searchIndexByID(messageId: Long): Int {
|
||||
var highestKey = skipListStorage.lastKey()
|
||||
var lowestKey = skipListStorage.firstKey()
|
||||
var midKey: Long = 0
|
||||
|
||||
while (lowestKey <= highestKey) {
|
||||
midKey = (lowestKey + highestKey).div(2)
|
||||
|
||||
if (messageId < midKey) {
|
||||
highestKey = midKey - 1
|
||||
} else if (messageId > midKey) {
|
||||
lowestKey = midKey + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return skipListStorage.headMap(midKey).size
|
||||
}
|
||||
|
||||
private fun getOrderId(message: MessageInfoModel): Long {
|
||||
return message.id?.toLong() ?: 0L
|
||||
}
|
||||
|
@ -117,4 +137,10 @@ internal class MessageRepositorySkipListReader(private val writer: MessageReposi
|
|||
} catch (exception: Exception) {
|
||||
EMPTY_MESSAGE_INFO_MODEL
|
||||
}
|
||||
|
||||
override fun indexOf(element: MessageInfoModel): Int = try {
|
||||
writer.searchIndexByID(element.id!!.toLong())
|
||||
} catch (exception: Exception) {
|
||||
INVALID_INDEX
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package com.azure.android.communication.ui.chat.repository.storage
|
||||
|
||||
import com.azure.android.communication.ui.chat.models.EMPTY_MESSAGE_INFO_MODEL
|
||||
import com.azure.android.communication.ui.chat.models.INVALID_INDEX
|
||||
import com.azure.android.communication.ui.chat.models.MessageInfoModel
|
||||
import com.azure.android.communication.ui.chat.repository.MessageRepositoryReader
|
||||
import com.azure.android.communication.ui.chat.repository.MessageRepositoryWriter
|
||||
|
@ -82,6 +83,25 @@ internal class MessageRepositoryTreeWriter : MessageRepositoryWriter {
|
|||
return treeMapStorage.get(key)!!
|
||||
}
|
||||
|
||||
fun searchIndexByID(messageId: Long): Int {
|
||||
var highestKey = treeMapStorage.lastKey()
|
||||
var lowestKey = treeMapStorage.firstKey()
|
||||
var midKey: Long = 0
|
||||
|
||||
while (lowestKey <= highestKey) {
|
||||
midKey = (lowestKey + highestKey).div(2)
|
||||
|
||||
if (messageId < midKey) {
|
||||
highestKey = midKey - 1
|
||||
} else if (messageId > midKey) {
|
||||
lowestKey = midKey + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return treeMapStorage.headMap(midKey).size
|
||||
}
|
||||
|
||||
private fun getOrderId(message: MessageInfoModel): Long {
|
||||
return message.id?.toLong() ?: 0L
|
||||
}
|
||||
|
@ -117,4 +137,10 @@ internal class MessageRepositoryTreeReader(private val writer: MessageRepository
|
|||
} catch (exception: Exception) {
|
||||
EMPTY_MESSAGE_INFO_MODEL
|
||||
}
|
||||
|
||||
override fun indexOf(element: MessageInfoModel): Int = try {
|
||||
writer.searchIndexByID(element.id!!.toLong())
|
||||
} catch (exception: Exception) {
|
||||
INVALID_INDEX
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ internal class ChatEventHandler {
|
|||
val event = chatEvent as ChatMessageReceivedEvent
|
||||
val infoModel = ChatEventModel(
|
||||
eventType = ChatEventType.CHAT_MESSAGE_RECEIVED.into(),
|
||||
infoModel = event.into(),
|
||||
infoModel = event.into(localParticipantIdentifier),
|
||||
eventReceivedOffsetDateTime = event.createdOn
|
||||
)
|
||||
eventSubscriber(infoModel)
|
||||
|
@ -118,7 +118,7 @@ internal class ChatEventHandler {
|
|||
val event = chatEvent as ChatMessageEditedEvent
|
||||
val infoModel = ChatEventModel(
|
||||
eventType = ChatEventType.CHAT_MESSAGE_EDITED.into(),
|
||||
infoModel = event.into(),
|
||||
infoModel = event.into(localParticipantIdentifier),
|
||||
eventReceivedOffsetDateTime = event.editedOn
|
||||
)
|
||||
eventSubscriber(infoModel)
|
||||
|
@ -127,7 +127,7 @@ internal class ChatEventHandler {
|
|||
val event = chatEvent as ChatMessageDeletedEvent
|
||||
val infoModel = ChatEventModel(
|
||||
eventType = ChatEventType.CHAT_MESSAGE_DELETED.into(),
|
||||
infoModel = event.into(),
|
||||
infoModel = event.into(localParticipantIdentifier),
|
||||
eventReceivedOffsetDateTime = event.deletedOn
|
||||
)
|
||||
eventSubscriber(infoModel)
|
||||
|
|
|
@ -6,11 +6,12 @@
|
|||
<string name="azure_communication_ui_chat_chat_action_bar_title">Chat</string>
|
||||
<string name="azure_communication_ui_chat_title_activity_compose_chat">ComposeChatActivity</string>
|
||||
<string name="azure_communication_ui_chat_demo_app_title">Chat Composite Demo App</string>
|
||||
<string name="azure_communication_ui_chat_enter_a_message">Hey, whats up?</string>
|
||||
<string name="azure_communication_ui_chat_enter_a_message">Type a new message</string>
|
||||
<string name="azure_communication_ui_chat_other">other</string>
|
||||
<string name="azure_communication_ui_chat_others">others</string>
|
||||
<string name="azure_communication_ui_chat_unread_new_message">1 new message</string>
|
||||
<string name="azure_communication_ui_chat_unread_new_messages">%1$s new messages</string>
|
||||
<string name="azure_communication_ui_chat_many_unread_new_messages">99+ new messages</string>
|
||||
<string name="azure_communication_ui_chat_first_name_is_typing">%1$s is typing</string>
|
||||
<string name="azure_communication_ui_chat_two_names_are_typing">%1$s and %2$s are typing"</string>
|
||||
<string name="azure_communication_ui_chat_three_or_more_are_typing">%1$s, %2$s and %3$d %4$s are typing</string>
|
||||
|
|
|
@ -22,7 +22,8 @@ internal class ChatReducerUnitTest {
|
|||
val reducer = ChatReducerImpl()
|
||||
val localParticipantInfoModel = mock<LocalParticipantInfoModel> { }
|
||||
val chatInfoModel = mock<ChatInfoModel>()
|
||||
val previousState = ChatState(ChatStatus.NONE, localParticipantInfoModel, chatInfoModel, "")
|
||||
val previousState =
|
||||
ChatState(ChatStatus.NONE, localParticipantInfoModel, chatInfoModel, "", "")
|
||||
val action = ChatAction.Initialization()
|
||||
|
||||
// act
|
||||
|
@ -38,7 +39,8 @@ internal class ChatReducerUnitTest {
|
|||
val reducer = ChatReducerImpl()
|
||||
val localParticipantInfoModel = mock<LocalParticipantInfoModel> { }
|
||||
val chatInfoModel = mock<ChatInfoModel>()
|
||||
val previousState = ChatState(ChatStatus.NONE, localParticipantInfoModel, chatInfoModel, "")
|
||||
val previousState =
|
||||
ChatState(ChatStatus.NONE, localParticipantInfoModel, chatInfoModel, "", "")
|
||||
val action = ChatAction.Initialized()
|
||||
|
||||
// act
|
||||
|
@ -54,10 +56,12 @@ internal class ChatReducerUnitTest {
|
|||
val reducer = ChatReducerImpl()
|
||||
val localParticipantInfoModel = mock<LocalParticipantInfoModel> { }
|
||||
val chatInfoModel = ChatInfoModel(threadId = "", topic = "Previous Chat topic")
|
||||
val previousState = ChatState(ChatStatus.NONE, localParticipantInfoModel, chatInfoModel, "")
|
||||
val previousState =
|
||||
ChatState(ChatStatus.NONE, localParticipantInfoModel, chatInfoModel, "", "")
|
||||
val action = ChatAction.TopicUpdated("New Chat topic")
|
||||
val afterChatInfoModel = ChatInfoModel(threadId = "", topic = "New Chat topic")
|
||||
val afterState = ChatState(ChatStatus.NONE, localParticipantInfoModel, afterChatInfoModel, "")
|
||||
val afterState =
|
||||
ChatState(ChatStatus.NONE, localParticipantInfoModel, afterChatInfoModel, "", "")
|
||||
|
||||
// act
|
||||
val newState = reducer.reduce(previousState, action)
|
||||
|
@ -72,10 +76,12 @@ internal class ChatReducerUnitTest {
|
|||
val reducer = ChatReducerImpl()
|
||||
val localParticipantInfoModel = mock<LocalParticipantInfoModel> { }
|
||||
val chatInfoModel = ChatInfoModel(threadId = "", topic = "", allMessagesFetched = false)
|
||||
val previousState = ChatState(ChatStatus.NONE, localParticipantInfoModel, chatInfoModel, "")
|
||||
val previousState =
|
||||
ChatState(ChatStatus.NONE, localParticipantInfoModel, chatInfoModel, "", "")
|
||||
val action = ChatAction.AllMessagesFetched()
|
||||
val afterChatInfoModel = ChatInfoModel(threadId = "", topic = "", allMessagesFetched = true)
|
||||
val afterState = ChatState(ChatStatus.NONE, localParticipantInfoModel, afterChatInfoModel, "")
|
||||
val afterState =
|
||||
ChatState(ChatStatus.NONE, localParticipantInfoModel, afterChatInfoModel, "", "")
|
||||
|
||||
// act
|
||||
val newState = reducer.reduce(previousState, action)
|
||||
|
@ -96,7 +102,8 @@ internal class ChatReducerUnitTest {
|
|||
allMessagesFetched = false,
|
||||
isThreadDeleted = false
|
||||
)
|
||||
val previousState = ChatState(ChatStatus.NONE, localParticipantInfoModel, chatInfoModel, "")
|
||||
val previousState =
|
||||
ChatState(ChatStatus.NONE, localParticipantInfoModel, chatInfoModel, "", "")
|
||||
|
||||
val action = ChatAction.ThreadDeleted()
|
||||
|
||||
|
@ -106,7 +113,8 @@ internal class ChatReducerUnitTest {
|
|||
allMessagesFetched = false,
|
||||
isThreadDeleted = true
|
||||
)
|
||||
val afterState = ChatState(ChatStatus.NONE, localParticipantInfoModel, afterChatInfoModel, "")
|
||||
val afterState =
|
||||
ChatState(ChatStatus.NONE, localParticipantInfoModel, afterChatInfoModel, "", "")
|
||||
|
||||
// act
|
||||
val newState = reducer.reduce(previousState, action)
|
||||
|
|
|
@ -279,7 +279,6 @@ class ParticipantsReducerUnitTest {
|
|||
// arrange
|
||||
val reducer = ParticipantsReducerImpl()
|
||||
val previousState = ParticipantsState(
|
||||
|
||||
participants = listOf(
|
||||
userOne,
|
||||
userTwo,
|
||||
|
|
|
@ -162,4 +162,31 @@ internal class MessageRepositoryListStorageUnitTest {
|
|||
Assert.assertEquals("6", repository[6].id)
|
||||
Assert.assertEquals("7", repository[7].id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageRepositoryListStorage_indexOfTest() {
|
||||
val storage = MessageRepository.createListBackedRepository()
|
||||
|
||||
val numberOfTestMessages = 50
|
||||
for (i in 1..numberOfTestMessages) {
|
||||
storage.addLocalMessage(
|
||||
MessageInfoModel(
|
||||
id = i.toString(),
|
||||
content = "Message $i",
|
||||
messageType = ChatMessageType.TEXT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Assert.assertEquals(
|
||||
1,
|
||||
storage.indexOf(
|
||||
MessageInfoModel(
|
||||
id = "2",
|
||||
content = "",
|
||||
messageType = ChatMessageType.TEXT
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -169,4 +169,31 @@ internal class MessageRepositorySkipListStorageUnitTest {
|
|||
|
||||
Assert.assertEquals(true, startTime <endTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageRepositorySkipListStorage_indexOfTest() {
|
||||
val storage = MessageRepository.createSkipListBackedRepository()
|
||||
|
||||
val numberOfTestMessages = 50
|
||||
for (i in 1..numberOfTestMessages) {
|
||||
storage.addLocalMessage(
|
||||
MessageInfoModel(
|
||||
id = i.toString(),
|
||||
content = "Message $i",
|
||||
messageType = ChatMessageType.TEXT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Assert.assertEquals(
|
||||
1,
|
||||
storage.indexOf(
|
||||
MessageInfoModel(
|
||||
id = "2",
|
||||
content = "",
|
||||
messageType = ChatMessageType.TEXT
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,4 +172,31 @@ class MessageRepositoryTreeStorageUnitTest {
|
|||
|
||||
Assert.assertEquals(true, startTime <endTime)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun messageRepositoryTreeStorage_indexOfTest() {
|
||||
val storage = MessageRepository.createTreeBackedRepository()
|
||||
|
||||
val numberOfTestMessages = 50
|
||||
for (i in 1..numberOfTestMessages) {
|
||||
storage.addLocalMessage(
|
||||
MessageInfoModel(
|
||||
id = i.toString(),
|
||||
content = "Message $i",
|
||||
messageType = ChatMessageType.TEXT
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Assert.assertEquals(
|
||||
1,
|
||||
storage.indexOf(
|
||||
MessageInfoModel(
|
||||
id = "2",
|
||||
content = "",
|
||||
messageType = ChatMessageType.TEXT
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,29 +52,6 @@ android {
|
|||
viewBinding true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
manifestPlaceholders = [
|
||||
appIcon : "@mipmap/ic_launcher",
|
||||
appIconRound: "@mipmap/ic_launcher_round"
|
||||
]
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'acs-ui-library.pro'
|
||||
if (file(String.valueOf(System.getenv("KEYSTORE_FILEPATH"))).canRead()) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
manifestPlaceholders = [
|
||||
appIcon : "@mipmap/ic_launcher_debug",
|
||||
appIconRound: "@mipmap/ic_launcher_debug_round"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
|
@ -122,21 +99,84 @@ android {
|
|||
|
||||
flavorDimensions "product"
|
||||
productFlavors {
|
||||
|
||||
calling {
|
||||
minSdkVersion 21
|
||||
dimension "product"
|
||||
matchingFallbacks = ["calling"]
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
manifestPlaceholders = [
|
||||
appIcon : "@mipmap/ic_launcher",
|
||||
appIconRound: "@mipmap/ic_launcher_round"
|
||||
]
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'acs-ui-library.pro'
|
||||
if (file(String.valueOf(System.getenv("KEYSTORE_FILEPATH"))).canRead()) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
manifestPlaceholders = [
|
||||
appIcon : "@mipmap/ic_launcher_debug",
|
||||
appIconRound: "@mipmap/ic_launcher_debug_round"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
chat {
|
||||
minSdkVersion 23
|
||||
dimension "product"
|
||||
matchingFallbacks = ["chat"]
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
manifestPlaceholders = [
|
||||
appIcon : "@mipmap/ic_launcher",
|
||||
appIconRound: "@mipmap/ic_launcher_round"
|
||||
]
|
||||
if (file(String.valueOf(System.getenv("KEYSTORE_FILEPATH"))).canRead()) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
manifestPlaceholders = [
|
||||
appIcon : "@mipmap/ic_launcher_debug",
|
||||
appIconRound: "@mipmap/ic_launcher_debug_round"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
callwithchat {
|
||||
minSdkVersion 23
|
||||
dimension "product"
|
||||
matchingFallbacks = ["callwithchat"]
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
manifestPlaceholders = [
|
||||
appIcon : "@mipmap/ic_launcher",
|
||||
appIconRound: "@mipmap/ic_launcher_round"
|
||||
]
|
||||
if (file(String.valueOf(System.getenv("KEYSTORE_FILEPATH"))).canRead()) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
manifestPlaceholders = [
|
||||
appIcon : "@mipmap/ic_launcher_debug",
|
||||
appIconRound: "@mipmap/ic_launcher_debug_round"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
|
|
|
@ -11,5 +11,6 @@
|
|||
android:text="TextView"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
android:textStyle="bold"
|
||||
/>
|
||||
|
||||
|
|
|
@ -9,8 +9,24 @@
|
|||
### Bug Fixes
|
||||
- N/A
|
||||
|
||||
## 1.1.0 (2022-11-09)
|
||||
|
||||
### New Features
|
||||
- `CallCompositeSetupScreenViewData` introduced for setting up call title and subtitle.
|
||||
- New error message `cameraFailure` added to address camera related errors.
|
||||
- Joining call is prevented with a new Error message now when network is not available.
|
||||
- Added permission setting capability to allow user to quickly navigate to app's info page when permissions are denied.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed Error banner and banner text color for dark theme.
|
||||
- Display DrawerDialog across screen rotation.
|
||||
- Fix ANR when trying to hang up call on hold.
|
||||
- Fix edge case with multiple activity instances.
|
||||
- Fix display name not getting truncated in participant list when they are too long.
|
||||
|
||||
## 1.1.0-beta.1 (2022-10-03)
|
||||
### Features
|
||||
|
||||
### New Features
|
||||
- Setting up Call Title and Subtitle is now availble by customizing `CallCompositeLocalOptions` with `CallCompositeSetupScreenViewData`.
|
||||
- Implemented new error message `cameraFailure` that can be sent to developers when initiating or turning on camera fails.
|
||||
- Error message now shown when network is not available before joining a call.
|
||||
|
@ -20,7 +36,7 @@
|
|||
- Display DrawerDialog across screen rotation.
|
||||
- Fix ANR when trying to hang up call on hold.
|
||||
- Fix edge case with multiple activity instances.
|
||||
- Fix display name not getting truncated in participant list when they are too long (https://github.com/Azure/communication-ui-library-android/pull/370).
|
||||
- Fix display name not getting truncated in participant list when they are too long
|
||||
|
||||
## 1.0.0 (2022-06-20)
|
||||
- This version is the public GA release with Calling UI Library
|
||||
|
|
Загрузка…
Ссылка в новой задаче