Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: typing indicator in conversation view (WPB-4706) #2292

Merged
merged 50 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e914e25
feat: base wip for typing indicator
yamilmedina Sep 26, 2023
aa316e6
feat: adjustment for typing indicator
yamilmedina Sep 27, 2023
c77abb7
Merge branch 'develop' into feat/typing-indicator-receiver
yamilmedina Sep 27, 2023
125fbd7
feat: kalium ref
yamilmedina Sep 27, 2023
f331564
feat: wip typing indicator
yamilmedina Sep 27, 2023
74fb69e
feat: typing indicator user mapping
yamilmedina Sep 27, 2023
f712e4c
Merge branch 'develop' into feat/typing-indicator-receiver
yamilmedina Sep 27, 2023
d86e088
Merge branch 'develop' into feat/typing-indicator-receiver
yamilmedina Sep 28, 2023
fc5d91c
feat: mapping users summary
yamilmedina Sep 28, 2023
c13ffd2
Merge branch 'develop' into feat/typing-indicator-receiver
yamilmedina Sep 29, 2023
f493f8b
feat: kalium fec
yamilmedina Sep 29, 2023
d617f94
feat: use usecase to observer users and map to ui element
yamilmedina Sep 29, 2023
b544794
feat: wip
yamilmedina Sep 29, 2023
914e42e
Merge branch 'develop' into feat/typing-indicator-receiver
yamilmedina Sep 30, 2023
c9ff7cb
feat: add typing indicator component v1
yamilmedina Sep 30, 2023
58f8e5f
feat: add typing indicator component animation
yamilmedina Sep 30, 2023
c831abc
feat: add typing indicator component animation
yamilmedina Oct 1, 2023
f05ef0e
feat: add typing indicator component animation wip
yamilmedina Oct 1, 2023
57dc860
feat: add typing indicator component animation and extract to file
yamilmedina Oct 1, 2023
3e0f79c
feat: add typing indicator component animation and extract strings
yamilmedina Oct 1, 2023
4756ce0
feat: add typing indicator component animation and extract strings
yamilmedina Oct 1, 2023
22f974f
feat: add typing indicator component animation and extract strings
yamilmedina Oct 1, 2023
1949554
feat: add typing indicator component animation and extract strings
yamilmedina Oct 1, 2023
3fb4b53
feat: add typing indicator component animation and extract avatar pre…
yamilmedina Oct 1, 2023
a737bda
feat: add typing indicator component animation and extract avatar pre…
yamilmedina Oct 1, 2023
7938ea4
feat: adjustment to text
yamilmedina Oct 2, 2023
bd80a66
Merge branch 'develop' into feat/typing-indicator-receiver
yamilmedina Oct 2, 2023
378805b
feat: kalium ref
yamilmedina Oct 2, 2023
a9e6b9c
feat: kalium ref
yamilmedina Oct 2, 2023
003ded3
feat: detekt
yamilmedina Oct 2, 2023
8cf4d4e
feat: detekt
yamilmedina Oct 2, 2023
ed599d7
fix: broken testst
yamilmedina Oct 2, 2023
6c0f026
fix: re-enable disabled test reset session
yamilmedina Oct 2, 2023
456a9ce
fix: test coverage
yamilmedina Oct 2, 2023
bacd414
fix: test coverage
yamilmedina Oct 2, 2023
ed6d386
fix: test coverage
yamilmedina Oct 2, 2023
5fdbe1b
fix: test coverage
yamilmedina Oct 2, 2023
1a2ff0e
fix: detekt
yamilmedina Oct 2, 2023
eaa11a0
chore: kalium ref
yamilmedina Oct 2, 2023
f854bf7
Merge branch 'develop' into feat/typing-indicator-receiver
yamilmedina Oct 2, 2023
77e1839
chore: address pr comments
yamilmedina Oct 2, 2023
3fbe18d
chore: detekt
yamilmedina Oct 2, 2023
f28b2e9
chore: address pr comments
yamilmedina Oct 2, 2023
51c78df
chore: kalium ref
yamilmedina Oct 2, 2023
3297013
chore: test cov for new viewmodel
yamilmedina Oct 2, 2023
a04f6a0
chore: test cov for new viewmodel
yamilmedina Oct 2, 2023
0b84210
chore: test cov for new viewmodel
yamilmedina Oct 2, 2023
49338ed
Merge branch 'develop' into feat/typing-indicator-receiver
yamilmedina Oct 2, 2023
48eba61
feat: not show status indicator in small preview
yamilmedina Oct 3, 2023
db395c0
feat: not show status indicator in small preview
yamilmedina Oct 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.wire.kalium.logic.feature.conversation.ObserveConversationListDetails
import com.wire.kalium.logic.feature.conversation.ObserveConversationMembersUseCase
import com.wire.kalium.logic.feature.conversation.ObserveIsSelfUserMemberUseCase
import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase
import com.wire.kalium.logic.feature.conversation.ObserveUsersTypingUseCase
import com.wire.kalium.logic.feature.conversation.RefreshConversationsWithoutMetadataUseCase
import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase
import com.wire.kalium.logic.feature.conversation.RenameConversationUseCase
Expand Down Expand Up @@ -235,4 +236,9 @@ class ConversationModule {
fun provideObserveArchivedUnreadConversationsCountUseCase(
conversationScope: ConversationScope
): ObserveArchivedUnreadConversationsCountUseCase = conversationScope.observeArchivedUnreadConversationsCount

@ViewModelScoped
@Provides
fun provideObserveUsersTypingUseCase(conversationScope: ConversationScope): ObserveUsersTypingUseCase =
conversationScope.observeUsersTyping
}
19 changes: 17 additions & 2 deletions app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant
import com.wire.android.ui.home.conversations.previewAsset
import com.wire.android.util.ui.WireSessionImageLoader
import com.wire.kalium.logic.data.message.UserSummary
import com.wire.kalium.logic.data.message.reaction.MessageReaction
import com.wire.kalium.logic.data.message.receipt.DetailedReceipt
import com.wire.kalium.logic.data.user.OtherUser
Expand Down Expand Up @@ -63,7 +64,7 @@
id = userSummary.userId,
name = userSummary.userName.orEmpty(),
handle = userSummary.userHandle.orEmpty(),
avatarData = previewAsset(wireSessionImageLoader),
avatarData = userSummary.previewAsset(wireSessionImageLoader),

Check warning on line 67 in app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt#L67

Added line #L67 was not covered by tests
membership = userTypeMapper.toMembership(userSummary.userType),
unavailable = !userSummary.isUserDeleted && userSummary.userName.orEmpty().isEmpty(),
isDeleted = userSummary.isUserDeleted,
Expand All @@ -77,7 +78,7 @@
id = userSummary.userId,
name = userSummary.userName.orEmpty(),
handle = userSummary.userHandle.orEmpty(),
avatarData = previewAsset(wireSessionImageLoader),
avatarData = userSummary.previewAsset(wireSessionImageLoader),

Check warning on line 81 in app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/mapper/UIParticipantMapper.kt#L81

Added line #L81 was not covered by tests
membership = userTypeMapper.toMembership(userSummary.userType),
unavailable = !userSummary.isUserDeleted && userSummary.userName.orEmpty().isEmpty(),
isDeleted = userSummary.isUserDeleted,
Expand All @@ -86,4 +87,18 @@
isDefederated = false
)
}

fun toUIParticipant(userSummary: UserSummary): UIParticipant = with(userSummary) {
return UIParticipant(
id = userSummary.userId,
name = userSummary.userName.orEmpty(),
handle = userSummary.userHandle.orEmpty(),
avatarData = previewAsset(wireSessionImageLoader),
membership = userTypeMapper.toMembership(userSummary.userType),
unavailable = !userSummary.isUserDeleted && userSummary.userName.orEmpty().isEmpty(),
isDeleted = userSummary.isUserDeleted,
isSelf = false,
isDefederated = false
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ import com.wire.android.model.ImageAsset.UserAvatarAsset
import com.wire.android.model.UserAvatarData
import com.wire.android.util.ui.WireSessionImageLoader
import com.wire.kalium.logic.data.conversation.MemberDetails
import com.wire.kalium.logic.data.message.reaction.MessageReaction
import com.wire.kalium.logic.data.message.receipt.DetailedReceipt
import com.wire.kalium.logic.data.message.UserSummary
import com.wire.kalium.logic.data.user.ConnectionState
import com.wire.kalium.logic.data.user.OtherUser
import com.wire.kalium.logic.data.user.SelfUser
Expand Down Expand Up @@ -70,18 +69,10 @@ val MemberDetails.userType: UserType
is SelfUser -> UserType.INTERNAL
}

fun MessageReaction.previewAsset(
fun UserSummary.previewAsset(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexandreferris here I refactored to reuse this mapper since we had this UserSummary from several points :D

wireSessionImageLoader: WireSessionImageLoader
) = UserAvatarData(
asset = this.userSummary.userPreviewAssetId?.let { UserAvatarAsset(wireSessionImageLoader, it) },
availabilityStatus = userSummary.availabilityStatus,
connectionState = userSummary.connectionStatus
)

fun DetailedReceipt.previewAsset(
wireSessionImageLoader: WireSessionImageLoader
) = UserAvatarData(
asset = this.userSummary.userPreviewAssetId?.let { UserAvatarAsset(wireSessionImageLoader, it) },
availabilityStatus = userSummary.availabilityStatus,
connectionState = userSummary.connectionStatus
asset = this.userPreviewAssetId?.let { UserAvatarAsset(wireSessionImageLoader, it) },
availabilityStatus = this.availabilityStatus,
connectionState = this.connectionStatus
)
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import com.wire.android.ui.home.conversations.call.ConversationCallViewModel
import com.wire.android.ui.home.conversations.call.ConversationCallViewState
import com.wire.android.ui.home.conversations.delete.DeleteMessageDialog
import com.wire.android.ui.home.conversations.details.GroupConversationDetailsNavBackArgs
import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant
import com.wire.android.ui.home.conversations.edit.EditMessageMenuItems
import com.wire.android.ui.home.conversations.info.ConversationDetailsData
import com.wire.android.ui.home.conversations.info.ConversationInfoViewModel
Expand Down Expand Up @@ -582,6 +583,7 @@ private fun ConversationScreen(
ConversationScreenContent(
conversationId = conversationInfoViewState.conversationId,
audioMessagesState = conversationMessagesViewState.audioMessagesState,
usersTyping = conversationMessagesViewState.usersTyping,
lastUnreadMessageInstant = conversationMessagesViewState.firstUnreadInstant,
unreadEventCount = conversationMessagesViewState.firstuUnreadEventIndex,
conversationDetailsData = conversationInfoViewState.conversationDetailsData,
Expand Down Expand Up @@ -626,6 +628,7 @@ private fun ConversationScreenContent(
lastUnreadMessageInstant: Instant?,
unreadEventCount: Int,
audioMessagesState: Map<String, AudioState>,
usersTyping: List<UIParticipant>,
messageComposerStateHolder: MessageComposerStateHolder,
messages: Flow<PagingData<UIMessage>>,
onSendMessage: (MessageBundle) -> Unit,
Expand Down Expand Up @@ -685,8 +688,8 @@ private fun ConversationScreenContent(
onClearMentionSearchResult = onClearMentionSearchResult,
onSendMessageBundle = onSendMessage,
tempWritableVideoUri = tempWritableVideoUri,
tempWritableImageUri = tempWritableImageUri

tempWritableImageUri = tempWritableImageUri,
usersTyping = usersTyping
)

// TODO: uncomment when we have the "scroll to bottom" button implemented
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.ui.home.conversations

import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.InfiniteTransition
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.MoreHoriz
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.wire.android.R
import com.wire.android.model.UserAvatarData
import com.wire.android.ui.common.UserProfileAvatar
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.home.conversations.details.participants.model.UIParticipant
import com.wire.android.ui.home.conversationslist.model.Membership
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.kalium.logic.data.id.QualifiedID

@Composable
fun UsersTypingIndicator(
usersTyping: List<UIParticipant>,
) {
if (usersTyping.isNotEmpty()) {
val rememberTransition =
rememberInfiniteTransition(label = stringResource(R.string.animation_label_typing_indicator_horizontal_transition))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.height(dimensions().spacing24x)
.background(
color = colorsScheme().surface,
shape = RoundedCornerShape(dimensions().corner14x),
)
) {
UsersTypingAvatarPreviews(usersTyping)
Text(
text = pluralStringResource(
R.plurals.typing_indicator_event_message,
usersTyping.size,
usersTyping.first().name,
usersTyping.size - 1
),
style = MaterialTheme.wireTypography.label01.copy(color = colorsScheme().secondaryText),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.weight(weight = 1f, fill = false)
.padding(
top = dimensions().spacing4x,
bottom = dimensions().spacing4x,
end = dimensions().spacing8x,
)
)
HorizontalBouncingWritingPen(infiniteTransition = rememberTransition)
}
}
}

@Suppress("MagicNumber")
@Composable
private fun UsersTypingAvatarPreviews(usersTyping: List<UIParticipant>, maxPreviewsDisplay: Int = 3) {
yamilmedina marked this conversation as resolved.
Show resolved Hide resolved
usersTyping.take(maxPreviewsDisplay).forEachIndexed { index, user ->
val isSingleUser = usersTyping.size == 1 || maxPreviewsDisplay == 1
UserProfileAvatar(
avatarData = user.avatarData,
size = dimensions().spacing16x,
padding = dimensions().spacing2x,
modifier = if (isSingleUser) Modifier
else {
Modifier.offset(
x = if (index == 0) dimensions().spacing8x else -(dimensions().spacing6x)
)
}
)
}
}

@Suppress("MagicNumber")
@Composable
private fun HorizontalBouncingWritingPen(
infiniteTransition: InfiniteTransition,
) {
Row(modifier = Modifier.fillMaxHeight()) {
val position by infiniteTransition.animateFloat(
initialValue = -5f, targetValue = -1f,
animationSpec = infiniteRepeatable(
animation = tween(1_000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = infiniteTransition.label
)

Icon(
imageVector = Icons.Default.MoreHoriz,
contentDescription = null,
tint = colorsScheme().secondaryText,
modifier = Modifier
.size(dimensions().spacing12x)
.offset(y = -dimensions().spacing2x)
.align(Alignment.Bottom)
)
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null,
tint = colorsScheme().secondaryText,
modifier = Modifier
.size(dimensions().spacing12x)
.offset(x = position.dp)
.align(Alignment.CenterVertically),
)
}
}

@PreviewMultipleThemes
@Composable
fun PreviewUsersTypingOne() {
Column(
modifier = Modifier
.background(color = colorsScheme().background)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
UsersTypingIndicator(
listOf(
UIParticipant(
id = QualifiedID("Alice", "wire.com"),
name = "Alice",
handle = "alice",
isSelf = false,
isService = false,
avatarData = UserAvatarData(),
membership = Membership.None,
connectionState = null,
unavailable = false,
isDeleted = false,
readReceiptDate = null,
botService = null,
isDefederated = false
)
)
)
}
}

@PreviewMultipleThemes
@Composable
fun PreviewUsersTypingMoreThanOne() {
Column(
modifier = Modifier
.background(color = colorsScheme().background)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
UsersTypingIndicator(
listOf(
UIParticipant(
id = QualifiedID("Bob", "wire.com"),
name = "Bob",
handle = "bob",
isSelf = false,
isService = false,
avatarData = UserAvatarData(),
membership = Membership.None,
connectionState = null,
unavailable = false,
isDeleted = false,
readReceiptDate = null,
botService = null,
isDefederated = false
),
UIParticipant(
id = QualifiedID("alice", "wire.com"),
name = "Alice Smith",
handle = "alice",
isSelf = false,
isService = false,
avatarData = UserAvatarData(),
membership = Membership.None,
connectionState = null,
unavailable = false,
isDeleted = false,
readReceiptDate = null,
botService = null,
isDefederated = false
)
)
)
}
}