diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml
index 6d2b3e3de0d..baaadb526b9 100644
--- a/.github/workflows/publish-test-results.yml
+++ b/.github/workflows/publish-test-results.yml
@@ -65,7 +65,7 @@ jobs:
PR_NUMBER=$(jq --raw-output .pull_request.number "$EVENT_FILE_PATH")
gh pr comment "$PR_NUMBER" --body "APKs built during tests are available [here]($CHECKS_LINK). Scroll down to **Artifacts**!"
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
node-version: 18
- name: Install datadog-ci
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 897d7058766..ade3fe8381a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -49,6 +49,8 @@
+
+
@@ -58,6 +60,10 @@
android:glEsVersion="0x00020000"
android:required="false" />
+
+
UILastMessageContent.None
+ MessagePreviewContent.VerificationChanged.VerifiedMls ->
+ UILastMessageContent.VerificationChanged(R.string.last_message_verified_conversation_mls)
+ MessagePreviewContent.VerificationChanged.VerifiedProteus ->
+ UILastMessageContent.VerificationChanged(R.string.last_message_verified_conversation_proteus)
+ MessagePreviewContent.VerificationChanged.DegradedMls ->
+ UILastMessageContent.VerificationChanged(R.string.last_message_conversations_verification_degraded_mls)
+ MessagePreviewContent.VerificationChanged.DegradedProteus ->
+ UILastMessageContent.VerificationChanged(R.string.last_message_conversations_verification_degraded_proteus)
Unknown -> UILastMessageContent.None
}
}
diff --git a/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt b/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt
index a85a0425084..1ce361e681b 100644
--- a/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt
+++ b/app/src/main/kotlin/com/wire/android/mapper/SystemMessageContentMapper.kt
@@ -70,6 +70,7 @@ class SystemMessageContentMapper @Inject constructor(
is MessageContent.ConversationVerifiedProteus -> mapConversationVerified(Conversation.Protocol.PROTEUS)
is MessageContent.FederationStopped -> mapFederationMessage(content)
is MessageContent.ConversationProtocolChanged -> mapConversationProtocolChanged(content)
+ is MessageContent.ConversationStartedUnverifiedWarning -> mapConversationCreatedUnverifiedWarning()
}
private fun mapConversationCreated(senderUserId: UserId, date: String, userList: List): UIMessageContent.SystemMessage {
@@ -85,6 +86,10 @@ class SystemMessageContentMapper @Inject constructor(
)
}
+ private fun mapConversationCreatedUnverifiedWarning(): UIMessageContent.SystemMessage {
+ return UIMessageContent.SystemMessage.ConversationMessageCreatedUnverifiedWarning
+ }
+
private fun mapConversationTimerChanged(
senderUserId: UserId,
content: MessageContent.ConversationMessageTimerChanged,
@@ -246,12 +251,16 @@ class SystemMessageContentMapper @Inject constructor(
private fun mapConversationHistoryLost(): UIMessageContent.SystemMessage =
UIMessageContent.SystemMessage.HistoryLost
+
private fun mapMLSWrongEpochWarning(): UIMessageContent.SystemMessage =
UIMessageContent.SystemMessage.MLSWrongEpochWarning()
+
private fun mapConversationHistoryListProtocolChanged(): UIMessageContent.SystemMessage =
UIMessageContent.SystemMessage.HistoryLostProtocolChanged
+
private fun mapConversationDegraded(protocol: Conversation.Protocol): UIMessageContent.SystemMessage =
UIMessageContent.SystemMessage.ConversationDegraded(protocol)
+
private fun mapConversationVerified(protocol: Conversation.Protocol): UIMessageContent.SystemMessage =
UIMessageContent.SystemMessage.ConversationVerified(protocol)
diff --git a/app/src/main/kotlin/com/wire/android/migration/MigrationMapper.kt b/app/src/main/kotlin/com/wire/android/migration/MigrationMapper.kt
index 7803ed9edc1..72dbf33eb64 100644
--- a/app/src/main/kotlin/com/wire/android/migration/MigrationMapper.kt
+++ b/app/src/main/kotlin/com/wire/android/migration/MigrationMapper.kt
@@ -106,7 +106,8 @@ class MigrationMapper @Inject constructor() {
userMessageTimer = null,
archived = false,
archivedDateTime = null,
- verificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
)
}
}
diff --git a/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt b/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt
index bea848e7b88..7e152e6b43f 100644
--- a/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt
+++ b/app/src/main/kotlin/com/wire/android/navigation/HomeDestination.kt
@@ -112,7 +112,7 @@ sealed class HomeDestination(
private const val ITEM_NAME_PREFIX = "HomeNavigationItem."
fun fromRoute(fullRoute: String): HomeDestination? =
- values().find { it.direction.route.getPrimaryRoute() == fullRoute.getPrimaryRoute() }
+ values().find { it.direction.route.getBaseRoute() == fullRoute.getBaseRoute() }
fun values(): Array =
arrayOf(Conversations, Calls, Mentions, Settings, Vault, Archive, Support, WhatsNew)
}
diff --git a/app/src/main/kotlin/com/wire/android/navigation/NavigationUtils.kt b/app/src/main/kotlin/com/wire/android/navigation/NavigationUtils.kt
index e4c28cce428..183aa352462 100644
--- a/app/src/main/kotlin/com/wire/android/navigation/NavigationUtils.kt
+++ b/app/src/main/kotlin/com/wire/android/navigation/NavigationUtils.kt
@@ -45,7 +45,7 @@ internal fun NavController.navigateToItem(command: NavigationCommand) {
fun lastDestination() = currentBackStack.value.lastOrNull { it.route() is DestinationSpec<*> }
fun lastNestedGraph() = lastDestination()?.takeIf { it.navGraph() != navGraph }?.navGraph()
fun firstDestinationWithRoute(route: String) =
- currentBackStack.value.firstOrNull { it.destination.route?.getPrimaryRoute() == route.getPrimaryRoute() }
+ currentBackStack.value.firstOrNull { it.destination.route?.getBaseRoute() == route.getBaseRoute() }
fun lastDestinationFromOtherGraph(graph: NavGraphSpec) = currentBackStack.value.lastOrNull { it.navGraph() != graph }
appLogger.d("[$TAG] -> command: ${command.destination.route.obfuscateId()}")
@@ -104,17 +104,11 @@ private fun NavOptionsBuilder.popUpTo(
internal fun NavDestination.toDestination(): Destination? =
this.route?.let { currentRoute -> NavGraphs.root.destinationsByRoute[currentRoute] }
-fun String.getPrimaryRoute(): String {
- val splitByQuestion = this.split("?")
- val splitBySlash = this.split("/")
-
- val primaryRoute = when {
- splitByQuestion.size > 1 -> splitByQuestion[0]
- splitBySlash.size > 1 -> splitBySlash[0]
- else -> this
+fun String.getBaseRoute(): String =
+ this.indexOfAny(listOf("?", "/")).let {
+ if (it != -1) this.substring(0, it)
+ else this
}
- return primaryRoute
-}
fun Direction.handleNavigation(context: Context, handleOtherDirection: (Direction) -> Unit) = when (this) {
is ExternalUriDirection -> CustomTabsHelper.launchUri(context, this.uri)
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt
index 6f4a29d8361..adb0d6108da 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/DeviceItem.kt
@@ -183,7 +183,7 @@ private fun DeviceItemTexts(
)
if (shouldShowVerifyLabel) {
Spacer(modifier = Modifier.width(MaterialTheme.wireDimensions.spacing8x))
- if (device.isVerifiedProteus) ProteusVerifiedIcon(Modifier.wrapContentWidth())
+ if (device.isVerifiedProteus) ProteusVerifiedIcon(Modifier.wrapContentWidth().align(Alignment.CenterVertically))
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt b/app/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt
index 4e3db7fa6ad..8ab97ef06a6 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/SearchBar.kt
@@ -31,47 +31,20 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.wire.android.R
import com.wire.android.ui.common.textfield.WireTextField
+import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireColorScheme
-
-@Composable
-fun SearchBar(
- placeholderText: String,
- onTextTyped: (TextFieldValue) -> Unit = {},
- modifier: Modifier = Modifier
-) {
- SearchBarInput(
- placeholderText = placeholderText,
- leadingIcon =
- {
- IconButton(onClick = { }) {
- Icon(
- painter = painterResource(id = R.drawable.ic_search),
- contentDescription = stringResource(R.string.content_description_conversation_search_icon),
- tint = MaterialTheme.wireColorScheme.onBackground
- )
- }
- },
- placeholderTextStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
- textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Start),
- onTextTyped = onTextTyped,
- modifier = modifier
- )
-}
+import com.wire.android.util.ui.PreviewMultipleThemes
@Composable
fun SearchBarInput(
@@ -80,10 +53,12 @@ fun SearchBarInput(
text: TextFieldValue = TextFieldValue(""),
onTextTyped: (TextFieldValue) -> Unit = {},
placeholderTextStyle: TextStyle = LocalTextStyle.current,
+ placeholderAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
textStyle: TextStyle = LocalTextStyle.current,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
) {
+
WireTextField(
modifier = modifier,
value = text,
@@ -112,14 +87,28 @@ fun SearchBarInput(
interactionSource = interactionSource,
textStyle = textStyle.copy(fontSize = 14.sp),
placeholderTextStyle = placeholderTextStyle.copy(fontSize = 14.sp),
+ placeholderAlignment = placeholderAlignment,
placeholderText = placeholderText,
maxLines = 1,
singleLine = true,
)
}
-@Preview(showBackground = true)
+@PreviewMultipleThemes
@Composable
-fun PreviewSearchBarCollapsed() {
- SearchBar("Search text")
+fun PreviewSearchBarInput() {
+ WireTheme {
+ SearchBarInput(
+ placeholderText = "placeholder",
+ leadingIcon = {
+ IconButton(onClick = { }) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_search),
+ contentDescription = stringResource(R.string.content_description_conversation_search_icon),
+ tint = MaterialTheme.wireColorScheme.onBackground
+ )
+ }
+ },
+ )
+ }
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/VerifiedIcons.kt b/app/src/main/kotlin/com/wire/android/ui/common/VerifiedIcons.kt
index a79f3740bee..0834be5d5e8 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/VerifiedIcons.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/VerifiedIcons.kt
@@ -17,6 +17,7 @@
*/
package com.wire.android.ui.common
+import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
@@ -26,10 +27,25 @@ import androidx.compose.ui.res.stringResource
import com.wire.android.R
@Composable
-fun ProteusVerifiedIcon(modifier: Modifier = Modifier) {
+fun ProteusVerifiedIcon(
+ modifier: Modifier = Modifier,
+ @StringRes contentDescriptionId: Int = R.string.label_client_verified
+) {
Image(
modifier = modifier.padding(start = dimensions().spacing4x),
painter = painterResource(id = R.drawable.ic_certificate_valid_proteus),
- contentDescription = stringResource(R.string.label_client_verified)
+ contentDescription = stringResource(contentDescriptionId)
+ )
+}
+
+@Composable
+fun MLSVerifiedIcon(
+ modifier: Modifier = Modifier,
+ @StringRes contentDescriptionId: Int = R.string.label_client_verified
+) {
+ Image(
+ modifier = modifier.padding(start = dimensions().spacing4x),
+ painter = painterResource(id = R.drawable.ic_certificate_valid_mls),
+ contentDescription = stringResource(contentDescriptionId)
)
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt
index 306a0e7508e..227973328eb 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetContent.kt
@@ -125,7 +125,10 @@ data class ConversationSheetContent(
val conversationTypeDetail: ConversationTypeDetail,
val selfRole: Conversation.Member.Role?,
val isTeamConversation: Boolean,
- val isArchived: Boolean
+ val isArchived: Boolean,
+ val protocol: Conversation.ProtocolInfo,
+ val mlsVerificationStatus: Conversation.VerificationStatus,
+ val proteusVerificationStatus: Conversation.VerificationStatus
) {
private val isSelfUserMember: Boolean get() = selfRole != null
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt
index 648c1f4c62a..d3833edaf8c 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationSheetState.kt
@@ -76,10 +76,14 @@ fun rememberConversationSheetState(
),
isTeamConversation = teamId != null,
selfRole = selfMemberRole,
- isArchived = conversationItem.isArchived
+ isArchived = conversationItem.isArchived,
+ protocol = Conversation.ProtocolInfo.Proteus,
+ mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED
)
}
}
+
is ConversationItem.PrivateConversation -> {
with(conversationItem) {
ConversationSheetContent(
@@ -95,10 +99,14 @@ fun rememberConversationSheetState(
),
isTeamConversation = isTeamConversation,
selfRole = Conversation.Member.Role.Member,
- isArchived = conversationItem.isArchived
+ isArchived = conversationItem.isArchived,
+ protocol = Conversation.ProtocolInfo.Proteus,
+ mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED
)
}
}
+
is ConversationItem.ConnectionConversation -> {
with(conversationItem) {
ConversationSheetContent(
@@ -110,7 +118,10 @@ fun rememberConversationSheetState(
),
isTeamConversation = isTeamConversation,
selfRole = Conversation.Member.Role.Member,
- isArchived = conversationItem.isArchived
+ isArchived = conversationItem.isArchived,
+ protocol = Conversation.ProtocolInfo.Proteus,
+ mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED
)
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt
index 72fe53f7c48..fc7934570c2 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/HomeSheetContent.kt
@@ -153,10 +153,11 @@ internal fun ConversationMainSheetContent(
with(conversationSheetContent) {
updateConversationArchiveStatus(
DialogState(
- conversationId,
- title,
- conversationTypeDetail,
- isArchived
+ conversationId = conversationId,
+ conversationName = title,
+ conversationTypeDetail = conversationTypeDetail,
+ isArchived = isArchived,
+ isMember = conversationSheetContent.selfRole != null
)
)
}
@@ -174,10 +175,11 @@ internal fun ConversationMainSheetContent(
onItemClick = {
clearConversationContent(
DialogState(
- conversationSheetContent.conversationId,
- conversationSheetContent.title,
- conversationSheetContent.conversationTypeDetail,
- conversationSheetContent.isArchived
+ conversationId = conversationSheetContent.conversationId,
+ conversationName = conversationSheetContent.title,
+ conversationTypeDetail = conversationSheetContent.conversationTypeDetail,
+ isArchived = conversationSheetContent.isArchived,
+ isMember = conversationSheetContent.selfRole != null
)
)
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt
index c4e8fdfc872..8ec324031e3 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt
@@ -71,6 +71,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.wire.android.R
import com.wire.android.ui.common.Icon
@@ -100,6 +101,7 @@ internal fun WireTextField(
visualTransformation: VisualTransformation = VisualTransformation.None,
textStyle: TextStyle = MaterialTheme.wireTypography.body01,
placeholderTextStyle: TextStyle = MaterialTheme.wireTypography.body01,
+ placeholderAlignment: Alignment.Horizontal = Alignment.Start,
inputMinHeight: Dp = MaterialTheme.wireDimensions.textFieldMinHeight,
shape: Shape = RoundedCornerShape(MaterialTheme.wireDimensions.textFieldCornerSize),
colors: WireTextFieldColors = wireTextFieldColors(),
@@ -162,6 +164,7 @@ internal fun WireTextField(
placeholderText,
state,
placeholderTextStyle,
+ placeholderAlignment,
inputMinHeight,
colors,
shouldDetectTaps,
@@ -227,6 +230,7 @@ private fun InnerText(
placeholderText: String? = null,
state: WireTextFieldState = WireTextFieldState.Default,
placeholderTextStyle: TextStyle = MaterialTheme.wireTypography.body01,
+ placeholderAlignment: Alignment.Horizontal = Alignment.Start,
inputMinHeight: Dp = 48.dp,
colors: WireTextFieldColors = wireTextFieldColors(),
shouldDetectTaps: Boolean = false,
@@ -254,26 +258,27 @@ private fun InnerText(
Tint(contentColor = colors.iconColor(state).value, content = leadingIcon)
}
}
- Box(Modifier.weight(1f)) {
- val padding = Modifier.padding(
- start = if (leadingIcon == null) 16.dp else 0.dp,
- end = if (trailingOrStateIcon == null) 16.dp else 0.dp,
- top = 2.dp, bottom = 2.dp
- )
+
+ Box(
+ Modifier
+ .weight(1f)
+ .padding(
+ start = if (leadingIcon == null) 16.dp else 0.dp,
+ end = if (trailingOrStateIcon == null) 16.dp else 0.dp,
+ top = 2.dp, bottom = 2.dp
+ )
+ ) {
if (value.text.isEmpty() && placeholderText != null) {
Text(
text = placeholderText,
style = placeholderTextStyle,
color = colors.placeholderColor(state).value,
modifier = Modifier
- .fillMaxWidth()
- .then(padding)
+ .align(placeholderAlignment.toAlignment())
)
}
Box(
- modifier = Modifier
- .fillMaxWidth()
- .then(padding),
+ modifier = Modifier.fillMaxWidth(),
propagateMinConstraints = true
) {
innerTextField()
@@ -287,6 +292,10 @@ private fun InnerText(
}
}
+private fun Alignment.Horizontal.toAlignment(): Alignment = Alignment { size, space, layoutDirection ->
+ IntOffset(this@toAlignment.align(size.width, space.width, layoutDirection), 0)
+}
+
@Preview(name = "Default WireTextField")
@Composable
fun PreviewWireTextField() {
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/WireCenterAlignedTopAppBar.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/WireCenterAlignedTopAppBar.kt
index 93847b255d7..27ca4b07465 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/WireCenterAlignedTopAppBar.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/WireCenterAlignedTopAppBar.kt
@@ -39,7 +39,6 @@ import com.wire.android.ui.common.dimensions
import com.wire.android.ui.theme.wireDimensions
import com.wire.android.ui.theme.wireTypography
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WireCenterAlignedTopAppBar(
title: String,
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt
index c28497c9025..f8484800564 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchBarState.kt
@@ -48,6 +48,7 @@ class SearchBarState(
private set
var searchQuery by mutableStateOf(searchQuery)
+ private set
fun closeSearch() {
isSearchActive = false
@@ -57,6 +58,10 @@ class SearchBarState(
isSearchActive = true
}
+ fun searchActiveChanged(isSearchActive: Boolean) {
+ this.isSearchActive = isSearchActive
+ }
+
fun searchQueryChanged(searchQuery: TextFieldValue) {
this.searchQuery = searchQuery
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt
index 6c72e05c290..19e4d2d75d0 100644
--- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/search/SearchTopBar.kt
@@ -21,123 +21,171 @@
package com.wire.android.ui.common.topappbar.search
+import android.annotation.SuppressLint
import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.BiasAlignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusEvent
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import com.wire.android.R
import com.wire.android.ui.common.SearchBarInput
import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireColorScheme
-import com.wire.android.ui.theme.wireDimensions
+import com.wire.android.util.ui.PreviewMultipleThemes
-@OptIn(ExperimentalAnimationApi::class)
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SearchTopBar(
+ modifier: Modifier = Modifier,
isSearchActive: Boolean,
searchBarHint: String,
searchQuery: TextFieldValue = TextFieldValue(""),
onSearchQueryChanged: (TextFieldValue) -> Unit,
- onInputClicked: () -> Unit,
- onCloseSearchClicked: () -> Unit,
+ onCloseSearchClicked: (() -> Unit)? = null,
+ onActiveChanged: (isActive: Boolean) -> Unit = {},
bottomContent: @Composable ColumnScope.() -> Unit = {}
) {
Column(
- modifier = Modifier
+ modifier = modifier
.wrapContentHeight()
.fillMaxWidth()
.background(MaterialTheme.wireColorScheme.background)
) {
- val interactionSource = remember {
- MutableInteractionSource()
- }
-
- val focusManager = LocalFocusManager.current
+ val interactionSource = remember { MutableInteractionSource() }
val focusRequester = remember { FocusRequester() }
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val focusManager = LocalFocusManager.current
- LaunchedEffect(isSearchActive) {
- if (!isSearchActive) {
+ fun setActive(isActive: Boolean) {
+ if (isActive) {
+ focusRequester.requestFocus()
+ keyboardController?.show()
+ } else {
focusManager.clearFocus()
+ keyboardController?.hide()
onSearchQueryChanged(TextFieldValue(""))
- } else {
- focusRequester.requestFocus()
}
}
- Box {
- SearchBarInput(
- placeholderText = searchBarHint,
- text = searchQuery,
- onTextTyped = onSearchQueryChanged,
- leadingIcon = {
- AnimatedContent(!isSearchActive, label = "") { isVisible ->
- IconButton(onClick = {
- if (!isVisible) {
- onCloseSearchClicked()
- }
- }) {
+ LaunchedEffect(isSearchActive) {
+ setActive(isSearchActive)
+ }
+
+ val placeholderAlignment by animateHorizontalAlignmentAsState(
+ targetAlignment = if (isSearchActive) Alignment.CenterStart else Alignment.Center
+ )
+
+ SearchBarInput(
+ placeholderText = searchBarHint,
+ text = searchQuery,
+ onTextTyped = onSearchQueryChanged,
+ leadingIcon = {
+ AnimatedContent(!isSearchActive, label = "") { isVisible ->
+ if (isVisible) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier.size(dimensions().buttonCircleMinSize)
+ ) {
Icon(
- painter = painterResource(
- id = if (isVisible) R.drawable.ic_search
- else R.drawable.ic_arrow_left
- ),
+ painter = painterResource(R.drawable.ic_search),
contentDescription = stringResource(R.string.content_description_conversation_search_icon),
- tint = MaterialTheme.wireColorScheme.onBackground
+ tint = MaterialTheme.wireColorScheme.onBackground,
+ )
+ }
+ } else {
+ IconButton(
+ onClick = { onCloseSearchClicked?.invoke() ?: setActive(false) },
+ modifier = Modifier.size(dimensions().buttonCircleMinSize)
+ ) {
+ Icon(
+ painter = rememberVectorPainter(image = Icons.Filled.ArrowBack),
+ contentDescription = stringResource(R.string.content_description_back_button),
+ tint = MaterialTheme.wireColorScheme.onBackground,
)
}
}
- },
- placeholderTextStyle = textStyleAlignment(isTopBarVisible = !isSearchActive),
- textStyle = textStyleAlignment(isTopBarVisible = !isSearchActive),
- interactionSource = interactionSource,
- modifier = Modifier
- .padding(dimensions().spacing8x)
- .focusRequester(focusRequester)
- )
- // We added an invisible clickable box only present when the search is not active.
- // That way we can still make the whole top bar clickable and intercept and discard the long press gestures.
- if (!isSearchActive) {
- Box(
- modifier = Modifier
- .matchParentSize()
- .padding(dimensions().spacing8x)
- .clip(RoundedCornerShape(MaterialTheme.wireDimensions.textFieldCornerSize))
- .clickable(onClick = onInputClicked)
- )
- }
- }
+ }
+ },
+ placeholderTextStyle = LocalTextStyle.current.copy(textAlign = if (!isSearchActive) TextAlign.Center else TextAlign.Start),
+ placeholderAlignment = placeholderAlignment,
+ textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Start),
+ interactionSource = interactionSource,
+ modifier = Modifier
+ .padding(dimensions().spacing8x)
+ .focusRequester(focusRequester)
+ .onFocusEvent { onActiveChanged(it.isFocused) }
+ )
bottomContent()
}
}
+@SuppressLint("UnrememberedMutableState")
@Composable
-private fun textStyleAlignment(isTopBarVisible: Boolean): TextStyle {
- return if (isTopBarVisible) LocalTextStyle.current.copy(textAlign = TextAlign.Center) else LocalTextStyle.current.copy(
- textAlign = TextAlign.Start
- )
+private fun animateHorizontalAlignmentAsState(
+ targetAlignment: Alignment,
+): State {
+ val biased = targetAlignment as BiasAlignment
+ val bias by animateFloatAsState(biased.horizontalBias, label = "AnimateHorizontalAlignment")
+ return derivedStateOf { BiasAlignment.Horizontal(bias) }
+}
+
+@PreviewMultipleThemes
+@Composable
+fun PreviewSearchTopBarActive() {
+ WireTheme {
+ SearchTopBar(
+ isSearchActive = true,
+ searchBarHint = "Search",
+ searchQuery = TextFieldValue(""),
+ onSearchQueryChanged = {},
+ onActiveChanged = {},
+ )
+ }
+}
+
+@PreviewMultipleThemes
+@Composable
+fun PreviewSearchTopBarInactive() {
+ WireTheme {
+ SearchTopBar(
+ isSearchActive = false,
+ searchBarHint = "Search",
+ searchQuery = TextFieldValue(""),
+ onSearchQueryChanged = {},
+ onActiveChanged = {},
+ )
+ }
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt
index 411e95d7f94..5d8159ab260 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt
@@ -260,8 +260,7 @@ fun HomeContent(
searchBarHint = stringResource(R.string.search_bar_conversations_hint),
searchQuery = searchBarState.searchQuery,
onSearchQueryChanged = searchBarState::searchQueryChanged,
- onInputClicked = searchBarState::openSearch,
- onCloseSearchClicked = searchBarState::closeSearch,
+ onActiveChanged = searchBarState::searchActiveChanged,
)
}
},
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt
index 18caf570ff2..f674e3c55f3 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt
@@ -39,6 +39,7 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -912,9 +913,17 @@ fun JumpToLastMessageButton(
) {
SmallFloatingActionButton(
onClick = { coroutineScope.launch { lazyListState.animateScrollToItem(0) } },
- containerColor = MaterialTheme.wireColorScheme.onSecondaryButtonDisabled,
- contentColor = MaterialTheme.wireColorScheme.secondaryButtonDisabled,
+ containerColor = MaterialTheme.wireColorScheme.scrollToBottomButtonColor,
+ contentColor = MaterialTheme.wireColorScheme.onScrollToBottomButtonColor,
shape = CircleShape,
+ elevation = FloatingActionButtonDefaults.elevation(dimensions().spacing0x),
+ modifier = Modifier
+ .padding(
+ PaddingValues(
+ bottom = dimensions().typingIndicatorHeight + dimensions().spacing8x,
+ end = dimensions().spacing16x
+ )
+ )
) {
Icon(
imageVector = Icons.Default.KeyboardArrowDown,
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationTopAppBar.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationTopAppBar.kt
index 3b048f551e4..1459ad286a9 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationTopAppBar.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationTopAppBar.kt
@@ -20,11 +20,11 @@
package com.wire.android.ui.home.conversations
-import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -48,6 +48,8 @@ import com.wire.android.R
import com.wire.android.model.UserAvatarData
import com.wire.android.ui.calling.controlbuttons.JoinButton
import com.wire.android.ui.calling.controlbuttons.StartCallButton
+import com.wire.android.ui.common.MLSVerifiedIcon
+import com.wire.android.ui.common.ProteusVerifiedIcon
import com.wire.android.ui.common.UserProfileAvatar
import com.wire.android.ui.common.button.WireSecondaryIconButton
import com.wire.android.ui.common.colorsScheme
@@ -133,7 +135,11 @@ private fun ConversationScreenTopAppBarContent(
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(weight = 1f, fill = false)
)
- VerificationIcon(conversationInfoViewState.protocolInfo, conversationInfoViewState.verificationStatus)
+ VerificationIcons(
+ conversationInfoViewState.protocolInfo,
+ conversationInfoViewState.mlsVerificationStatus,
+ conversationInfoViewState.proteusVerificationStatus
+ )
if (isDropDownEnabled && isInteractionEnabled) {
Icon(
painter = painterResource(id = R.drawable.ic_dropdown_icon),
@@ -181,21 +187,29 @@ private fun ConversationScreenTopAppBarContent(
}
@Composable
-private fun VerificationIcon(protocolInfo: Conversation.ProtocolInfo?, verificationStatus: Conversation.VerificationStatus?) {
- if (verificationStatus != Conversation.VerificationStatus.VERIFIED || protocolInfo == null) return
-
- val (iconId, contentDescriptionId) = when (protocolInfo) {
- is Conversation.ProtocolInfo.MLS ->
- R.drawable.ic_certificate_valid_mls to R.string.content_description_mls_certificate_valid
+private fun RowScope.VerificationIcons(
+ protocolInfo: Conversation.ProtocolInfo?,
+ mlsVerificationStatus: Conversation.VerificationStatus?,
+ proteusVerificationStatus: Conversation.VerificationStatus?
+) {
+ val mlsIcon: @Composable () -> Unit = {
+ if (mlsVerificationStatus == Conversation.VerificationStatus.VERIFIED) {
+ MLSVerifiedIcon(contentDescriptionId = R.string.content_description_mls_certificate_valid)
+ }
+ }
+ val proteusIcon: @Composable () -> Unit = {
+ if (proteusVerificationStatus == Conversation.VerificationStatus.VERIFIED) {
+ ProteusVerifiedIcon(contentDescriptionId = R.string.content_description_proteus_certificate_valid)
+ }
+ }
- is Conversation.ProtocolInfo.Proteus, is Conversation.ProtocolInfo.Mixed ->
- R.drawable.ic_certificate_valid_proteus to R.string.content_description_proteus_certificate_valid
+ if (protocolInfo is Conversation.ProtocolInfo.Proteus) {
+ proteusIcon()
+ mlsIcon()
+ } else {
+ mlsIcon()
+ proteusIcon()
}
- Image(
- modifier = Modifier.padding(start = dimensions().spacing4x),
- painter = painterResource(id = iconId),
- contentDescription = stringResource(contentDescriptionId)
- )
}
@Composable
@@ -371,3 +385,30 @@ fun PreviewConversationScreenTopAppBarShortTitleWithOngoingCall() {
isSearchEnabled = false
)
}
+
+@Preview("Topbar with a short conversation title and verified")
+@Composable
+fun PreviewConversationScreenTopAppBarShortTitleWithVerified() {
+ val conversationId = QualifiedID("", "")
+ ConversationScreenTopAppBarContent(
+ ConversationInfoViewState(
+ conversationId = ConversationId("value", "domain"),
+ conversationName = UIText.DynamicString("Short title"),
+ conversationDetailsData = ConversationDetailsData.Group(conversationId),
+ conversationAvatar = ConversationAvatar.Group(conversationId),
+ protocolInfo = Conversation.ProtocolInfo.Proteus,
+ proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED,
+ mlsVerificationStatus = Conversation.VerificationStatus.VERIFIED
+ ),
+ onBackButtonClick = {},
+ onDropDownClick = {},
+ isDropDownEnabled = true,
+ onSearchButtonClick = {},
+ onPhoneButtonClick = {},
+ hasOngoingCall = false,
+ onJoinCallButtonClick = {},
+ onPermanentPermissionDecline = {},
+ isInteractionEnabled = true,
+ isSearchEnabled = false
+ )
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt
index ca7dd4d3c21..bac6cd25114 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt
@@ -47,6 +47,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.ui.graphics.Color
import com.wire.android.R
import com.wire.android.media.audiomessage.AudioState
import com.wire.android.model.Clickable
@@ -106,7 +107,10 @@ fun MessageItem(
onSelfDeletingMessageRead: (UIMessage) -> Unit,
onFailedMessageRetryClicked: (String) -> Unit = {},
onFailedMessageCancelClicked: (String) -> Unit = {},
- onLinkClick: (String) -> Unit = {}
+ onLinkClick: (String) -> Unit = {},
+ defaultBackgroundColor: Color = Color.Transparent,
+ shouldDisplayMessageStatus: Boolean = true,
+ shouldDisplayFooter: Boolean = true
) {
with(message) {
val selfDeletionTimerState = rememberSelfDeletionTimer(header.messageStatus.expirationStatus)
@@ -133,7 +137,7 @@ fun MessageItem(
Modifier.background(color)
} else {
- Modifier
+ Modifier.background(defaultBackgroundColor)
}
Box(backgroundColorModifier) {
@@ -236,7 +240,7 @@ fun MessageItem(
onLinkClick = onLinkClick
)
}
- if (isMyMessage) {
+ if (isMyMessage && shouldDisplayMessageStatus) {
MessageStatusIndicator(
status = message.header.messageStatus.flowStatus,
isGroupConversation = conversationDetailsData is ConversationDetailsData.Group,
@@ -249,10 +253,12 @@ fun MessageItem(
HorizontalSpace.x24()
}
}
- MessageFooter(
- messageFooter,
- onReactionClicked
- )
+ if (shouldDisplayFooter) {
+ MessageFooter(
+ messageFooter = messageFooter,
+ onReactionClicked = onReactionClicked
+ )
+ }
} else {
MessageDecryptionFailure(
messageHeader = header,
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt
index 9266e57dc6e..72f670f15dd 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt
@@ -265,6 +265,7 @@ private fun getColorFilter(message: SystemMessage): ColorFilter? {
is SystemMessage.ConversationMessageTimerDeactivated,
is SystemMessage.FederationMemberRemoved,
is SystemMessage.FederationStopped,
+ is SystemMessage.ConversationMessageCreatedUnverifiedWarning,
is SystemMessage.MLSWrongEpochWarning -> ColorFilter.tint(colorsScheme().onBackground)
}
}
@@ -495,6 +496,7 @@ private val SystemMessage.expandable
is SystemMessage.ConversationDegraded -> false
is SystemMessage.ConversationVerified -> false
is SystemMessage.FederationStopped -> false
+ is SystemMessage.ConversationMessageCreatedUnverifiedWarning -> false
}
private fun List.toUserNamesListString(res: Resources): String = when {
@@ -581,6 +583,7 @@ fun SystemMessage.annotatedString(
)
is SystemMessage.FederationStopped -> domainList.toTypedArray()
+ is SystemMessage.ConversationMessageCreatedUnverifiedWarning -> arrayOf()
}
return res.annotatedText(stringResId, normalStyle, boldStyle, normalColor, boldColor, errorColor, isErrorString, *args)
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt
index c210cef85a7..73e2350db24 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt
@@ -21,9 +21,14 @@
package com.wire.android.ui.home.conversations.details
import androidx.annotation.StringRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.LocalOverscrollConfiguration
-import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
@@ -32,6 +37,8 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -44,10 +51,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
@@ -60,7 +69,10 @@ import com.wire.android.appLogger
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.Navigator
import com.wire.android.navigation.style.PopUpNavigationAnimation
+import com.wire.android.ui.common.CollapsingTopBarScaffold
+import com.wire.android.ui.common.MLSVerifiedIcon
import com.wire.android.ui.common.MoreOptionIcon
+import com.wire.android.ui.common.ProteusVerifiedIcon
import com.wire.android.ui.common.TabItem
import com.wire.android.ui.common.WireTabRow
import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout
@@ -69,11 +81,12 @@ import com.wire.android.ui.common.bottomsheet.conversation.rememberConversationS
import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState
import com.wire.android.ui.common.calculateCurrentTab
import com.wire.android.ui.common.dialogs.ArchiveConversationDialog
-import com.wire.android.ui.common.scaffold.WireScaffold
+import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.common.topBarElevation
import com.wire.android.ui.common.topappbar.NavigationIconType
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
+import com.wire.android.ui.common.topappbar.WireTopAppBarTitle
import com.wire.android.ui.common.visbility.rememberVisibilityState
import com.wire.android.ui.destinations.AddMembersSearchScreenDestination
import com.wire.android.ui.destinations.EditConversationNameScreenDestination
@@ -81,6 +94,7 @@ import com.wire.android.ui.destinations.EditGuestAccessScreenDestination
import com.wire.android.ui.destinations.EditSelfDeletingMessagesScreenDestination
import com.wire.android.ui.destinations.GroupConversationAllParticipantsScreenDestination
import com.wire.android.ui.destinations.OtherUserProfileScreenDestination
+import com.wire.android.ui.destinations.SearchConversationMessagesScreenDestination
import com.wire.android.ui.destinations.SelfUserProfileScreenDestination
import com.wire.android.ui.destinations.ServiceDetailsScreenDestination
import com.wire.android.ui.home.conversations.details.dialog.ClearConversationContentDialog
@@ -95,8 +109,11 @@ import com.wire.android.ui.home.conversations.details.participants.model.UIParti
import com.wire.android.ui.home.conversationslist.model.DialogState
import com.wire.android.ui.home.conversationslist.model.GroupDialogState
import com.wire.android.ui.theme.WireTheme
+import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireDimensions
+import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.ui.UIText
+import com.wire.kalium.logic.data.conversation.Conversation
import kotlinx.coroutines.launch
@RootNavGraph
@@ -116,6 +133,16 @@ fun GroupConversationDetailsScreen(
val snackbarHostState = LocalSnackbarHostState.current
val showSnackbarMessage: (UIText) -> Unit = remember { { scope.launch { snackbarHostState.showSnackbar(it.asString(resources)) } } }
+ val onSearchConversationMessagesClick: () -> Unit = {
+ navigator.navigate(
+ NavigationCommand(
+ SearchConversationMessagesScreenDestination(
+ conversationId = viewModel.conversationId
+ )
+ )
+ )
+ }
+
GroupConversationDetailsContent(
conversationSheetContent = viewModel.conversationSheetContent,
bottomSheetEventsHandler = viewModel,
@@ -195,7 +222,8 @@ fun GroupConversationDetailsScreen(
onEditGroupName = {
navigator.navigate(NavigationCommand(EditConversationNameScreenDestination(viewModel.conversationId)))
},
- isLoading = viewModel.requestInProgress
+ isLoading = viewModel.requestInProgress,
+ onSearchConversationMessagesClick = onSearchConversationMessagesClick
)
val tryAgainSnackBarMessage = stringResource(id = R.string.error_unknown_message)
@@ -235,7 +263,8 @@ private fun GroupConversationDetailsContent(
onLeaveGroup: (GroupDialogState) -> Unit,
onDeleteGroup: (GroupDialogState) -> Unit,
groupParticipantsState: GroupConversationParticipantsState,
- isLoading: Boolean
+ isLoading: Boolean,
+ onSearchConversationMessagesClick: () -> Unit
) {
val scope = rememberCoroutineScope()
val resources = LocalContext.current.resources
@@ -281,64 +310,94 @@ private fun GroupConversationDetailsContent(
clearConversationDialogState.dismiss()
archiveConversationDialogState.dismiss()
}
- WireScaffold(
- topBar = {
+
+ CollapsingTopBarScaffold(
+ topBarHeader = {
WireCenterAlignedTopAppBar(
elevation = elevationState,
- title = stringResource(R.string.conversation_details_title),
+ titleContent = {
+ WireTopAppBarTitle(
+ title = stringResource(R.string.conversation_details_title),
+ style = MaterialTheme.wireTypography.title01,
+ maxLines = 2
+ )
+ VerificationInfo(conversationSheetContent)
+ },
navigationIconType = NavigationIconType.Close,
onNavigationPressed = onBackPressed,
actions = { MoreOptionIcon(onButtonClicked = openBottomSheet) }
- ) {
- WireTabRow(
- tabs = GroupConversationDetailsTabItem.entries,
- selectedTabIndex = currentTabState,
- onTabChange = { scope.launch { pagerState.animateScrollToPage(it) } },
- modifier = Modifier.padding(top = MaterialTheme.wireDimensions.spacing16x),
- divider = {} // no divider
+ )
+ },
+ topBarCollapsing = {
+ conversationSheetState.conversationSheetContent?.let {
+ GroupConversationDetailsTopBarCollapsing(
+ title = it.title,
+ conversationId = it.conversationId,
+ totalParticipants = groupParticipantsState.data.allCount,
+ isLoading = isLoading,
+ onSearchConversationMessagesClick = onSearchConversationMessagesClick
)
}
},
- modifier = Modifier.fillMaxHeight(),
- ) { internalPadding ->
- var focusedTabIndex: Int by remember { mutableStateOf(initialPageIndex) }
- val keyboardController = LocalSoftwareKeyboardController.current
- val focusManager = LocalFocusManager.current
-
- CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
- HorizontalPager(
- state = pagerState,
- modifier = Modifier
- .fillMaxWidth()
- .padding(internalPadding)
- ) { pageIndex ->
- when (GroupConversationDetailsTabItem.entries[pageIndex]) {
- GroupConversationDetailsTabItem.OPTIONS -> GroupConversationOptions(
- lazyListState = lazyListStates[pageIndex],
- onEditGuestAccess = onEditGuestAccess,
- onEditSelfDeletingMessages = onEditSelfDeletingMessages,
- onEditGroupName = onEditGroupName
- )
-
- GroupConversationDetailsTabItem.PARTICIPANTS -> GroupConversationParticipants(
- groupParticipantsState = groupParticipantsState,
- openFullListPressed = openFullListPressed,
- onAddParticipantsPressed = onAddParticipantsPressed,
- onProfilePressed = onProfilePressed,
- lazyListState = lazyListStates[pageIndex]
+ topBarFooter = {
+ AnimatedVisibility(
+ visible = conversationSheetState.conversationSheetContent != null,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ Surface(
+ shadowElevation = elevationState,
+ color = MaterialTheme.wireColorScheme.background
+ ) {
+ WireTabRow(
+ tabs = GroupConversationDetailsTabItem.entries,
+ selectedTabIndex = currentTabState,
+ onTabChange = { scope.launch { pagerState.animateScrollToPage(it) } },
+ modifier = Modifier.padding(top = MaterialTheme.wireDimensions.spacing16x),
+ divider = {} // no divider
)
}
}
+ },
+ content = {
+ var focusedTabIndex: Int by remember { mutableStateOf(initialPageIndex) }
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val focusManager = LocalFocusManager.current
+
+ CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
+ HorizontalPager(
+ state = pagerState,
+ modifier = Modifier
+ .fillMaxWidth()
+ ) { pageIndex ->
+ when (GroupConversationDetailsTabItem.entries[pageIndex]) {
+ GroupConversationDetailsTabItem.OPTIONS -> GroupConversationOptions(
+ lazyListState = lazyListStates[pageIndex],
+ onEditGuestAccess = onEditGuestAccess,
+ onEditSelfDeletingMessages = onEditSelfDeletingMessages,
+ onEditGroupName = onEditGroupName
+ )
- LaunchedEffect(pagerState.isScrollInProgress, focusedTabIndex, pagerState.currentPage) {
- if (!pagerState.isScrollInProgress && focusedTabIndex != pagerState.currentPage) {
- keyboardController?.hide()
- focusManager.clearFocus()
- focusedTabIndex = pagerState.currentPage
+ GroupConversationDetailsTabItem.PARTICIPANTS -> GroupConversationParticipants(
+ groupParticipantsState = groupParticipantsState,
+ openFullListPressed = openFullListPressed,
+ onAddParticipantsPressed = onAddParticipantsPressed,
+ onProfilePressed = onProfilePressed,
+ lazyListState = lazyListStates[pageIndex]
+ )
+ }
+ }
+
+ LaunchedEffect(pagerState.isScrollInProgress, focusedTabIndex, pagerState.currentPage) {
+ if (!pagerState.isScrollInProgress && focusedTabIndex != pagerState.currentPage) {
+ keyboardController?.hide()
+ focusManager.clearFocus()
+ focusedTabIndex = pagerState.currentPage
+ }
}
}
}
- }
+ )
WireModalSheetLayout(
sheetState = sheetState,
@@ -406,6 +465,59 @@ private fun GroupConversationDetailsContent(
)
}
+@Composable
+private fun VerificationInfo(conversationSheetContent: ConversationSheetContent?) {
+ if (conversationSheetContent == null) return
+
+ val isProteusVerified = conversationSheetContent.proteusVerificationStatus == Conversation.VerificationStatus.VERIFIED
+ val isMlsVerified = conversationSheetContent.mlsVerificationStatus == Conversation.VerificationStatus.VERIFIED
+ val isProteusProtocol = conversationSheetContent.protocol == Conversation.ProtocolInfo.Proteus
+
+ if (isProteusVerified && (isProteusProtocol || !isMlsVerified)) {
+ ProteusVerifiedLabel()
+ } else if (isMlsVerified) {
+ MLSVerifiedLabel()
+ }
+}
+
+@Composable
+private fun MLSVerifiedLabel() {
+ VerifiedLabel(
+ stringResource(id = R.string.label_conversations_details_verified_mls).uppercase(),
+ MaterialTheme.wireColorScheme.mlsVerificationTextColor
+ ) { MLSVerifiedIcon() }
+}
+
+@Composable
+private fun ProteusVerifiedLabel() {
+ VerifiedLabel(
+ stringResource(id = R.string.label_conversations_details_verified_proteus).uppercase(),
+ MaterialTheme.wireColorScheme.primary
+ ) { ProteusVerifiedIcon() }
+}
+
+@Composable
+private fun VerifiedLabel(text: String, color: Color, icon: @Composable RowScope.() -> Unit = {}) {
+ Row(
+ modifier = Modifier
+ .padding(top = dimensions().spacing4x)
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Text(
+ modifier = Modifier.padding(
+ start = dimensions().spacing6x,
+ end = dimensions().spacing6x
+ ),
+ text = text,
+ style = MaterialTheme.wireTypography.label01,
+ color = color,
+ overflow = TextOverflow.Ellipsis
+ )
+ icon()
+ }
+}
+
enum class GroupConversationDetailsTabItem(@StringRes override val titleResId: Int) : TabItem {
OPTIONS(R.string.conversation_details_options_tab),
PARTICIPANTS(R.string.conversation_details_participants_tab);
@@ -428,7 +540,8 @@ fun PreviewGroupConversationDetails() {
isLoading = false,
onEditGroupName = {},
onEditSelfDeletingMessages = {},
- onEditGuestAccess = {}
+ onEditGuestAccess = {},
+ onSearchConversationMessagesClick = {}
)
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsTopBarCollapsing.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsTopBarCollapsing.kt
new file mode 100644
index 00000000000..d2cf6f777da
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsTopBarCollapsing.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.details
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.constraintlayout.compose.ConstraintLayout
+import com.wire.android.R
+import com.wire.android.ui.common.colorsScheme
+import com.wire.android.ui.common.conversationColor
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesButton
+import com.wire.android.ui.home.conversationslist.common.GroupConversationAvatar
+import com.wire.android.ui.theme.wireColorScheme
+import com.wire.android.ui.theme.wireTypography
+import com.wire.android.util.ui.UIText
+import com.wire.kalium.logic.data.id.ConversationId
+
+@Composable
+fun GroupConversationDetailsTopBarCollapsing(
+ title: String,
+ conversationId: ConversationId,
+ totalParticipants: Int,
+ isLoading: Boolean,
+ onSearchConversationMessagesClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ modifier = modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ GroupConversationAvatar(
+ color = colorsScheme().conversationColor(id = conversationId),
+ size = dimensions().groupAvatarConversationDetailsTopBarSize,
+ cornerRadius = dimensions().groupAvatarConversationDetailsCornerRadius,
+ padding = dimensions().avatarConversationTopBarClickablePadding,
+ )
+ }
+ ConstraintLayout(
+ Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .animateContentSize()
+ ) {
+ val (userDescription) = createRefs()
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .wrapContentSize()
+ .constrainAs(userDescription) {
+ top.linkTo(parent.top)
+ bottom.linkTo(parent.bottom)
+ start.linkTo(parent.start)
+ end.linkTo(parent.end)
+ }
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(
+ horizontal = dimensions().spacing16x,
+ vertical = dimensions().spacing4x
+ )
+ ) {
+ Text(
+ text = title.ifBlank {
+ if (isLoading) ""
+ else UIText.StringResource(R.string.group_unavailable_label).asString()
+ },
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ style = MaterialTheme.wireTypography.body02,
+ color = MaterialTheme.colorScheme.onBackground
+ )
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(horizontal = dimensions().spacing64x)
+ ) {
+ Text(
+ text = stringResource(
+ id = R.string.conversation_details_participants_count,
+ totalParticipants
+ ),
+ style = MaterialTheme.wireTypography.subline01,
+ color = MaterialTheme.wireColorScheme.secondaryText
+ )
+ }
+ }
+ }
+
+ SearchConversationMessagesButton(
+ onSearchConversationMessagesClick = onSearchConversationMessagesClick
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt
index b257ace5767..c86b4087e48 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModel.kt
@@ -150,7 +150,10 @@ class GroupConversationDetailsViewModel @Inject constructor(
conversationTypeDetail = ConversationTypeDetail.Group(conversationId, groupDetails.isSelfUserCreator),
isTeamConversation = groupDetails.conversation.teamId?.value != null,
selfRole = groupDetails.selfRole,
- isArchived = groupDetails.conversation.archived
+ isArchived = groupDetails.conversation.archived,
+ protocol = groupDetails.conversation.protocol,
+ mlsVerificationStatus = groupDetails.conversation.mlsVerificationStatus,
+ proteusVerificationStatus = groupDetails.conversation.proteusVerificationStatus
)
val isGuestAllowed = groupDetails.conversation.isGuestAllowed() || groupDetails.conversation.isNonTeamMemberAllowed()
val isUpdatingReadReceiptAllowed = if (selfTeam == null) {
@@ -387,7 +390,14 @@ class GroupConversationDetailsViewModel @Inject constructor(
viewModelScope.launch {
val shouldArchive = dialogState.isArchived.not()
requestInProgress = true
- val result = withContext(dispatcher.io()) { updateConversationArchivedStatus(conversationId, shouldArchive, timestamp) }
+ val result = withContext(dispatcher.io()) {
+ updateConversationArchivedStatus(
+ conversationId = conversationId,
+ shouldArchiveConversation = shouldArchive,
+ onlyLocally = !dialogState.isMember,
+ archivedStatusTimestamp = timestamp
+ )
+ }
requestInProgress = false
when (result) {
ArchiveStatusUpdateResult.Failure -> onMessage(
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt
index dc6cdd4f606..3ef82c796bd 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModel.kt
@@ -118,7 +118,8 @@ class ConversationInfoViewModel @Inject constructor(
hasUserPermissionToEdit = detailsData !is ConversationDetailsData.None,
conversationType = conversationDetails.conversation.type,
protocolInfo = conversationDetails.conversation.protocol,
- verificationStatus = conversationDetails.conversation.verificationStatus
+ mlsVerificationStatus = conversationDetails.conversation.mlsVerificationStatus,
+ proteusVerificationStatus = conversationDetails.conversation.proteusVerificationStatus
)
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt
index b8b11cc394f..de4822c984d 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewState.kt
@@ -36,7 +36,8 @@ data class ConversationInfoViewState(
val hasUserPermissionToEdit: Boolean = false,
val conversationType: Conversation.Type = Conversation.Type.ONE_ON_ONE,
val protocolInfo: Conversation.ProtocolInfo? = null,
- val verificationStatus: Conversation.VerificationStatus? = null,
+ val mlsVerificationStatus: Conversation.VerificationStatus? = null,
+ val proteusVerificationStatus: Conversation.VerificationStatus? = null,
)
sealed class ConversationDetailsData {
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt
index 721a6780304..eea235af9fc 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt
@@ -193,6 +193,8 @@ sealed class UILastMessageContent {
data class MultipleMessage(val messages: List, val separator: String = " ") : UILastMessageContent()
data class Connection(val connectionState: ConnectionState, val userId: UserId) : UILastMessageContent()
+
+ data class VerificationChanged(@StringRes val textResId: Int) : UILastMessageContent()
}
sealed class UIMessageContent {
@@ -423,7 +425,8 @@ sealed class UIMessageContent {
object HistoryLost : SystemMessage(
R.drawable.ic_info,
R.string.label_system_message_conversation_history_lost,
- true)
+ true
+ )
object HistoryLostProtocolChanged : SystemMessage(
R.drawable.ic_info,
@@ -467,7 +470,8 @@ sealed class UIMessageContent {
data class ConversationDegraded(val protocol: Conversation.Protocol) : SystemMessage(
if (protocol == Conversation.Protocol.MLS) R.drawable.ic_conversation_degraded_mls
else R.drawable.ic_shield_holo,
- R.string.label_system_message_conversation_degraded
+ if (protocol == Conversation.Protocol.MLS) R.string.label_system_message_conversation_degraded_mls
+ else R.string.label_system_message_conversation_degraded_proteus
)
data class ConversationVerified(val protocol: Conversation.Protocol) : SystemMessage(
@@ -476,6 +480,11 @@ sealed class UIMessageContent {
if (protocol == Conversation.Protocol.MLS) R.string.label_system_message_conversation_verified_mls
else R.string.label_system_message_conversation_verified_proteus
)
+
+ data object ConversationMessageCreatedUnverifiedWarning : SystemMessage(
+ R.drawable.ic_info,
+ R.string.label_system_message_conversation_started_sensitive_information
+ )
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt
index a941bee7d42..296b2304612 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/SearchPeopleRouter.kt
@@ -111,15 +111,12 @@ fun SearchPeopleScreen(
}
},
topBarCollapsing = {
- val onInputClicked: () -> Unit = remember { { searchBarState.openSearch() } }
- val onCloseSearchClicked: () -> Unit = remember { { searchBarState.closeSearch() } }
SearchTopBar(
isSearchActive = searchBarState.isSearchActive,
searchBarHint = stringResource(R.string.label_search_people),
searchQuery = searchQuery,
onSearchQueryChanged = onSearchQueryChanged,
- onInputClicked = onInputClicked,
- onCloseSearchClicked = onCloseSearchClicked
+ onActiveChanged = searchBarState::searchActiveChanged,
) {
if (screenType == SearchPeopleScreenType.CONVERSATION_DETAILS
&& searchPeopleState.isServicesAllowed
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesButton.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesButton.kt
new file mode 100644
index 00000000000..bf09fee6a11
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesButton.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.search.messages
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
+import com.wire.android.R
+import com.wire.android.ui.common.button.WireSecondaryButton
+import com.wire.android.ui.common.dimensions
+
+@Composable
+fun SearchConversationMessagesButton(
+ onSearchConversationMessagesClick: () -> Unit
+) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .wrapContentSize()
+ .fillMaxWidth()
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(
+ top = dimensions().spacing24x,
+ start = dimensions().spacing16x,
+ end = dimensions().spacing16x,
+ )
+ .fillMaxWidth()
+ ) {
+ WireSecondaryButton(
+ text = stringResource(R.string.label_search_button),
+ onClick = onSearchConversationMessagesClick,
+ minSize = DpSize(dimensions().spacing0x, dimensions().spacing48x),
+ fillMaxWidth = true,
+ leadingIcon = {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_search),
+ contentDescription = stringResource(R.string.label_search_messages),
+ tint = MaterialTheme.colorScheme.onBackground,
+ modifier = Modifier.padding(end = dimensions().spacing8x)
+ )
+ }
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesEmptyScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesEmptyScreen.kt
new file mode 100644
index 00000000000..3397a2cfc8b
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesEmptyScreen.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.search.messages
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import com.wire.android.BuildConfig
+import com.wire.android.R
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.theme.WireTheme
+import com.wire.android.ui.theme.wireColorScheme
+import com.wire.android.ui.theme.wireTypography
+import com.wire.android.util.CustomTabsHelper
+import com.wire.android.util.ui.PreviewMultipleThemes
+
+@Composable
+fun SearchConversationMessagesEmptyScreen() {
+ val context = LocalContext.current
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.height(dimensions().spacing16x))
+ Text(
+ text = stringResource(R.string.label_search_messages_empty_title),
+ style = MaterialTheme.wireTypography.body01.copy(color = MaterialTheme.wireColorScheme.secondaryText),
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.height(dimensions().spacing8x))
+ Text(
+ text = stringResource(R.string.label_learn_more),
+ style = MaterialTheme.wireTypography.body02.copy(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colorScheme.primary
+ ),
+ modifier = Modifier.clickable {
+ CustomTabsHelper.launchUrl(
+ context,
+ LEARN_ABOUT_SEARCH_URL
+ )
+ }
+ )
+ }
+ }
+}
+
+private const val LEARN_ABOUT_SEARCH_URL =
+ "${BuildConfig.URL_SUPPORT}/hc/en-us/articles/115001426529-Search-in-a-conversation"
+
+@PreviewMultipleThemes
+@Composable
+fun previewSearchConversationMessagesEmptyScreen() {
+ WireTheme {
+ SearchConversationMessagesEmptyScreen()
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNavArgs.kt
new file mode 100644
index 00000000000..cc96a1bbe68
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNavArgs.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.search.messages
+
+import com.wire.kalium.logic.data.id.ConversationId
+
+data class SearchConversationMessagesNavArgs(
+ val conversationId: ConversationId
+)
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNoResultsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNoResultsScreen.kt
new file mode 100644
index 00000000000..a33bc202ab4
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesNoResultsScreen.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.search.messages
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import com.wire.android.R
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.theme.WireTheme
+import com.wire.android.ui.theme.wireColorScheme
+import com.wire.android.ui.theme.wireTypography
+import com.wire.android.util.ui.PreviewMultipleThemes
+
+@Composable
+fun SearchConversationMessagesNoResultsScreen() {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Spacer(modifier = Modifier.height(dimensions().spacing16x))
+ Text(
+ text = stringResource(R.string.label_search_messages_no_results),
+ style = MaterialTheme.wireTypography.body01.copy(color = MaterialTheme.wireColorScheme.secondaryText),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+}
+
+@PreviewMultipleThemes
+@Composable
+fun previewSearchConversationMessagesNoResultsScreen() {
+ WireTheme {
+ SearchConversationMessagesNoResultsScreen()
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt
new file mode 100644
index 00000000000..0f3c65a04f6
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.search.messages
+
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import com.wire.android.ui.common.colorsScheme
+import com.wire.android.ui.home.conversations.MessageItem
+import com.wire.android.ui.home.conversations.info.ConversationDetailsData
+import com.wire.android.ui.home.conversations.mock.mockMessageWithText
+import com.wire.android.ui.home.conversations.model.UIMessage
+import com.wire.android.ui.theme.WireTheme
+import com.wire.android.util.ui.PreviewMultipleThemes
+
+@Composable
+fun SearchConversationMessagesResultsScreen(
+ searchResult: List
+) {
+ LazyColumn {
+ items(searchResult) { message ->
+ when (message) {
+ is UIMessage.Regular -> {
+ MessageItem(
+ message = message,
+ conversationDetailsData = ConversationDetailsData.None,
+ audioMessagesState = mapOf(),
+ onLongClicked = { },
+ onAssetMessageClicked = { },
+ onAudioClick = { },
+ onChangeAudioPosition = { _, _ -> },
+ onImageMessageClicked = { _, _ -> },
+ onOpenProfile = { },
+ onReactionClicked = { _, _ -> },
+ onResetSessionClicked = { _, _ -> },
+ onSelfDeletingMessageRead = { },
+ defaultBackgroundColor = colorsScheme().backgroundVariant,
+ shouldDisplayMessageStatus = false,
+ shouldDisplayFooter = false
+ )
+ }
+ is UIMessage.System -> { }
+ }
+ }
+ }
+}
+
+@PreviewMultipleThemes
+@Composable
+fun previewSearchConversationMessagesResultsScreen() {
+ WireTheme {
+ SearchConversationMessagesResultsScreen(
+ searchResult = listOf(
+ mockMessageWithText,
+ mockMessageWithText,
+ )
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt
new file mode 100644
index 00000000000..a4dbff8b8ca
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesScreen.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.search.messages
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import com.wire.android.R
+import com.wire.android.navigation.Navigator
+import com.wire.android.navigation.style.PopUpNavigationAnimation
+import com.wire.android.ui.common.CollapsingTopBarScaffold
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.common.topappbar.search.SearchTopBar
+import com.wire.android.ui.home.conversations.model.UIMessage
+
+@RootNavGraph
+@Destination(
+ navArgsDelegate = SearchConversationMessagesNavArgs::class,
+ style = PopUpNavigationAnimation::class
+)
+@Composable
+fun SearchConversationMessagesScreen(
+ navigator: Navigator,
+ searchConversationMessagesViewModel: SearchConversationMessagesViewModel = hiltViewModel()
+) {
+ with(searchConversationMessagesViewModel.searchConversationMessagesState) {
+ CollapsingTopBarScaffold(
+ topBarHeader = { },
+ topBarCollapsing = {
+ SearchTopBar(
+ isSearchActive = true, // we want the search to be always active and back arrow visible on this particular screen
+ searchBarHint = stringResource(id = R.string.label_search_messages),
+ searchQuery = searchQuery,
+ onSearchQueryChanged = searchConversationMessagesViewModel::searchQueryChanged,
+ modifier = Modifier.padding(top = dimensions().spacing24x),
+ onCloseSearchClicked = navigator::navigateBack,
+ )
+ },
+ content = {
+ SearchConversationMessagesResultContent(
+ searchQuery = searchQuery.text,
+ noneSearchSucceed = isEmptyResult,
+ searchResult = searchResult
+ )
+ },
+ bottomBar = { },
+ snapOnFling = false,
+ keepElevationWhenCollapsed = true
+ )
+ }
+}
+
+@Composable
+fun SearchConversationMessagesResultContent(
+ searchQuery: String,
+ noneSearchSucceed: Boolean,
+ searchResult: List
+) {
+ if (searchQuery.isEmpty()) {
+ SearchConversationMessagesEmptyScreen()
+ } else {
+ if (noneSearchSucceed) {
+ SearchConversationMessagesNoResultsScreen()
+ } else {
+ SearchConversationMessagesResultsScreen(
+ searchResult = searchResult
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesState.kt
new file mode 100644
index 00000000000..43fa32f70b5
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesState.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.search.messages
+
+import androidx.compose.ui.text.input.TextFieldValue
+import com.wire.android.ui.home.conversations.model.UIMessage
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+data class SearchConversationMessagesState(
+ val searchQuery: TextFieldValue = TextFieldValue(""),
+ val searchResult: ImmutableList = persistentListOf(),
+ val isEmptyResult: Boolean = false
+)
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt
new file mode 100644
index 00000000000..5ea8bebdba9
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesViewModel.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.search.messages
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
+import androidx.lifecycle.viewmodel.compose.saveable
+import com.wire.android.ui.home.conversations.search.SearchPeopleViewModel
+import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase
+import com.wire.android.ui.navArgs
+import com.wire.kalium.logic.data.id.QualifiedID
+import com.wire.kalium.logic.functional.onSuccess
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class SearchConversationMessagesViewModel @Inject constructor(
+ private val getSearchMessagesForConversation: GetConversationMessagesFromSearchUseCase,
+ savedStateHandle: SavedStateHandle
+) : ViewModel() {
+
+ private val searchConversationMessagesNavArgs: SearchConversationMessagesNavArgs = savedStateHandle.navArgs()
+ private val conversationId: QualifiedID = searchConversationMessagesNavArgs.conversationId
+
+ @OptIn(SavedStateHandleSaveableApi::class)
+ var searchConversationMessagesState by savedStateHandle.saveable(
+ stateSaver = Saver(
+ save = { it.searchQuery.text },
+ restore = { SearchConversationMessagesState(searchQuery = TextFieldValue(it)) }
+ )
+ ) { mutableStateOf(SearchConversationMessagesState()) }
+
+ private val mutableSearchQueryFlow = MutableStateFlow(searchConversationMessagesState.searchQuery.text)
+
+ init {
+ viewModelScope.launch {
+ mutableSearchQueryFlow
+ .debounce(SearchPeopleViewModel.DEFAULT_SEARCH_QUERY_DEBOUNCE)
+ .collectLatest { searchTerm ->
+ getSearchMessagesForConversation(
+ searchTerm = searchTerm,
+ conversationId = conversationId
+ ).onSuccess { uiMessages ->
+ searchConversationMessagesState = searchConversationMessagesState.copy(
+ searchResult = uiMessages.toPersistentList(),
+ isEmptyResult = uiMessages.isEmpty()
+ )
+ }
+ }
+ }
+ }
+
+ fun searchQueryChanged(searchQuery: TextFieldValue) {
+ val textQueryChanged = searchConversationMessagesState.searchQuery.text != searchQuery.text
+ // we set the state with a searchQuery, immediately to update the UI first
+ searchConversationMessagesState = searchConversationMessagesState.copy(searchQuery = searchQuery)
+ if (textQueryChanged) {
+ viewModelScope.launch {
+ mutableSearchQueryFlow.emit(searchQuery.text)
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt
new file mode 100644
index 00000000000..716a98e1da4
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCase.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.usecase
+
+import com.wire.android.mapper.MessageMapper
+import com.wire.android.ui.home.conversations.model.UIMessage
+import com.wire.kalium.logic.CoreFailure
+import com.wire.kalium.logic.data.id.ConversationId
+import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase
+import com.wire.kalium.logic.feature.message.GetConversationMessagesFromSearchQueryUseCase
+import com.wire.kalium.logic.functional.Either
+import com.wire.kalium.logic.functional.map
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import javax.inject.Inject
+
+class GetConversationMessagesFromSearchUseCase @Inject constructor(
+ private val getConversationMessagesFromSearch: GetConversationMessagesFromSearchQueryUseCase,
+ private val observeMemberDetailsByIds: ObserveUserListByIdUseCase,
+ private val messageMapper: MessageMapper
+) {
+
+ /**
+ * This operation combines messages searched from a conversation and its respective user to UI
+ * @param searchQuery The search term used to define which messages will be returned.
+ * @param conversationId The conversation ID that it will look for messages in.
+ * @return A [Either>] indicating the success of the operation.
+ */
+ suspend operator fun invoke(
+ searchTerm: String,
+ conversationId: ConversationId
+ ): Either> =
+ if (searchTerm.length >= MINIMUM_CHARACTERS_TO_SEARCH) {
+ getConversationMessagesFromSearch(
+ searchQuery = searchTerm,
+ conversationId = conversationId
+ ).map { foundMessages ->
+ foundMessages.flatMap { messageItem ->
+ observeMemberDetailsByIds(
+ userIdList = messageMapper.memberIdList(
+ messages = foundMessages
+ )
+ ).map { usersList ->
+ messageMapper.toUIMessage(
+ userList = usersList,
+ message = messageItem
+ )?.let { listOf(it) } ?: emptyList()
+ }.first()
+ }
+ }
+ } else {
+ Either.Right(value = listOf())
+ }
+
+ private companion object {
+ const val MINIMUM_CHARACTERS_TO_SEARCH = 2
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt
index 4b511ff7522..a10b1ca9c2c 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModel.kt
@@ -414,8 +414,14 @@ class ConversationListViewModel @Inject constructor(
with(dialogState) {
viewModelScope.launch {
val isArchiving = !isArchived
+
requestInProgress = true
- val result = updateConversationArchivedStatus(conversationId, isArchiving, timestamp)
+ val result = updateConversationArchivedStatus(
+ conversationId = conversationId,
+ shouldArchiveConversation = isArchiving,
+ onlyLocally = !dialogState.isMember,
+ archivedStatusTimestamp = timestamp
+ )
requestInProgress = false
when (result) {
is ArchiveStatusUpdateResult.Failure -> {
@@ -486,7 +492,9 @@ private fun ConversationDetails.toConversationItem(
isSelfUserMember = isSelfUserMember,
teamId = conversation.teamId,
selfMemberRole = selfRole,
- isArchived = conversation.archived
+ isArchived = conversation.archived,
+ mlsVerificationStatus = conversation.mlsVerificationStatus,
+ proteusVerificationStatus = conversation.proteusVerificationStatus
)
}
@@ -517,7 +525,9 @@ private fun ConversationDetails.toConversationItem(
userId = otherUser.id,
blockingState = otherUser.BlockState,
teamId = otherUser.teamId,
- isArchived = conversation.archived
+ isArchived = conversation.archived,
+ mlsVerificationStatus = conversation.mlsVerificationStatus,
+ proteusVerificationStatus = conversation.proteusVerificationStatus
)
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt
index 19f18bd8d87..aef200a5964 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/ConversationItemFactory.kt
@@ -32,6 +32,8 @@ import com.wire.android.R
import com.wire.android.model.Clickable
import com.wire.android.model.UserAvatarData
import com.wire.android.ui.calling.controlbuttons.JoinButton
+import com.wire.android.ui.common.MLSVerifiedIcon
+import com.wire.android.ui.common.ProteusVerifiedIcon
import com.wire.android.ui.common.RowItemTemplate
import com.wire.android.ui.common.WireRadioButton
import com.wire.android.ui.common.colorsScheme
@@ -45,6 +47,7 @@ import com.wire.android.ui.home.conversationslist.model.ConversationInfo
import com.wire.android.ui.home.conversationslist.model.ConversationItem
import com.wire.android.ui.home.conversationslist.model.toUserInfoLabel
import com.wire.android.util.ui.UIText
+import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.conversation.MutedConversationStatus
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.QualifiedID
@@ -98,6 +101,8 @@ fun ConversationItemFactory(
)
is UILastMessageContent.Connection -> ConnectionLabel(connectionInfo = messageContent)
+ is UILastMessageContent.VerificationChanged -> LastMessageSubtitle(UIText.StringResource(messageContent.textResId))
+
else -> {}
}
}
@@ -110,6 +115,7 @@ fun ConversationItemFactory(
)
}
+@Suppress("ComplexMethod")
@Composable
private fun GeneralConversationItem(
searchQuery: String,
@@ -140,7 +146,15 @@ private fun GeneralConversationItem(
ConversationTitle(
name = groupName.ifEmpty { stringResource(id = R.string.member_name_deleted_label) },
isLegalHold = conversation.isLegalHold,
- searchQuery = searchQuery
+ searchQuery = searchQuery,
+ badges = {
+ if (proteusVerificationStatus == Conversation.VerificationStatus.VERIFIED) {
+ ProteusVerifiedIcon(contentDescriptionId = R.string.content_description_proteus_certificate_valid)
+ }
+ if (mlsVerificationStatus == Conversation.VerificationStatus.VERIFIED) {
+ MLSVerifiedIcon(contentDescriptionId = R.string.content_description_mls_certificate_valid)
+ }
+ }
)
},
subTitle = subTitle,
@@ -245,7 +259,9 @@ fun PreviewGroupConversationItemWithUnreadCount() {
badgeEventType = BadgeEventType.UnreadMessage(100),
selfMemberRole = null,
teamId = null,
- isArchived = false
+ isArchived = false,
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
),
searchQuery = "",
isSelectableItem = false,
@@ -268,7 +284,9 @@ fun PreviewGroupConversationItemWithNoBadges() {
badgeEventType = BadgeEventType.None,
selfMemberRole = null,
teamId = null,
- isArchived = false
+ isArchived = false,
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
),
searchQuery = "",
isSelectableItem = false,
@@ -291,7 +309,9 @@ fun PreviewGroupConversationItemWithMutedBadgeAndUnreadMentionBadge() {
badgeEventType = BadgeEventType.UnreadMention,
selfMemberRole = null,
teamId = null,
- isArchived = false
+ isArchived = false,
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
),
searchQuery = "",
isSelectableItem = false,
@@ -315,7 +335,9 @@ fun PreviewGroupConversationItemWithOngoingCall() {
selfMemberRole = null,
teamId = null,
hasOnGoingCall = true,
- isArchived = false
+ isArchived = false,
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
),
searchQuery = "",
isSelectableItem = false,
@@ -376,7 +398,9 @@ fun PreviewPrivateConversationItemWithBlockedBadge() {
blockingState = BlockingState.BLOCKED,
teamId = null,
userId = UserId("value", "domain"),
- isArchived = false
+ isArchived = false,
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
),
searchQuery = "",
isSelectableItem = false,
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/UserLabel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/UserLabel.kt
index 42034edf2ce..dd134bf1125 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/UserLabel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/common/UserLabel.kt
@@ -27,9 +27,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.wire.android.R
+import com.wire.android.ui.common.MLSVerifiedIcon
import com.wire.android.ui.common.MembershipQualifierLabel
+import com.wire.android.ui.common.ProteusVerifiedIcon
import com.wire.android.ui.home.conversationslist.model.Membership
import com.wire.android.ui.home.conversationslist.model.hasLabel
+import com.wire.kalium.logic.data.conversation.Conversation
@Composable
fun UserLabel(
@@ -47,6 +50,12 @@ fun UserLabel(
Spacer(modifier = Modifier.width(6.dp))
MembershipQualifierLabel(membership)
}
+ if (proteusVerificationStatus == Conversation.VerificationStatus.VERIFIED) {
+ ProteusVerifiedIcon(contentDescriptionId = R.string.content_description_proteus_certificate_valid)
+ }
+ if (mlsVerificationStatus == Conversation.VerificationStatus.VERIFIED) {
+ MLSVerifiedIcon(contentDescriptionId = R.string.content_description_mls_certificate_valid)
+ }
},
searchQuery = searchQuery
)
@@ -57,5 +66,7 @@ data class UserInfoLabel(
val labelName: String,
val isLegalHold: Boolean,
val membership: Membership,
- val unavailable: Boolean = false
+ val unavailable: Boolean = false,
+ val proteusVerificationStatus: Conversation.VerificationStatus? = null,
+ val mlsVerificationStatus: Conversation.VerificationStatus? = null,
)
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt
index be72a99c490..eef09b9ad42 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/ConversationItem.kt
@@ -40,6 +40,8 @@ sealed class ConversationItem {
abstract val badgeEventType: BadgeEventType
abstract val teamId: TeamId?
abstract val isArchived: Boolean
+ abstract val mlsVerificationStatus: Conversation.VerificationStatus
+ abstract val proteusVerificationStatus: Conversation.VerificationStatus
val isTeamConversation get() = teamId != null
@@ -56,6 +58,8 @@ sealed class ConversationItem {
override val badgeEventType: BadgeEventType,
override val teamId: TeamId?,
override val isArchived: Boolean,
+ override val mlsVerificationStatus: Conversation.VerificationStatus,
+ override val proteusVerificationStatus: Conversation.VerificationStatus
) : ConversationItem()
data class PrivateConversation(
@@ -69,7 +73,9 @@ sealed class ConversationItem {
override val lastMessageContent: UILastMessageContent?,
override val badgeEventType: BadgeEventType,
override val teamId: TeamId?,
- override val isArchived: Boolean
+ override val isArchived: Boolean,
+ override val mlsVerificationStatus: Conversation.VerificationStatus,
+ override val proteusVerificationStatus: Conversation.VerificationStatus
) : ConversationItem()
data class ConnectionConversation(
@@ -83,6 +89,8 @@ sealed class ConversationItem {
override val isArchived: Boolean = false,
) : ConversationItem() {
override val teamId: TeamId? = null
+ override val mlsVerificationStatus: Conversation.VerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ override val proteusVerificationStatus: Conversation.VerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
}
}
@@ -111,7 +119,9 @@ fun ConversationItem.PrivateConversation.toUserInfoLabel() =
labelName = conversationInfo.name,
isLegalHold = isLegalHold,
membership = conversationInfo.membership,
- unavailable = conversationInfo.isSenderUnavailable
+ unavailable = conversationInfo.isSenderUnavailable,
+ mlsVerificationStatus = mlsVerificationStatus,
+ proteusVerificationStatus = proteusVerificationStatus
)
fun ConversationItem.ConnectionConversation.toUserInfoLabel() =
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/GroupDialogState.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/GroupDialogState.kt
index cffea2a5b4c..4c172647caa 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/GroupDialogState.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/model/GroupDialogState.kt
@@ -32,5 +32,6 @@ data class DialogState(
val conversationId: ConversationId,
val conversationName: String,
val conversationTypeDetail: ConversationTypeDetail,
- val isArchived: Boolean
+ val isArchived: Boolean,
+ val isMember: Boolean
)
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AttachmentOptions.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AttachmentOptions.kt
index a0cc294d185..e32aba168c9 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AttachmentOptions.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/AttachmentOptions.kt
@@ -101,7 +101,7 @@ fun AttachmentOptionsComponent(
BoxWithConstraints(Modifier.fillMaxSize()) {
val fullWidth: Dp = with(density) { constraints.maxWidth.toDp() }
val minPadding: Dp = dimensions().spacing2x
- val minColumnWidth: Dp = with(density) { maxTextWidth.toDp() + dimensions().spacing24x }
+ val minColumnWidth: Dp = with(density) { maxTextWidth.toDp() + dimensions().spacing28x }
val visibleAttachmentOptions = attachmentOptions.filter { it.shouldShow }
val params by remember(fullWidth, visibleAttachmentOptions.size) {
derivedStateOf {
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt
index a84a792ddd0..900e5a2f26b 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/EnabledMessageComposer.kt
@@ -21,6 +21,7 @@ import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
@@ -131,7 +132,7 @@ fun EnabledMessageComposer(
if (!inputStateHolder.isTextExpanded) {
UsersTypingIndicatorForConversation(conversationId = conversationId)
}
- if (messageComposerViewState.value.mentionSearchResult.isNotEmpty()) {
+ if (!inputStateHolder.isTextExpanded && messageComposerViewState.value.mentionSearchResult.isNotEmpty()) {
MembersMentionList(
membersToMention = messageComposerViewState.value.mentionSearchResult,
searchQuery = messageComposition.value.messageText,
@@ -144,13 +145,14 @@ fun EnabledMessageComposer(
}
}
val fillRemainingSpaceOrWrapContent =
- if (!inputStateHolder.isTextExpanded) {
- Modifier.wrapContentHeight()
- } else {
+ if (inputStateHolder.isTextExpanded) {
Modifier.weight(1f)
+ } else {
+ Modifier.wrapContentHeight()
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Bottom,
modifier = fillRemainingSpaceOrWrapContent
.fillMaxWidth()
.background(color = colorsScheme().backgroundVariant)
@@ -162,7 +164,7 @@ fun EnabledMessageComposer(
}
if (additionalOptionStateHolder.additionalOptionsSubMenuState != AdditionalOptionSubMenuState.RecordAudio) {
- Box(fillRemainingSpaceOrWrapContent) {
+ Box(fillRemainingSpaceOrWrapContent, contentAlignment = Alignment.BottomCenter) {
var currentSelectedLineIndex by remember { mutableStateOf(0) }
var cursorCoordinateY by remember { mutableStateOf(0F) }
@@ -202,8 +204,7 @@ fun EnabledMessageComposer(
)
val mentionSearchResult = messageComposerViewState.value.mentionSearchResult
- if (mentionSearchResult.isNotEmpty() &&
- inputStateHolder.isTextExpanded
+ if (mentionSearchResult.isNotEmpty() && inputStateHolder.isTextExpanded
) {
DropDownMentionsSuggestions(
currentSelectedLineIndex = currentSelectedLineIndex,
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageActions.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageActions.kt
index 70c22843f20..03c000b712d 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageActions.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageActions.kt
@@ -25,9 +25,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
@@ -105,9 +105,8 @@ fun MessageEditActions(
Row(
modifier = modifier
.fillMaxWidth()
- .wrapContentSize()
+ .height(dimensions().spacing64x)
) {
-
Box( // we need to wrap it because button is smaller than minimum touch size so compose will add paddings to it to be 48dp anyway
modifier = Modifier.size(width = dimensions().spacing64x, height = dimensions().spacing56x),
contentAlignment = Alignment.CenterEnd
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt
index 7a58135ea31..7981a6849ff 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt
@@ -27,7 +27,6 @@ import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
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.heightIn
import androidx.compose.foundation.layout.padding
@@ -93,8 +92,6 @@ fun ActiveMessageComposerInput(
) {
Column(
modifier = modifier
- .wrapContentHeight()
- .fillMaxWidth()
.background(inputType.backgroundColor())
) {
Divider(color = MaterialTheme.wireColorScheme.outline)
@@ -115,17 +112,19 @@ fun ActiveMessageComposerInput(
}
val stretchToMaxParentConstraintHeightOrWithInBoundary = if (isTextExpanded) {
- Modifier.fillMaxHeight()
+ Modifier.weight(1F)
} else {
- Modifier.heightIn(max = dimensions().messageComposerActiveInputMaxHeight)
- }.weight(1F)
+ Modifier
+ .heightIn(max = dimensions().messageComposerActiveInputMaxHeight)
+ .weight(1F)
+ }
if (isTextExpanded) {
Column(
horizontalAlignment = Alignment.End,
modifier = Modifier
.fillMaxWidth()
- .wrapContentHeight()
+ .weight(1F)
) {
InputContent(
conversationId = conversationId,
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreState.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreState.kt
index ea255e155e8..0259c085a00 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreState.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreState.kt
@@ -20,6 +20,7 @@
package com.wire.android.ui.home.settings.backup
+import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
import okio.Path
data class BackupAndRestoreState(
@@ -27,7 +28,7 @@ data class BackupAndRestoreState(
val restoreFileValidation: RestoreFileValidation,
val restorePasswordValidation: PasswordValidation,
val backupCreationProgress: BackupCreationProgress,
- val backupCreationPasswordValidation: PasswordValidation
+ val passwordValidation: ValidatePasswordResult = ValidatePasswordResult.Valid
) {
data class CreatedBackup(val path: Path, val assetName: String, val assetSize: Long, val isEncrypted: Boolean)
@@ -37,7 +38,7 @@ data class BackupAndRestoreState(
restoreFileValidation = RestoreFileValidation.Initial,
backupCreationProgress = BackupCreationProgress.InProgress(),
restorePasswordValidation = PasswordValidation.NotVerified,
- backupCreationPasswordValidation = PasswordValidation.Valid,
+ passwordValidation = ValidatePasswordResult.Valid,
)
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt
index dae2745abf3..22ee2757c2d 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/BackupAndRestoreViewModel.kt
@@ -33,6 +33,8 @@ import com.wire.android.appLogger
import com.wire.android.util.FileManager
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.data.asset.KaliumFileSystem
+import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
+import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase
import com.wire.kalium.logic.feature.backup.CreateBackupResult
import com.wire.kalium.logic.feature.backup.CreateBackupUseCase
import com.wire.kalium.logic.feature.backup.RestoreBackupResult
@@ -58,6 +60,7 @@ class BackupAndRestoreViewModel
private val importBackup: RestoreBackupUseCase,
private val createBackupFile: CreateBackupUseCase,
private val verifyBackup: VerifyBackupUseCase,
+ private val validatePassword: ValidatePasswordUseCase,
private val kaliumFileSystem: KaliumFileSystem,
private val fileManager: FileManager,
private val dispatcher: DispatcherProvider,
@@ -228,7 +231,13 @@ class BackupAndRestoreViewModel
}
fun validateBackupCreationPassword(backupPassword: TextFieldValue) {
- // TODO: modify in case the password requirements change
+ state = state.copy(
+ passwordValidation = if (backupPassword.text.isEmpty()) {
+ ValidatePasswordResult.Valid
+ } else {
+ validatePassword(backupPassword.text)
+ }
+ )
}
fun cancelBackupCreation() = viewModelScope.launch(dispatcher.main()) {
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/dialog/create/CreateBackupDialogFlow.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/dialog/create/CreateBackupDialogFlow.kt
index 57eda0f256a..8ed5ee1135d 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/dialog/create/CreateBackupDialogFlow.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/dialog/create/CreateBackupDialogFlow.kt
@@ -29,7 +29,6 @@ import androidx.compose.ui.text.input.TextFieldValue
import com.wire.android.R
import com.wire.android.ui.home.settings.backup.BackupAndRestoreState
import com.wire.android.ui.home.settings.backup.BackupCreationProgress
-import com.wire.android.ui.home.settings.backup.PasswordValidation
import com.wire.android.ui.home.settings.backup.dialog.common.FailureDialog
@Composable
@@ -47,7 +46,7 @@ fun CreateBackupDialogFlow(
when (currentBackupDialogStep) {
BackUpDialogStep.SetPassword -> {
SetBackupPasswordDialog(
- isBackupPasswordValid = backUpAndRestoreState.backupCreationPasswordValidation is PasswordValidation.Valid,
+ passwordValidation = backUpAndRestoreState.passwordValidation,
onBackupPasswordChanged = onValidateBackupPassword,
onCreateBackup = { password ->
toCreatingBackup()
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/dialog/create/CreateBackupDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/dialog/create/CreateBackupDialogs.kt
index 655666c7558..4bd5c91629e 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/dialog/create/CreateBackupDialogs.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/backup/dialog/create/CreateBackupDialogs.kt
@@ -25,6 +25,7 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -42,18 +43,20 @@ import com.wire.android.ui.common.WireDialog
import com.wire.android.ui.common.WireDialogButtonProperties
import com.wire.android.ui.common.WireDialogButtonType
import com.wire.android.ui.common.button.WireButtonState
+import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.spacers.VerticalSpace
import com.wire.android.ui.common.textfield.WirePasswordTextField
import com.wire.android.ui.common.textfield.WireTextFieldState
import com.wire.android.ui.common.wireDialogPropertiesBuilder
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.permission.rememberCreateFileFlow
+import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
import java.util.Locale
import kotlin.math.roundToInt
@Composable
fun SetBackupPasswordDialog(
- isBackupPasswordValid: Boolean,
+ passwordValidation: ValidatePasswordResult,
onBackupPasswordChanged: (TextFieldValue) -> Unit,
onCreateBackup: (String) -> Unit,
onDismissDialog: () -> Unit
@@ -73,12 +76,14 @@ fun SetBackupPasswordDialog(
onClick = { onCreateBackup(backupPassword.text) },
text = stringResource(id = R.string.backup_dialog_create_backup_now),
type = WireDialogButtonType.Primary,
- state = if (!isBackupPasswordValid) WireButtonState.Disabled else WireButtonState.Default
+ state = if (passwordValidation.isValid) WireButtonState.Default else WireButtonState.Disabled
)
) {
WirePasswordTextField(
+ modifier = Modifier.padding(bottom = dimensions().spacing16x),
labelText = stringResource(R.string.label_textfield_optional_password).uppercase(Locale.getDefault()),
- state = if (!isBackupPasswordValid) WireTextFieldState.Error("some error") else WireTextFieldState.Default,
+ descriptionText = stringResource(R.string.create_account_details_password_description),
+ state = if (passwordValidation.isValid) WireTextFieldState.Default else WireTextFieldState.Error(),
value = backupPassword,
onValueChange = {
backupPassword = it
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt
index 5f962dd030a..7c9e635c788 100644
--- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt
@@ -34,6 +34,7 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.wire.android.BuildConfig
import com.wire.android.R
+import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.Navigator
import com.wire.android.ui.authentication.devices.model.Device
import com.wire.android.ui.authentication.devices.model.lastActiveDescription
@@ -54,6 +55,7 @@ import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.scaffold.WireScaffold
import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
import com.wire.android.ui.common.topappbar.WireTopAppBarTitle
+import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination
import com.wire.android.ui.home.conversationslist.common.FolderHeader
import com.wire.android.ui.settings.devices.model.DeviceDetailsState
import com.wire.android.ui.theme.wireColorScheme
@@ -86,7 +88,13 @@ fun DeviceDetailsScreen(
onDialogDismiss = viewModel::onDialogDismissed,
onErrorDialogDismiss = viewModel::clearDeleteClientError,
onNavigateBack = navigator::navigateBack,
- onUpdateClientVerification = viewModel::onUpdateVerificationStatus
+ onUpdateClientVerification = viewModel::onUpdateVerificationStatus,
+ enrollE2eiCertificate = viewModel::enrollE2eiCertificate,
+ onNavigateToE2eiCertificateDetailsScreen = {
+ navigator.navigate(
+ NavigationCommand(E2eiCertificateDetailsScreenDestination(it))
+ )
+ }
)
}
}
@@ -96,10 +104,12 @@ fun DeviceDetailsContent(
state: DeviceDetailsState,
onDeleteDevice: () -> Unit = {},
onNavigateBack: () -> Unit = {},
+ onNavigateToE2eiCertificateDetailsScreen: (String) -> Unit = {},
onPasswordChange: (TextFieldValue) -> Unit = {},
onRemoveConfirm: () -> Unit = {},
onDialogDismiss: () -> Unit = {},
onErrorDialogDismiss: () -> Unit = {},
+ enrollE2eiCertificate: () -> Unit = {},
onUpdateClientVerification: (Boolean) -> Unit = {}
) {
val screenState = rememberConversationScreenState()
@@ -151,7 +161,17 @@ fun DeviceDetailsContent(
Divider(color = MaterialTheme.wireColorScheme.background)
}
}
-
+ item {
+ EndToEndIdentityCertificateItem(
+ isE2eiCertificateActivated = state.isE2eiCertificateActivated,
+ certificate = state.e2eiCertificate,
+ isSelfClient = state.isSelfClient,
+ enrollE2eiCertificate = enrollE2eiCertificate,
+ updateE2eiCertificate = {},
+ showCertificate = onNavigateToE2eiCertificateDetailsScreen
+ )
+ Divider(color = colorsScheme().background)
+ }
item {
FolderHeader(
name = stringResource(id = R.string.label_proteus_details).uppercase(),
@@ -244,7 +264,7 @@ private fun DeviceDetailsTopBar(
maxLines = 2
)
if (!isCurrentDevice && device.isVerifiedProteus) {
- ProteusVerifiedIcon()
+ ProteusVerifiedIcon(Modifier.align(Alignment.CenterVertically))
}
}
}
@@ -366,7 +386,8 @@ private fun VerificationDescription(
text = stringResource(
id = R.string.label_client_verification_description,
userName ?: stringResource(id = R.string.unknown_user_name)
- ), leanMoreLink = stringResource(id = R.string.url_self_client_verification_learn_more)
+ ),
+ leanMoreLink = stringResource(id = R.string.url_self_client_verification_learn_more)
)
}
@@ -382,7 +403,10 @@ private fun VerificationDescription(
leanMoreLink = stringResource(id = R.string.url_self_client_fingerprint_learn_more)
)
} else {
- DescriptionText(text = stringResource(id = R.string.label_fingerprint_description), leanMoreLink = null)
+ DescriptionText(
+ text = stringResource(id = R.string.label_fingerprint_description),
+ leanMoreLink = null
+ )
}
}
}
@@ -426,7 +450,11 @@ private fun DescriptionText(
ClickableText(text = annotatedString, onClick = { offset ->
leanMoreLink?.let {
- annotatedString.getStringAnnotations(tag = "learn_more", start = offset, end = offset).firstOrNull()?.let {
+ annotatedString.getStringAnnotations(
+ tag = "learn_more",
+ start = offset,
+ end = offset
+ ).firstOrNull()?.let {
CustomTabsHelper.launchUrl(context, it.item)
}
}
@@ -492,6 +520,7 @@ fun PreviewDeviceDetailsScreen() {
isCurrentDevice = false
),
onPasswordChange = { },
+ enrollE2eiCertificate = { },
onRemoveConfirm = { },
onDialogDismiss = { },
onErrorDialogDismiss = { }
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt
index 949c0c4fcaf..2886561fe91 100644
--- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt
@@ -23,6 +23,8 @@ import com.wire.kalium.logic.feature.client.DeleteClientUseCase
import com.wire.kalium.logic.feature.client.GetClientDetailsResult
import com.wire.kalium.logic.feature.client.ObserveClientDetailsUseCase
import com.wire.kalium.logic.feature.client.UpdateClientVerificationStatusUseCase
+import com.wire.kalium.logic.feature.e2ei.usecase.GetE2EICertificateUseCaseResult
+import com.wire.kalium.logic.feature.e2ei.usecase.GetE2eiCertificateUseCase
import com.wire.kalium.logic.feature.user.GetUserInfoResult
import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase
import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase
@@ -30,6 +32,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
+@Suppress("TooManyFunctions", "LongParameterList")
@HiltViewModel
class DeviceDetailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@@ -40,7 +43,8 @@ class DeviceDetailsViewModel @Inject constructor(
private val isPasswordRequired: IsPasswordRequiredUseCase,
private val fingerprintUseCase: ClientFingerprintUseCase,
private val updateClientVerificationStatus: UpdateClientVerificationStatusUseCase,
- private val observeUserInfo: ObserveUserInfoUseCase
+ private val observeUserInfo: ObserveUserInfoUseCase,
+ private val e2eiCertificate: GetE2eiCertificateUseCase
) : SavedStateViewModel(savedStateHandle) {
private val deviceDetailsNavArgs: DeviceDetailsNavArgs = savedStateHandle.navArgs()
@@ -54,6 +58,7 @@ class DeviceDetailsViewModel @Inject constructor(
observeDeviceDetails()
getClientFingerPrint()
observeUserName()
+ getE2eiCertificate()
}
private val isSelfClient: Boolean
@@ -74,6 +79,22 @@ class DeviceDetailsViewModel @Inject constructor(
}
}
+ private fun getE2eiCertificate() {
+ val certificate = e2eiCertificate(deviceId)
+ state = if (certificate is GetE2EICertificateUseCaseResult.Success) {
+ state.copy(
+ isE2eiCertificateActivated = true,
+ e2eiCertificate = certificate.certificate
+ )
+ } else {
+ state.copy(isE2eiCertificateActivated = false)
+ }
+ }
+
+ fun enrollE2eiCertificate() {
+ // TODO invoke correspondent use case
+ }
+
private fun getClientFingerPrint() {
viewModelScope.launch {
state = when (val result = fingerprintUseCase(userId, deviceId)) {
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt
new file mode 100644
index 00000000000..cf9870c8df1
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.settings.devices
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.wire.android.R
+import com.wire.android.ui.common.colorsScheme
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.settings.devices.button.GetE2eiCertificateButton
+import com.wire.android.ui.settings.devices.button.ShowE2eiCertificateButton
+import com.wire.android.ui.settings.devices.button.UpdateE2eiCertificateButton
+import com.wire.android.ui.theme.wireColorScheme
+import com.wire.android.ui.theme.wireDimensions
+import com.wire.android.ui.theme.wireTypography
+import com.wire.android.util.ui.PreviewMultipleThemes
+import com.wire.kalium.logic.feature.e2ei.CertificateStatus
+import com.wire.kalium.logic.feature.e2ei.E2eiCertificate
+
+@Composable
+fun EndToEndIdentityCertificateItem(
+ isE2eiCertificateActivated: Boolean,
+ certificate: E2eiCertificate,
+ isSelfClient: Boolean,
+ enrollE2eiCertificate: () -> Unit,
+ updateE2eiCertificate: () -> Unit,
+ showCertificate: (String) -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .padding(
+ top = MaterialTheme.wireDimensions.spacing12x,
+ bottom = MaterialTheme.wireDimensions.spacing12x,
+ start = MaterialTheme.wireDimensions.spacing16x,
+ end = MaterialTheme.wireDimensions.spacing12x
+ )
+ ) {
+ Text(
+ text = stringResource(id = R.string.item_title_e2ei_certificate),
+ style = MaterialTheme.wireTypography.title02,
+ color = MaterialTheme.wireColorScheme.onBackground
+ )
+ Text(
+ modifier = Modifier.padding(
+ top = dimensions().spacing8x,
+ bottom = dimensions().spacing4x
+ ),
+ text = stringResource(id = R.string.item_subtitle_status_e2ei_certificate).uppercase(),
+ style = MaterialTheme.wireTypography.label01,
+ color = MaterialTheme.wireColorScheme.secondaryText,
+ )
+ Column {
+ if (isE2eiCertificateActivated) {
+ when (certificate.status) {
+ CertificateStatus.REVOKED -> {
+ E2EIStatusRow(
+ label = stringResource(id = R.string.e2ei_certificat_status_revoked),
+ labelColor = colorsScheme().error,
+ icon = R.drawable.ic_certificate_revoked_mls
+ )
+ SerialNumberBlock(certificate.serialNumber)
+ ShowE2eiCertificateButton(
+ enabled = true,
+ isLoading = false,
+ onShowCertificateClicked = {
+ showCertificate(certificate.certificateDetail)
+ }
+ )
+ }
+
+ CertificateStatus.EXPIRED -> {
+ E2EIStatusRow(
+ label = stringResource(id = R.string.e2ei_certificat_status_expired),
+ labelColor = colorsScheme().error,
+ icon = R.drawable.ic_certificate_not_activated_mls
+ )
+ SerialNumberBlock(certificate.serialNumber)
+ UpdateE2eiCertificateButton(
+ enabled = true,
+ isLoading = false,
+ updateE2eiCertificate
+ )
+ ShowE2eiCertificateButton(
+ enabled = true,
+ isLoading = false,
+ onShowCertificateClicked = {
+ showCertificate(certificate.certificateDetail)
+ }
+ )
+ }
+
+ CertificateStatus.VALID -> {
+ E2EIStatusRow(
+ label = stringResource(id = R.string.e2ei_certificat_status_valid),
+ labelColor = colorsScheme().validE2eiStatusColor,
+ icon = R.drawable.ic_certificate_valid_mls
+ )
+ SerialNumberBlock(certificate.serialNumber)
+ ShowE2eiCertificateButton(
+ enabled = true,
+ isLoading = false,
+ onShowCertificateClicked = {
+ showCertificate(certificate.certificateDetail)
+ }
+ )
+ }
+ }
+ } else {
+ E2EIStatusRow(
+ label = stringResource(id = R.string.e2ei_certificat_status_not_activated),
+ labelColor = colorsScheme().error,
+ icon = R.drawable.ic_certificate_not_activated_mls
+ )
+ if (isSelfClient) {
+ GetE2eiCertificateButton(enabled = true, isLoading = false) { }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun SerialNumberBlock(serialNumber: String) {
+ Column {
+ Text(
+ modifier = Modifier.padding(
+ top = dimensions().spacing8x,
+ bottom = dimensions().spacing4x
+ ),
+ text = stringResource(id = R.string.item_subtitle_serial_number_e2ei_certificate).uppercase(),
+ style = MaterialTheme.wireTypography.label01,
+ color = MaterialTheme.wireColorScheme.secondaryText,
+ )
+ val updatedSerialNumber = serialNumber
+ .replaceRange(24, 24, "\n")
+ .replace(":", " : ")
+
+ Text(
+ text = updatedSerialNumber.uppercase(),
+ style = MaterialTheme.wireTypography.body01,
+ color = MaterialTheme.wireColorScheme.onBackground,
+ )
+ }
+}
+
+@Composable
+private fun E2EIStatusRow(
+ label: String,
+ labelColor: Color,
+ icon: Int,
+ iconContentDescription: String = ""
+) {
+ Row {
+ Text(
+ modifier = Modifier.padding(end = dimensions().spacing4x),
+ text = label,
+ style = MaterialTheme.wireTypography.body02,
+ color = labelColor,
+ )
+ Image(
+ painter = painterResource(id = icon),
+ contentDescription = iconContentDescription,
+ )
+ }
+}
+
+@PreviewMultipleThemes
+@Composable
+fun PreviewEndToEndIdentityCertificateItem() {
+ EndToEndIdentityCertificateItem(
+ isE2eiCertificateActivated = true,
+ isSelfClient = false,
+ certificate = E2eiCertificate(
+ issuer = "Wire",
+ status = CertificateStatus.VALID,
+ serialNumber = "e5:d5:e6:75:7e:04:86:07:14:3c:a0:ed:9a:8d:e4:fd",
+ certificateDetail = ""
+ ),
+ enrollE2eiCertificate = {},
+ updateE2eiCertificate = {},
+ showCertificate = {}
+ )
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/button/GetE2eiCertificateButton.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/button/GetE2eiCertificateButton.kt
new file mode 100644
index 00000000000..94cc4a62871
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/button/GetE2eiCertificateButton.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.settings.devices.button
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.wire.android.R
+import com.wire.android.ui.common.button.WireButtonState
+import com.wire.android.ui.common.button.WirePrimaryButton
+import com.wire.android.ui.common.dimensions
+import com.wire.android.util.ui.PreviewMultipleThemes
+
+@Composable
+fun GetE2eiCertificateButton(
+ enabled: Boolean,
+ isLoading: Boolean,
+ onGetCertificateClicked: () -> Unit
+) {
+ WirePrimaryButton(
+ text = stringResource(id = R.string.get_e2ei_certificate_button),
+ fillMaxWidth = true,
+ onClick = onGetCertificateClicked,
+ loading = isLoading,
+ state = if (!enabled) WireButtonState.Disabled
+ else WireButtonState.Default,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ top = dimensions().spacing8x,
+ bottom = dimensions().spacing8x,
+ )
+ )
+}
+
+@PreviewMultipleThemes
+@Composable
+fun PreviewGetE2eiCertificateButton() {
+ GetE2eiCertificateButton(
+ enabled = true,
+ isLoading = false,
+ onGetCertificateClicked = {}
+ )
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/button/ShowE2eiCertificateButton.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/button/ShowE2eiCertificateButton.kt
new file mode 100644
index 00000000000..ae5af6954e5
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/button/ShowE2eiCertificateButton.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.settings.devices.button
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ChevronRight
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.wire.android.R
+import com.wire.android.ui.common.Icon
+import com.wire.android.ui.common.button.WireButtonState
+import com.wire.android.ui.common.button.WireSecondaryButton
+import com.wire.android.ui.common.dimensions
+import com.wire.android.util.ui.PreviewMultipleThemes
+
+@Composable
+fun ShowE2eiCertificateButton(
+ enabled: Boolean,
+ isLoading: Boolean,
+ onShowCertificateClicked: () -> Unit
+) {
+ WireSecondaryButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ top = dimensions().spacing8x,
+ bottom = dimensions().spacing8x
+ ),
+ text = stringResource(id = R.string.show_e2ei_certificate_details_button),
+ fillMaxWidth = true,
+ onClick = onShowCertificateClicked,
+ loading = isLoading,
+ state = if (!enabled) WireButtonState.Disabled else WireButtonState.Default,
+ trailingIcon = Icons.Filled.ChevronRight.Icon()
+ )
+}
+
+@PreviewMultipleThemes
+@Composable
+fun PreviewShowE2eiCertificateButton() {
+ ShowE2eiCertificateButton(
+ enabled = true,
+ isLoading = false,
+ onShowCertificateClicked = {}
+ )
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/button/UpdateE2eiCertificateButton.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/button/UpdateE2eiCertificateButton.kt
new file mode 100644
index 00000000000..7e438ba31b1
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/button/UpdateE2eiCertificateButton.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.settings.devices.button
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.wire.android.R
+import com.wire.android.ui.common.button.WireButtonState
+import com.wire.android.ui.common.button.WirePrimaryButton
+import com.wire.android.ui.common.dimensions
+import com.wire.android.util.ui.PreviewMultipleThemes
+
+@Composable
+fun UpdateE2eiCertificateButton(
+ enabled: Boolean,
+ isLoading: Boolean,
+ onUpdateCertificateClicked: () -> Unit
+) {
+ WirePrimaryButton(
+ text = stringResource(id = R.string.update_e2ei_certificat_button),
+ fillMaxWidth = true,
+ onClick = onUpdateCertificateClicked,
+ loading = isLoading,
+ state = if (!enabled) WireButtonState.Disabled
+ else WireButtonState.Default,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = dimensions().spacing8x)
+ )
+}
+
+@PreviewMultipleThemes
+@Composable
+fun PreviewUpdateE2eiCertificateButton() {
+ UpdateE2eiCertificateButton(
+ enabled = true,
+ isLoading = false,
+ onUpdateCertificateClicked = {}
+ )
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsBottomSheet.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsBottomSheet.kt
new file mode 100644
index 00000000000..1ae656397d0
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsBottomSheet.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.settings.devices.e2ei
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.wire.android.R
+import com.wire.android.ui.common.bottomsheet.MenuBottomSheetItem
+import com.wire.android.ui.common.bottomsheet.MenuItemIcon
+import com.wire.android.ui.common.bottomsheet.MenuModalSheetContent
+import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader
+import com.wire.android.ui.common.bottomsheet.WireModalSheetLayout
+import com.wire.android.ui.common.bottomsheet.WireModalSheetState
+
+@Composable
+fun E2eiCertificateDetailsBottomSheet(
+ sheetState: WireModalSheetState,
+ onCopyToClipboard: () -> Unit,
+ onDownload: () -> Unit,
+) {
+ val coroutineScope = rememberCoroutineScope()
+
+ WireModalSheetLayout(sheetState = sheetState, coroutineScope = coroutineScope) {
+ MenuModalSheetContent(
+ header = MenuModalSheetHeader.Gone,
+ menuItems = buildList {
+ add {
+ CreateCertificateSheetItem(
+ title = stringResource(R.string.e2ei_certificate_details_copy_to_clipboard),
+ icon = R.drawable.ic_copy,
+ onClicked = onCopyToClipboard,
+ enabled = true
+ )
+ }
+ add {
+ CreateCertificateSheetItem(
+ title = stringResource(R.string.e2ei_certificate_details_download),
+ icon = R.drawable.ic_download,
+ onClicked = onDownload,
+ enabled = true
+ )
+ }
+ }
+ )
+ }
+}
+
+@Composable
+private fun CreateCertificateSheetItem(
+ title: String,
+ icon: Int,
+ onClicked: () -> Unit,
+ enabled: Boolean = true,
+) {
+ MenuBottomSheetItem(
+ title = title,
+ onItemClick = onClicked,
+ icon = {
+ MenuItemIcon(
+ id = icon,
+ contentDescription = "",
+ )
+ },
+ enabled = enabled
+ )
+}
+
+@Preview
+@Composable
+fun PreviewE2eiCertificateDetailsBottomSheet() {
+ E2eiCertificateDetailsBottomSheet(
+ sheetState = WireModalSheetState(),
+ onCopyToClipboard = { },
+ onDownload = { }
+ )
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt
new file mode 100644
index 00000000000..a413017156d
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.settings.devices.e2ei
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.RootNavGraph
+import com.wire.android.R
+import com.wire.android.navigation.Navigator
+import com.wire.android.navigation.style.PopUpNavigationAnimation
+import com.wire.android.ui.common.button.WireSecondaryIconButton
+import com.wire.android.ui.common.colorsScheme
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.common.scaffold.WireScaffold
+import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
+import com.wire.android.ui.common.topappbar.NavigationIconType
+import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
+import com.wire.android.util.copyLinkToClipboard
+import com.wire.android.util.createPemFile
+import com.wire.android.util.saveFileToDownloadsFolder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okio.Path.Companion.toOkioPath
+
+@RootNavGraph
+@Destination(
+ navArgsDelegate = E2eiCertificateDetailsScreenNavArgs::class,
+ style = PopUpNavigationAnimation::class,
+)
+@Composable
+fun E2eiCertificateDetailsScreen(
+ e2eiCertificateDetailsViewModel: E2eiCertificateDetailsViewModel = hiltViewModel(),
+ navigator: Navigator
+) {
+ val snackbarHostState = LocalSnackbarHostState.current
+ val scope = rememberCoroutineScope()
+ val context = LocalContext.current
+
+ WireScaffold(
+ topBar = {
+ WireCenterAlignedTopAppBar(
+ onNavigationPressed = navigator::navigateBack,
+ title = stringResource(R.string.e2ei_certificate_details_screen_title),
+ navigationIconType = NavigationIconType.Back,
+ actions = {
+ WireSecondaryIconButton(
+ onButtonClicked = {
+ e2eiCertificateDetailsViewModel.state.wireModalSheetState.show()
+ },
+ iconResource = R.drawable.ic_more,
+ contentDescription = R.string.content_description_more_options
+ )
+ }
+ )
+ }
+ ) {
+ val clipboardManager = LocalClipboardManager.current
+
+ with(e2eiCertificateDetailsViewModel) {
+ val copiedToClipboardString =
+ stringResource(id = R.string.e2ei_certificate_details_certificate_copied_to_clipboard)
+ val downloadedString = stringResource(id = R.string.media_gallery_on_image_downloaded)
+
+ E2eiCertificateDetailsContent(
+ padding = it,
+ certificateString = getCertificate()
+ )
+ E2eiCertificateDetailsBottomSheet(
+ sheetState = state.wireModalSheetState,
+ onCopyToClipboard = {
+ clipboardManager.copyLinkToClipboard(getCertificate())
+ scope.launch {
+ state.wireModalSheetState.hide()
+ snackbarHostState.showSnackbar(copiedToClipboardString)
+ }
+ },
+ onDownload = {
+ scope.launch {
+ withContext(Dispatchers.IO) {
+ createPemFile(CERTIFICATE_FILE_NAME, getCertificate()).also {
+ saveFileToDownloadsFolder(
+ context = context,
+ assetName = CERTIFICATE_FILE_NAME,
+ assetDataPath = it.toPath().toOkioPath(),
+ assetDataSize = it.length()
+ )
+ }
+ }
+ state.wireModalSheetState.hide()
+ snackbarHostState.showSnackbar(downloadedString)
+ }
+ }
+ )
+ }
+ }
+}
+
+@Composable
+fun E2eiCertificateDetailsContent(
+ padding: PaddingValues,
+ certificateString: String
+) {
+ val textStyle = TextStyle(
+ textAlign = TextAlign.Justify,
+ fontSize = 12.sp,
+ lineHeight = 18.sp,
+ color = colorsScheme().onBackground
+ )
+ val scroll = rememberScrollState(0)
+ Text(
+ modifier = Modifier
+ .verticalScroll(scroll)
+ .padding(
+ top = padding.calculateTopPadding() + dimensions().spacing16x,
+ start = padding.calculateStartPadding(LayoutDirection.Ltr) + dimensions().spacing16x,
+ end = padding.calculateEndPadding(LayoutDirection.Ltr) + dimensions().spacing16x,
+ bottom = padding.calculateBottomPadding()
+ ),
+ text = certificateString,
+ style = textStyle
+ )
+}
+
+const val CERTIFICATE_FILE_NAME = "certificate.pem"
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreenNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreenNavArgs.kt
new file mode 100644
index 00000000000..9ebe51da34f
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreenNavArgs.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.settings.devices.e2ei
+
+data class E2eiCertificateDetailsScreenNavArgs(
+ val certificateString: String
+)
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt
new file mode 100644
index 00000000000..4e77cdbbb17
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsViewModel.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.settings.devices.e2ei
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import com.wire.android.ui.common.bottomsheet.WireModalSheetState
+import com.wire.android.ui.navArgs
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class E2eiCertificateDetailsViewModel @Inject constructor(
+ savedStateHandle: SavedStateHandle,
+) : ViewModel() {
+
+ var state: E2eiCertificateDetailsState by mutableStateOf(E2eiCertificateDetailsState())
+ private set
+
+ private val e2eiCertificateDetailsScreenNavArgs: E2eiCertificateDetailsScreenNavArgs =
+ savedStateHandle.navArgs()
+
+ fun getCertificate() = e2eiCertificateDetailsScreenNavArgs.certificateString
+}
+
+data class E2eiCertificateDetailsState(
+ val wireModalSheetState: WireModalSheetState = WireModalSheetState()
+)
diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt
index ab60d1b755c..24b01807567 100644
--- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt
@@ -3,6 +3,7 @@ package com.wire.android.ui.settings.devices.model
import com.wire.android.ui.authentication.devices.model.Device
import com.wire.android.ui.authentication.devices.remove.RemoveDeviceDialogState
import com.wire.android.ui.authentication.devices.remove.RemoveDeviceError
+import com.wire.kalium.logic.feature.e2ei.E2eiCertificate
data class DeviceDetailsState(
val device: Device = Device(),
@@ -11,5 +12,7 @@ data class DeviceDetailsState(
val error: RemoveDeviceError = RemoveDeviceError.None,
val fingerPrint: String? = null,
val isSelfClient: Boolean = false,
- val userName: String? = null
+ val userName: String? = null,
+ val isE2eiCertificateActivated: Boolean = false,
+ val e2eiCertificate: E2eiCertificate = E2eiCertificate()
)
diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt
index e76814f5cfb..249d644cede 100644
--- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaAuthenticatedViewModel.kt
@@ -192,7 +192,9 @@ class ImportMediaAuthenticatedViewModel @Inject constructor(
isSelfUserMember = isSelfUserMember,
teamId = conversation.teamId,
selfMemberRole = selfRole,
- isArchived = conversation.archived
+ isArchived = conversation.archived,
+ mlsVerificationStatus = conversation.mlsVerificationStatus,
+ proteusVerificationStatus = conversation.proteusVerificationStatus
)
}
@@ -228,7 +230,9 @@ class ImportMediaAuthenticatedViewModel @Inject constructor(
userId = otherUser.id,
blockingState = otherUser.BlockState,
teamId = otherUser.teamId,
- isArchived = conversation.archived
+ isArchived = conversation.archived,
+ mlsVerificationStatus = conversation.mlsVerificationStatus,
+ proteusVerificationStatus = conversation.proteusVerificationStatus
)
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt
index f045a281f52..a609edb4986 100644
--- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt
@@ -403,8 +403,7 @@ private fun ImportMediaContent(
onSearchQueryChanged(it)
searchBarState.searchQueryChanged(it)
},
- onInputClicked = searchBarState::openSearch,
- onCloseSearchClicked = searchBarState::closeSearch
+ onActiveChanged = searchBarState::searchActiveChanged,
)
}
ConversationList(
diff --git a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt
index 9c7d52548c8..e2cab12dff4 100644
--- a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorPalette.kt
@@ -62,6 +62,8 @@ object WireColorPalette {
@Stable
val LightGreen500 = Color(0xFF207C37)
@Stable
+ val LightGreen550 = Color(0xFF1D7833)
+ @Stable
val LightGreen600 = Color(0xFF1A632C)
@Stable
val LightGreen700 = Color(0xFF134A21)
@@ -187,6 +189,7 @@ object WireColorPalette {
val DarkGreen400 = Color(0xFF59E27C)
@Stable
val DarkGreen500 = Color(0xFF30DB5B)
+ val DarkGreen550 = Color(0xFF1D7833)
@Stable
val DarkGreen600 = Color(0xFF26AF49)
@Stable
diff --git a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt
index 5008f4dee84..6811c2e7f7e 100644
--- a/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt
@@ -100,7 +100,11 @@ data class WireColorScheme(
val unclassifiedBannerBackgroundColor: Color,
val unclassifiedBannerForegroundColor: Color,
val recordAudioStartColor: Color,
- val recordAudioStopColor: Color
+ val recordAudioStopColor: Color,
+ val scrollToBottomButtonColor: Color,
+ val onScrollToBottomButtonColor: Color,
+ val validE2eiStatusColor: Color,
+ val mlsVerificationTextColor: Color,
) {
fun toColorScheme(): ColorScheme = ColorScheme(
primary = primary,
@@ -231,7 +235,11 @@ private val LightWireColorScheme = WireColorScheme(
unclassifiedBannerBackgroundColor = WireColorPalette.LightRed600,
unclassifiedBannerForegroundColor = Color.White,
recordAudioStartColor = WireColorPalette.LightBlue500,
- recordAudioStopColor = WireColorPalette.LightRed500
+ recordAudioStopColor = WireColorPalette.LightRed500,
+ scrollToBottomButtonColor = WireColorPalette.Gray70,
+ onScrollToBottomButtonColor = Color.White,
+ validE2eiStatusColor = WireColorPalette.LightGreen550,
+ mlsVerificationTextColor = WireColorPalette.DarkGreen700
)
// Dark WireColorScheme
@@ -336,7 +344,11 @@ private val DarkWireColorScheme = WireColorScheme(
unclassifiedBannerBackgroundColor = WireColorPalette.DarkRed500,
unclassifiedBannerForegroundColor = Color.Black,
recordAudioStartColor = WireColorPalette.LightBlue500,
- recordAudioStopColor = WireColorPalette.LightRed500
+ recordAudioStopColor = WireColorPalette.LightRed500,
+ scrollToBottomButtonColor = WireColorPalette.Gray60,
+ onScrollToBottomButtonColor = Color.Black,
+ validE2eiStatusColor = WireColorPalette.DarkGreen550,
+ mlsVerificationTextColor = WireColorPalette.DarkGreen700
)
@PackagePrivate
diff --git a/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt b/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt
index c8bc7b15aa8..6c00ead58df 100644
--- a/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt
@@ -46,6 +46,8 @@ data class WireDimensions(
val groupAvatarCornerRadius: Dp,
val avatarConversationTopBarSize: Dp,
val groupAvatarConversationTopBarCornerRadius: Dp,
+ val groupAvatarConversationDetailsTopBarSize: Dp,
+ val groupAvatarConversationDetailsCornerRadius: Dp,
val avatarConversationTopBarClickablePadding: Dp,
// Drawer Navigation
val homeDrawerHorizontalPadding: Dp,
@@ -210,6 +212,8 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions(
groupAvatarCornerRadius = 10.dp,
avatarConversationTopBarSize = 24.dp,
groupAvatarConversationTopBarCornerRadius = 8.dp,
+ groupAvatarConversationDetailsTopBarSize = 64.dp,
+ groupAvatarConversationDetailsCornerRadius = 20.dp,
avatarConversationTopBarClickablePadding = 2.dp,
homeDrawerHorizontalPadding = 8.dp,
homeDrawerBottomPadding = 16.dp,
@@ -237,7 +241,7 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions(
messageQuoteBorderRadius = 1.dp,
messageQuoteIconSize = 10.dp,
messageAssetBorderRadius = 10.dp,
- messageComposerActiveInputMaxHeight = 168.dp,
+ messageComposerActiveInputMaxHeight = 128.dp,
attachmentButtonSize = 40.dp,
textFieldMinHeight = 48.dp,
textFieldCornerSize = 16.dp,
diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt
index ecfa398f3e6..c45204efa01 100644
--- a/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/common/UserProfileInfo.kt
@@ -62,6 +62,7 @@ import com.wire.android.ui.common.UserProfileAvatar
import com.wire.android.ui.common.banner.SecurityClassificationBannerForUser
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.progress.WireCircularProgressIndicator
+import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesButton
import com.wire.android.ui.home.conversationslist.model.Membership
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireTypography
@@ -88,7 +89,9 @@ fun UserProfileInfo(
modifier: Modifier = Modifier,
connection: ConnectionState = ConnectionState.ACCEPTED,
delayToShowPlaceholderIfNoAsset: Duration = 200.milliseconds,
- isProteusVerified: Boolean = false
+ isProteusVerified: Boolean = false,
+ onSearchConversationMessagesClick: () -> Unit = {},
+ shouldShowSearchButton: Boolean = false
) {
Column(
horizontalAlignment = CenterHorizontally,
@@ -227,6 +230,12 @@ fun UserProfileInfo(
modifier = Modifier.padding(top = dimensions().spacing8x)
)
}
+
+ if (shouldShowSearchButton) {
+ SearchConversationMessagesButton(
+ onSearchConversationMessagesClick = onSearchConversationMessagesClick
+ )
+ }
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserConnectionStatusInfo.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserConnectionStatusInfo.kt
index 682b26cdbac..047687aec96 100644
--- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserConnectionStatusInfo.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserConnectionStatusInfo.kt
@@ -58,16 +58,8 @@ fun OtherUserConnectionStatusInfo(connectionStatus: ConnectionState, membership:
style = MaterialTheme.wireTypography.title02
)
}
- val descriptionResource = when (connectionStatus) {
- ConnectionState.PENDING, ConnectionState.IGNORED -> R.string.connection_label_accepting_request_description
- ConnectionState.ACCEPTED, ConnectionState.BLOCKED -> null
- else -> if (membership == Membership.None) {
- R.string.connection_label_member_not_conneted
- } else {
- R.string.connection_label_member_not_belongs_to_team
- }
- }
- descriptionResource?.let {
+
+ descriptionResourceForConnectionAndMembership(connectionStatus, membership)?.let {
Text(
text = stringResource(it),
textAlign = TextAlign.Center,
@@ -80,6 +72,20 @@ fun OtherUserConnectionStatusInfo(connectionStatus: ConnectionState, membership:
}
}
+@Composable
+private fun descriptionResourceForConnectionAndMembership(
+ connectionStatus: ConnectionState,
+ membership: Membership
+) = when (connectionStatus) {
+ ConnectionState.PENDING, ConnectionState.IGNORED -> R.string.connection_label_accepting_request_description
+ ConnectionState.ACCEPTED, ConnectionState.BLOCKED -> null
+ else -> if (membership == Membership.None) {
+ R.string.connection_label_member_not_conneted
+ } else {
+ R.string.connection_label_member_not_belongs_to_team
+ }
+}
+
@Composable
@Preview
fun PreviewOtherUserConnectionStatusInfo() {
diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserConnectionUnverifiedWarning.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserConnectionUnverifiedWarning.kt
new file mode 100644
index 00000000000..53884ac03bc
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserConnectionUnverifiedWarning.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.userprofile.other
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import com.wire.android.R
+import com.wire.android.ui.common.dimensions
+import com.wire.android.ui.common.spacers.VerticalSpace
+import com.wire.android.ui.theme.wireColorScheme
+import com.wire.android.ui.theme.wireTypography
+import com.wire.kalium.logic.data.user.ConnectionState
+
+@Composable
+fun OtherUserConnectionUnverifiedWarning(
+ userName: String,
+ connectionStatus: ConnectionState
+) {
+ Box(
+ Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .padding(start = dimensions().spacing32x, end = dimensions().spacing32x)
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ unverifiedDescriptionResource(connectionStatus)?.let {
+ Text(
+ text = stringResource(it, userName),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.wireColorScheme.error,
+ style = MaterialTheme.wireTypography.body01
+ )
+ VerticalSpace.x24()
+ }
+ }
+ }
+}
+
+@Composable
+private fun unverifiedDescriptionResource(connectionStatus: ConnectionState) = when (connectionStatus) {
+ ConnectionState.PENDING, ConnectionState.IGNORED -> R.string.connection_label_received_unverified_warning
+ ConnectionState.ACCEPTED, ConnectionState.BLOCKED -> null
+ else -> R.string.connection_label_send_unverified_warning
+}
+
+@Composable
+@Preview
+fun PreviewOtherUserConnectionUnverifiedWarning() {
+ OtherUserConnectionUnverifiedWarning("Bob", ConnectionState.NOT_CONNECTED)
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt
index c7d3e2c0480..7800e6660eb 100644
--- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreen.kt
@@ -89,6 +89,7 @@ import com.wire.android.ui.common.visbility.rememberVisibilityState
import com.wire.android.ui.connection.ConnectionActionButton
import com.wire.android.ui.destinations.ConversationScreenDestination
import com.wire.android.ui.destinations.DeviceDetailsScreenDestination
+import com.wire.android.ui.destinations.SearchConversationMessagesScreenDestination
import com.wire.android.ui.home.conversations.details.dialog.ClearConversationContentDialog
import com.wire.android.ui.home.conversationslist.model.DialogState
import com.wire.android.ui.home.conversationslist.model.Membership
@@ -128,6 +129,21 @@ fun OtherUserProfileScreen(
val openBottomSheet: () -> Unit = remember { { scope.launch { sheetState.show() } } }
val closeBottomSheet: () -> Unit = remember { { scope.launch { sheetState.hide() } } }
+ val conversationId = viewModel.state.conversationId
+ ?: viewModel.state.conversationSheetContent?.conversationId
+ val shouldShowSearchButton = viewModel.shouldShowSearchButton(conversationId = conversationId)
+ val onSearchConversationMessagesClick: () -> Unit = {
+ conversationId?.let {
+ navigator.navigate(
+ NavigationCommand(
+ SearchConversationMessagesScreenDestination(
+ conversationId = it
+ )
+ )
+ )
+ }
+ }
+
OtherProfileScreenContent(
scope = scope,
state = viewModel.state,
@@ -143,6 +159,8 @@ fun OtherUserProfileScreen(
},
onOpenConversation = { navigator.navigate(NavigationCommand(ConversationScreenDestination(it), BackStackMode.UPDATE_EXISTED)) },
onOpenDeviceDetails = { navigator.navigate(NavigationCommand(DeviceDetailsScreenDestination(navArgs.userId, it.clientId))) },
+ onSearchConversationMessagesClick = onSearchConversationMessagesClick,
+ shouldShowSearchButton = shouldShowSearchButton,
navigateBack = navigator::navigateBack,
navigationIconType = NavigationIconType.Close,
)
@@ -175,6 +193,8 @@ fun OtherProfileScreenContent(
onIgnoreConnectionRequest: (String) -> Unit = { },
onOpenConversation: (ConversationId) -> Unit = {},
onOpenDeviceDetails: (Device) -> Unit = {},
+ onSearchConversationMessagesClick: () -> Unit,
+ shouldShowSearchButton: Boolean,
navigateBack: () -> Unit = {}
) {
val otherUserProfileScreenState = rememberOtherUserProfileScreenState()
@@ -249,7 +269,13 @@ fun OtherProfileScreenContent(
openConversationBottomSheet = openConversationBottomSheet
)
},
- topBarCollapsing = { TopBarCollapsing(state) },
+ topBarCollapsing = {
+ TopBarCollapsing(
+ state = state,
+ onSearchConversationMessagesClick = onSearchConversationMessagesClick,
+ shouldShowSearchButton = shouldShowSearchButton
+ )
+ },
topBarFooter = { TopBarFooter(state, pagerState, tabBarElevationState, tabItems, currentTabState, scope) },
content = {
Content(
@@ -345,8 +371,15 @@ private fun TopBarHeader(
}
@Composable
-private fun TopBarCollapsing(state: OtherUserProfileState) {
- Crossfade(targetState = state, label = "OtherUserProfileScreenTopBarCollapsing") { targetState ->
+private fun TopBarCollapsing(
+ state: OtherUserProfileState,
+ onSearchConversationMessagesClick: () -> Unit,
+ shouldShowSearchButton: Boolean
+) {
+ Crossfade(
+ targetState = state,
+ label = "OtherUserProfileScreenTopBarCollapsing"
+ ) { targetState ->
UserProfileInfo(
userId = targetState.userId,
isLoading = targetState.isAvatarLoading,
@@ -358,7 +391,9 @@ private fun TopBarCollapsing(state: OtherUserProfileState) {
editableState = EditableState.NotEditable,
modifier = Modifier.padding(bottom = dimensions().spacing16x),
connection = targetState.connectionState,
- isProteusVerified = targetState.isProteusVerified
+ isProteusVerified = targetState.isProteusVerified,
+ onSearchConversationMessagesClick = onSearchConversationMessagesClick,
+ shouldShowSearchButton = shouldShowSearchButton
)
}
}
@@ -412,6 +447,7 @@ private fun Content(
Column {
if (!state.isDataLoading) {
OtherUserConnectionStatusInfo(state.connectionState, state.membership)
+ OtherUserConnectionUnverifiedWarning(state.fullName, state.connectionState)
}
when {
state.isDataLoading || state.botService != null -> Box {} // no content visible while loading
@@ -505,13 +541,17 @@ enum class OtherUserProfileTabItem(@StringRes override val titleResId: Int) : Ta
fun PreviewOtherProfileScreenContent() {
WireTheme(isPreview = true) {
OtherProfileScreenContent(
- rememberCoroutineScope(),
- OtherUserProfileState.PREVIEW.copy(connectionState = ConnectionState.ACCEPTED),
- NavigationIconType.Back,
- false,
- rememberWireModalSheetState(),
- {}, {}, OtherUserProfileEventsHandler.PREVIEW,
- OtherUserProfileBottomSheetEventsHandler.PREVIEW
+ scope = rememberCoroutineScope(),
+ state = OtherUserProfileState.PREVIEW.copy(connectionState = ConnectionState.ACCEPTED),
+ navigationIconType = NavigationIconType.Back,
+ requestInProgress = false,
+ sheetState = rememberWireModalSheetState(),
+ openBottomSheet = {},
+ closeBottomSheet = {},
+ eventsHandler = OtherUserProfileEventsHandler.PREVIEW,
+ bottomSheetEventsHandler = OtherUserProfileBottomSheetEventsHandler.PREVIEW,
+ onSearchConversationMessagesClick = {},
+ shouldShowSearchButton = false
)
}
}
@@ -522,13 +562,17 @@ fun PreviewOtherProfileScreenContent() {
fun PreviewOtherProfileScreenContentNotConnected() {
WireTheme(isPreview = true) {
OtherProfileScreenContent(
- rememberCoroutineScope(),
- OtherUserProfileState.PREVIEW.copy(connectionState = ConnectionState.CANCELLED),
- NavigationIconType.Back,
- false,
- rememberWireModalSheetState(),
- {}, {}, OtherUserProfileEventsHandler.PREVIEW,
- OtherUserProfileBottomSheetEventsHandler.PREVIEW,
+ scope = rememberCoroutineScope(),
+ state = OtherUserProfileState.PREVIEW.copy(connectionState = ConnectionState.CANCELLED),
+ navigationIconType = NavigationIconType.Back,
+ requestInProgress = false,
+ sheetState = rememberWireModalSheetState(),
+ openBottomSheet = {},
+ closeBottomSheet = {},
+ eventsHandler = OtherUserProfileEventsHandler.PREVIEW,
+ bottomSheetEventsHandler = OtherUserProfileBottomSheetEventsHandler.PREVIEW,
+ onSearchConversationMessagesClick = {},
+ shouldShowSearchButton = false
)
}
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt
index c6cc450170b..22bdbdc652c 100644
--- a/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModel.kt
@@ -54,6 +54,7 @@ import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.conversation.MutedConversationStatus
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.QualifiedID
+import com.wire.kalium.logic.data.user.ConnectionState
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.client.ObserveClientsByUserIdUseCase
import com.wire.kalium.logic.feature.client.PersistOtherUserClientsUseCase
@@ -299,7 +300,13 @@ class OtherUserProfileScreenViewModel @Inject constructor(
viewModelScope.launch {
val shouldArchive = !dialogState.isArchived
requestInProgress = true
- val result = withContext(dispatchers.io()) { updateConversationArchivedStatus(dialogState.conversationId, shouldArchive) }
+ val result = withContext(dispatchers.io()) {
+ updateConversationArchivedStatus(
+ conversationId = dialogState.conversationId,
+ shouldArchiveConversation = shouldArchive,
+ onlyLocally = !dialogState.isMember
+ )
+ }
requestInProgress = false
when (result) {
ArchiveStatusUpdateResult.Failure -> {
@@ -379,9 +386,19 @@ class OtherUserProfileScreenViewModel @Inject constructor(
),
isTeamConversation = conversation.isTeamGroup(),
selfRole = Conversation.Member.Role.Member,
- isArchived = conversation.archived
+ isArchived = conversation.archived,
+ protocol = conversation.protocol,
+ mlsVerificationStatus = conversation.mlsVerificationStatus,
+ proteusVerificationStatus = conversation.proteusVerificationStatus
)
}
)
}
+
+ fun shouldShowSearchButton(conversationId: ConversationId?): Boolean =
+ conversationId != null && state.connectionState in listOf(
+ ConnectionState.ACCEPTED,
+ ConnectionState.BLOCKED,
+ ConnectionState.MISSING_LEGALHOLD_CONSENT
+ )
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt
index 5ade6c96e85..5df06246774 100644
--- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt
@@ -27,12 +27,14 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.BuildConfig
import com.wire.android.appLogger
+import com.wire.android.datastore.GlobalDataStore
import com.wire.android.datastore.UserDataStore
import com.wire.android.di.AuthServerConfigProvider
import com.wire.android.di.CurrentAccount
import com.wire.android.feature.AccountSwitchUseCase
import com.wire.android.feature.SwitchAccountActions
import com.wire.android.feature.SwitchAccountParam
+import com.wire.android.feature.SwitchAccountResult
import com.wire.android.mapper.OtherAccountMapper
import com.wire.android.model.ImageAsset.UserAvatarAsset
import com.wire.android.notification.NotificationChannelsManager
@@ -70,6 +72,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
+// TODO cover this class with unit test
// Suppress for now after removing mockMethodForAvatar it should not complain
@Suppress("TooManyFunctions", "LongParameterList")
@HiltViewModel
@@ -91,7 +94,8 @@ class SelfUserProfileViewModel @Inject constructor(
private val endCall: EndCallUseCase,
private val isReadOnlyAccount: IsReadOnlyAccountUseCase,
private val notificationChannelsManager: NotificationChannelsManager,
- private val notificationManager: WireNotificationManager
+ private val notificationManager: WireNotificationManager,
+ private val globalDataStore: GlobalDataStore
) : ViewModel() {
var userProfileState by mutableStateOf(SelfUserProfileState(userId = selfUserId, isAvatarLoading = true))
@@ -213,8 +217,11 @@ class SelfUserProfileViewModel @Inject constructor(
notificationManager.stopObservingOnLogout(selfUserId)
notificationChannelsManager.deleteChannelGroup(selfUserId)
- accountSwitch(SwitchAccountParam.TryToSwitchToNextAccount)
- .callAction(switchAccountActions)
+ accountSwitch(SwitchAccountParam.TryToSwitchToNextAccount).also {
+ if (it == SwitchAccountResult.NoOtherAccountToSwitch) {
+ globalDataStore.clearAppLockPasscode()
+ }
+ }.callAction(switchAccountActions)
}
}
diff --git a/app/src/main/kotlin/com/wire/android/util/ClipboardCopier.kt b/app/src/main/kotlin/com/wire/android/util/ClipboardCopier.kt
index 2d0135f6e44..d75dc4a8d38 100644
--- a/app/src/main/kotlin/com/wire/android/util/ClipboardCopier.kt
+++ b/app/src/main/kotlin/com/wire/android/util/ClipboardCopier.kt
@@ -23,6 +23,6 @@ package com.wire.android.util
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.text.AnnotatedString
-fun ClipboardManager.copyLinkToClipboard(link: String) {
- setText(AnnotatedString(link))
+fun ClipboardManager.copyLinkToClipboard(text: String) {
+ setText(AnnotatedString(text))
}
diff --git a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt
index 63edb30291a..b8938b5cc9f 100644
--- a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt
+++ b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt
@@ -58,6 +58,7 @@ import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.data.asset.isAudioMimeType
import com.wire.kalium.logic.util.buildFileName
import com.wire.kalium.logic.util.splitFileExtensionAndCopyCounter
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okio.Path
import java.io.File
@@ -112,6 +113,22 @@ fun getTempWritableAttachmentUri(context: Context, attachmentPath: Path): Uri {
return FileProvider.getUriForFile(context, context.getProviderAuthority(), file)
}
+suspend fun createPemFile(
+ pathname: String,
+ content: String
+): File {
+ return withContext(Dispatchers.IO) {
+ return@withContext File(
+ Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_DOWNLOADS
+ ),
+ pathname
+ ).apply {
+ writeText(content)
+ }
+ }
+}
+
private fun Context.saveFileDataToDownloadsFolder(assetName: String, downloadedDataPath: Path, fileSize: Long): Uri? {
val resolver = contentResolver
val mimeType = Uri.parse(downloadedDataPath.toString()).getMimeType(this@saveFileDataToDownloadsFolder)
diff --git a/app/src/main/res/drawable-night/ic_certificate_not_activated_mls.xml b/app/src/main/res/drawable-night/ic_certificate_not_activated_mls.xml
new file mode 100644
index 00000000000..a229be4887f
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_certificate_not_activated_mls.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_certificate_revoked_mls.xml b/app/src/main/res/drawable-night/ic_certificate_revoked_mls.xml
new file mode 100644
index 00000000000..b452e46beb3
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_certificate_revoked_mls.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_certificate_expired_mls.xml b/app/src/main/res/drawable/ic_certificate_expired_mls.xml
new file mode 100644
index 00000000000..9d575b4d809
--- /dev/null
+++ b/app/src/main/res/drawable/ic_certificate_expired_mls.xml
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_certificate_not_activated_mls.xml b/app/src/main/res/drawable/ic_certificate_not_activated_mls.xml
new file mode 100644
index 00000000000..ba2cd1efcea
--- /dev/null
+++ b/app/src/main/res/drawable/ic_certificate_not_activated_mls.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_certificate_revoked_mls.xml b/app/src/main/res/drawable/ic_certificate_revoked_mls.xml
new file mode 100644
index 00000000000..02362ae6361
--- /dev/null
+++ b/app/src/main/res/drawable/ic_certificate_revoked_mls.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml
index 39999a6376e..0c0cae718bd 100644
--- a/app/src/main/res/values-af/strings.xml
+++ b/app/src/main/res/values-af/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index e8d45d64096..53d34e3b335 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -597,7 +597,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index 109ee050226..5d24aa54920 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 39999a6376e..0c0cae718bd 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 7f78a072c11..d2e56b12c56 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -593,7 +593,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 39999a6376e..0c0cae718bd 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 7aa3218c54e..74c74aad2ce 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -590,7 +590,7 @@
%1$s **Teilnehmer** konnten der Gruppe nicht hinzugefügt werden.
%1$s konnten der Gruppe nicht hinzugefügt werden.
%1$s konnte der Gruppe nicht hinzugefügt werden.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 39999a6376e..0c0cae718bd 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 60c2f2eb786..a18a2337682 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -587,7 +587,7 @@ Hasta 500 personas pueden unirse a una conversación en grupo.
%1$s **participants** could not be added to the group.
%1$s no pudieron ser añadidos a la conversación.
%1$s no pudo ser añadido a la conversación.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index ddfa543a5b3..94fd65f80a4 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 109ee050226..5d24aa54920 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 39999a6376e..0c0cae718bd 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 2d3a72e70ba..7cdd6b8822f 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -578,7 +578,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index 240af6cb17c..c3e583b9d16 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -593,7 +593,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 109ee050226..5d24aa54920 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 826b3377474..3d4c79ffb99 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -585,7 +585,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 188d7310541..7eb3b8d911e 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -589,7 +589,7 @@
%1$s **résztvevőket** nem sikerült hozzáadni a csoporthoz.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml
index b6375f34ee7..fcd9ef822a9 100644
--- a/app/src/main/res/values-id/strings.xml
+++ b/app/src/main/res/values-id/strings.xml
@@ -587,7 +587,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 56abf9440ed..984cc48d773 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -587,7 +587,7 @@ Fino a 500 persone possono unirsi a una conversazione di gruppo.
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 1cdf54afb67..129c6204400 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -587,7 +587,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 1cdf54afb67..129c6204400 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -587,7 +587,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index 0634c2d468c..d8e03f4c744 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -593,7 +593,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index 109ee050226..5d24aa54920 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 39999a6376e..0c0cae718bd 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml
index 39999a6376e..0c0cae718bd 100644
--- a/app/src/main/res/values-no/strings.xml
+++ b/app/src/main/res/values-no/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml
index 109ee050226..5d24aa54920 100644
--- a/app/src/main/res/values-pa/strings.xml
+++ b/app/src/main/res/values-pa/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 1c2772760ea..ab8be0a63ad 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -593,7 +593,7 @@ Do grupy może dołączyć maksymalnie 500 osób.
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index e3fcc4f3398..375bddc88c5 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -588,7 +588,7 @@ Até 500 pessoas podem participar de uma conversa em grupo.
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index ecb7160d0d5..424fe7ef390 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -591,7 +591,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index ae990370859..9b24bbe051d 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -593,7 +593,7 @@
Не удалось добавить в группу %1$s **участников**.
Не удалось добавить %1$s в группу.
Не удалось добавить %1$s в группу.
- Эта беседа больше не верифицируется, так как кто-то из пользователей использует по крайней мере одно устройство без действительного сертификата сквозной идентификации.
+ Эта беседа больше не верифицируется, так как кто-то из пользователей использует по крайней мере одно устройство без действительного сертификата сквозной идентификации.
Все устройства верифицированы (сквозная идентификация)
Все отпечатки верифицированы (Proteus)
diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml
index f2cb92e9a70..5be28b47260 100644
--- a/app/src/main/res/values-si/strings.xml
+++ b/app/src/main/res/values-si/strings.xml
@@ -579,7 +579,7 @@
**සහභාගීන්** %1$s ක් සමූහයට එක් කිරීමට නොහැකි විය.
%1$s දෙනෙක් සමූහයට එක් කිරීමට නොහැකි විය.
%1$s දෙනෙක් සමූහයට එක් කිරීමට නොහැකි විය.
- ඇතැම් පරිශ්රීලකයින් වලංගු අන්ත අනන්යතා සහතිකයක් නැතිව අවම වශයෙන් එක් උපාංගයක් භාවිතා කරන බැවින් මෙම සංවාදය තවදුරටත් සත්යාපිත නොවේ.
+ ඇතැම් පරිශ්රීලකයින් වලංගු අන්ත අනන්යතා සහතිකයක් නැතිව අවම වශයෙන් එක් උපාංගයක් භාවිතා කරන බැවින් මෙම සංවාදය තවදුරටත් සත්යාපිත නොවේ.
සියලු උපාංග සත්යාපිතයි (අන්ත අනන්යතාව)
සියලු ඇඟිලි සටහන් සත්යාපිතයි (ප්රෝතියස්)
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 872f9ce6950..c97cab9970c 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -593,7 +593,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index 909f267e803..da9dc9ab421 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -593,7 +593,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index ecb7160d0d5..424fe7ef390 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -591,7 +591,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 650275c6a28..44dc6b8281e 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 1c1afc9d28f..99126c42070 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -589,7 +589,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index c82967628b6..bf372c93951 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -593,7 +593,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 1cdf54afb67..129c6204400 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -587,7 +587,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index b6375f34ee7..fcd9ef822a9 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -587,7 +587,7 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9c35b0067d6..c6cdcbbba73 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -104,7 +104,8 @@
Delete the group
Start audio call
Wire
- Conversation search icon
+ Search icon
+ Search input
Enable rich text mode button
Rich text formatting Header
Rich text formatting Bold
@@ -304,6 +305,18 @@
Invalid password
Remove this device if you have stopped using it. You will be logged out of this device immediately.
Remove your device if you have stopped using it. According to your team\'s security settings, your conversation history will also be deleted.
+
+ End-to-end Identity Certificate
+ Status
+ Serial Number
+ Show Certificate Details
+ Get Certificate
+ Update Certificate
+ Valid
+ Expired
+ Revoked
+ Not activated
+
Add this Device
Enter your password to use Wire on this device.
@@ -417,6 +430,7 @@
you
Add Participants
Name not available
+ Group name not available
Conversation Details
OPTIONS
PARTICIPANTS
@@ -424,6 +438,7 @@
GROUP ADMINS (%d)
GROUP MEMBERS (%d)
This group has %s participants.\nUp to 500 people can join a group conversation.
+ %s participants
Show all participants (%d)
Group Participants
Add participants
@@ -602,9 +617,13 @@
%1$s **participants** could not be added to the group.
%1$s could not be added to the group.
%1$s could not be added to the group.
- This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
+ This conversation is no longer verified, as some user uses at least one device without a valid end-to-end identity certificate.
All devices are verified (end-to-end identity)
All fingerprints are verified (Proteus)
+ Communication in Wire is always end-to-end encrypted. Everything you send and receive in this conversation is only accessible to you and other group participants.\n**Please still be careful with who you share sensitive information.**
+ Verified (Proteus)
+ Verified (End-to-end Identity)
- You added 1 person to the conversation
@@ -652,6 +671,10 @@
left the conversation.
joined the conversation.
left the conversation.
+ All device fingerprints are verified (Proteus)
+ All devices are verified (End-to-end Identity)
+ Conversation is no longer verified
+ Conversation is no longer verified
- %1$d ping
@@ -680,7 +703,12 @@
- %s is typing
- %1$s and %2$d more are typing
-
+
+ Search messages
+ Search all messages in this conversation.
+ No results could be found.\nPlease refine your search and try again.
+ Search
+
CONTACTS
New Group
New Conversation
@@ -816,6 +844,8 @@
If you accept their request, they will be added as a contact and you two can communicate directly.
Accept
Ignore
+ Get certainty about the identity of %s\'s before connecting.
+ Please verify the person\'s identity before accepting the connection request.
Media Gallery
Saved to Downloads folder
@@ -906,7 +936,7 @@
Lock with passcode
Set app lock passcode
- The app will lock itself after %1$s of inactivity. To unlock the app you need to enter this passcode.\n\nMake sure to remember this passcode as there is no way to recover it.
+ The app will lock itself after %1$s of inactivity. To unlock the app you need to enter this passcode or use biometric authentication.\n\nMake sure to remember this passcode as there is no way to recover it.
Passcode
Set a passcode
Enter passcode to unlock Wire
@@ -974,7 +1004,7 @@
MLS Thumbprint
Set password
- The backup will be compressed, and you can encrypt it with a password.
+ The backup will be compressed and encrypted with a password. Make sure to store it in a secure place.
Back Up Now
Creating Backup
Saving conversations...
@@ -1229,4 +1259,9 @@
Authenticate with biometrics
To unlock Wire
Use passcode
+
+ Certificate Details
+ Copy to Clipboard
+ Download
+ Certificate copied to clipboard
diff --git a/app/src/test/kotlin/com/wire/android/framework/TestConversation.kt b/app/src/test/kotlin/com/wire/android/framework/TestConversation.kt
index 3d44a0c7878..01af3d57d03 100644
--- a/app/src/test/kotlin/com/wire/android/framework/TestConversation.kt
+++ b/app/src/test/kotlin/com/wire/android/framework/TestConversation.kt
@@ -52,7 +52,8 @@ object TestConversation {
userMessageTimer = null,
archived = false,
archivedDateTime = null,
- verificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
)
val SELF = Conversation(
ID.copy(value = "SELF ID"),
@@ -73,7 +74,8 @@ object TestConversation {
userMessageTimer = null,
archived = false,
archivedDateTime = null,
- verificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
)
fun GROUP(protocolInfo: ProtocolInfo = ProtocolInfo.Proteus) = Conversation(
@@ -95,7 +97,8 @@ object TestConversation {
userMessageTimer = null,
archived = false,
archivedDateTime = null,
- verificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
)
fun one_on_one(convId: ConversationId) = Conversation(
@@ -117,7 +120,8 @@ object TestConversation {
userMessageTimer = null,
archived = false,
archivedDateTime = null,
- verificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
)
val USER_1 = UserId("member1", "domainMember")
@@ -146,6 +150,7 @@ object TestConversation {
userMessageTimer = null,
archived = false,
archivedDateTime = null,
- verificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
)
}
diff --git a/app/src/test/kotlin/com/wire/android/navigation/NavigationUtilsTest.kt b/app/src/test/kotlin/com/wire/android/navigation/NavigationUtilsTest.kt
index 88397803c4f..fbc47c9d6f2 100644
--- a/app/src/test/kotlin/com/wire/android/navigation/NavigationUtilsTest.kt
+++ b/app/src/test/kotlin/com/wire/android/navigation/NavigationUtilsTest.kt
@@ -103,4 +103,47 @@ internal class NavigationUtilsTest {
// Then
assertEquals(mappedImagePrivateAsset, expectedPrivateAssetImage)
}
+
+ @Test
+ fun `given route without segments and parameters, when getting primary route, then return only host part which is the same route`() {
+ // Given
+ val route = "route"
+ // When
+ val result = route.getBaseRoute()
+ // Then
+ assertEquals(route, result)
+ }
+
+ @Test
+ fun `given route with segments but without parameters, when getting primary route, then return only host part`() {
+ // Given
+ val host = "route"
+ val route = "$host/segment1/segment2"
+ // When
+ val result = route.getBaseRoute()
+ // Then
+ assertEquals(host, result)
+ }
+
+ @Test
+ fun `given route without segments but with parameters, when getting primary route, then return only host part`() {
+ // Given
+ val host = "route"
+ val route = "$host?param1=value1¶m2=value2"
+ // When
+ val result = route.getBaseRoute()
+ // Then
+ assertEquals(host, result)
+ }
+
+ @Test
+ fun `given route with segments and parameters, when getting primary route, then return only host part`() {
+ // Given
+ val host = "route"
+ val route = "$host/segment1/segment2?param1=value1¶m2=value2"
+ // When
+ val result = route.getBaseRoute()
+ // Then
+ assertEquals(host, result)
+ }
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt
index cdfb6dcc203..fe0d6b98338 100644
--- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsViewModelTest.kt
@@ -147,7 +147,8 @@ class GroupConversationDetailsViewModelTest {
conversationId = conversationDetails.conversation.id,
isCreator = conversationDetails.isSelfUserCreator
),
- isArchived = conversationDetails.conversation.archived
+ isArchived = conversationDetails.conversation.archived,
+ isMember = true
)
val (arrangement, viewModel) = GroupConversationDetailsViewModelArrangement()
@@ -167,7 +168,8 @@ class GroupConversationDetailsViewModelTest {
arrangement.updateConversationArchivedStatus(
conversationId = viewModel.conversationId,
shouldArchiveConversation = !conversationDetails.conversation.archived,
- archivedStatusTimestamp = archivingEventTimestamp
+ archivedStatusTimestamp = archivingEventTimestamp,
+ onlyLocally = false
)
}
}
@@ -194,7 +196,8 @@ class GroupConversationDetailsViewModelTest {
conversationId = conversationDetails.conversation.id,
isCreator = conversationDetails.isSelfUserCreator
),
- isArchived = conversationDetails.conversation.archived
+ isArchived = conversationDetails.conversation.archived,
+ isMember = true
)
val (arrangement, viewModel) = GroupConversationDetailsViewModelArrangement()
@@ -214,7 +217,8 @@ class GroupConversationDetailsViewModelTest {
arrangement.updateConversationArchivedStatus(
conversationId = viewModel.conversationId,
shouldArchiveConversation = false,
- archivedStatusTimestamp = archivingEventTimestamp
+ archivedStatusTimestamp = archivingEventTimestamp,
+ onlyLocally = false
)
}
}
@@ -415,7 +419,10 @@ class GroupConversationDetailsViewModelTest {
conversationTypeDetail = ConversationTypeDetail.Group(details.conversation.id, details.isSelfUserCreator),
selfRole = Conversation.Member.Role.Member,
isTeamConversation = details.conversation.isTeamGroup(),
- isArchived = false
+ isArchived = false,
+ protocol = Conversation.ProtocolInfo.Proteus,
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
)
// When - Then
assertEquals(expected, viewModel.conversationSheetContent)
@@ -585,7 +592,8 @@ class GroupConversationDetailsViewModelTest {
userMessageTimer = null,
archived = false,
archivedDateTime = null,
- verificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
),
legalHoldStatus = LegalHoldStatus.DISABLED,
hasOngoingCall = false,
@@ -692,7 +700,7 @@ internal class GroupConversationDetailsViewModelArrangement {
coEvery { isMLSEnabledUseCase() } returns true
coEvery { updateConversationMutedStatus(any(), any(), any()) } returns ConversationUpdateStatusResult.Success
coEvery { observeSelfDeletionTimerSettingsForConversation(any(), any()) } returns flowOf(SelfDeletionTimer.Disabled)
- coEvery { updateConversationArchivedStatus(any(), any()) } returns ArchiveStatusUpdateResult.Success
+ coEvery { updateConversationArchivedStatus(any(), any(), any()) } returns ArchiveStatusUpdateResult.Success
}
suspend fun withConversationDetailUpdate(conversationDetails: ConversationDetails) = apply {
@@ -719,8 +727,8 @@ internal class GroupConversationDetailsViewModelArrangement {
}
suspend fun withUpdateArchivedStatus(result: ArchiveStatusUpdateResult) = apply {
- coEvery { updateConversationArchivedStatus(any(), any()) } returns result
coEvery { updateConversationArchivedStatus(any(), any(), any()) } returns result
+ coEvery { updateConversationArchivedStatus(any(), any(), any(), any()) } returns result
}
fun arrange() = this to viewModel
diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt
index cc73a66a62c..0d10563c47b 100644
--- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/info/ConversationInfoViewModelTest.kt
@@ -233,8 +233,8 @@ class ConversationInfoViewModelTest {
viewModel.conversationInfoViewState.protocolInfo
)
assertEquals(
- groupConversationDetails.conversation.verificationStatus,
- viewModel.conversationInfoViewState.verificationStatus
+ groupConversationDetails.conversation.mlsVerificationStatus,
+ viewModel.conversationInfoViewState.mlsVerificationStatus
)
cancel()
}
@@ -257,8 +257,8 @@ class ConversationInfoViewModelTest {
viewModel.conversationInfoViewState.protocolInfo
)
assertEquals(
- groupConversationDetails.conversation.verificationStatus,
- viewModel.conversationInfoViewState.verificationStatus
+ groupConversationDetails.conversation.mlsVerificationStatus,
+ viewModel.conversationInfoViewState.mlsVerificationStatus
)
cancel()
}
@@ -281,8 +281,8 @@ class ConversationInfoViewModelTest {
viewModel.conversationInfoViewState.protocolInfo
)
assertEquals(
- groupConversationDetails.conversation.verificationStatus,
- viewModel.conversationInfoViewState.verificationStatus
+ groupConversationDetails.conversation.mlsVerificationStatus,
+ viewModel.conversationInfoViewState.mlsVerificationStatus
)
cancel()
}
@@ -302,7 +302,7 @@ class ConversationInfoViewModelTest {
)
assertEquals(
null,
- viewModel.conversationInfoViewState.verificationStatus
+ viewModel.conversationInfoViewState.mlsVerificationStatus
)
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt
new file mode 100644
index 00000000000..0db253effeb
--- /dev/null
+++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/SearchConversationMessagesViewModelTest.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.messages
+
+import android.os.Bundle
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.core.os.bundleOf
+import androidx.lifecycle.SavedStateHandle
+import com.wire.android.config.CoroutineTestExtension
+import com.wire.android.config.NavigationTestExtension
+import com.wire.android.config.mockUri
+import com.wire.android.ui.home.conversations.mock.mockMessageWithText
+import com.wire.android.ui.home.conversations.model.MessageBody
+import com.wire.android.ui.home.conversations.model.UIMessage
+import com.wire.android.ui.home.conversations.model.UIMessageContent
+import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesNavArgs
+import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesViewModel
+import com.wire.android.ui.home.conversations.usecase.GetConversationMessagesFromSearchUseCase
+import com.wire.android.ui.navArgs
+import com.wire.android.util.ui.UIText
+import com.wire.kalium.logic.data.id.ConversationId
+import com.wire.kalium.logic.functional.Either
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.internal.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(CoroutineTestExtension::class)
+@ExtendWith(NavigationTestExtension::class)
+class SearchConversationMessagesViewModelTest {
+
+ @Test
+ fun `given search term, when searching for messages, then specific messages are returned`() =
+ runTest() {
+ // given
+ val searchTerm = "message"
+ val messages = listOf(
+ mockMessageWithText.copy(
+ messageContent = UIMessageContent.TextMessage(
+ messageBody = MessageBody(
+ UIText.DynamicString("message1")
+ )
+ )
+ ),
+ mockMessageWithText.copy(
+ messageContent = UIMessageContent.TextMessage(
+ messageBody = MessageBody(
+ UIText.DynamicString("message2")
+ )
+ )
+ )
+ )
+
+ val (arrangement, viewModel) = SearchConversationMessagesViewModelArrangement()
+ .withSuccessSearch(searchTerm, messages)
+ .arrange()
+
+ // when
+ viewModel.searchQueryChanged(TextFieldValue(searchTerm))
+ advanceUntilIdle()
+
+ // then
+ assertEquals(
+ TextFieldValue(searchTerm),
+ viewModel.searchConversationMessagesState.searchQuery
+ )
+ coVerify(exactly = 1) {
+ arrangement.getSearchMessagesForConversation(
+ searchTerm,
+ arrangement.conversationId
+ )
+ }
+ }
+
+ class SearchConversationMessagesViewModelArrangement {
+ val conversationId: ConversationId = ConversationId(
+ value = "some-dummy-value",
+ domain = "some-dummy-domain"
+ )
+
+ @MockK
+ private lateinit var savedStateHandle: SavedStateHandle
+
+ @MockK
+ lateinit var getSearchMessagesForConversation: GetConversationMessagesFromSearchUseCase
+
+ private val viewModel: SearchConversationMessagesViewModel by lazy {
+ SearchConversationMessagesViewModel(
+ getSearchMessagesForConversation = getSearchMessagesForConversation,
+ savedStateHandle = savedStateHandle
+ )
+ }
+
+ init {
+ // Tests setup
+ MockKAnnotations.init(this, relaxUnitFun = true)
+ mockUri()
+ every { savedStateHandle.navArgs() } returns SearchConversationMessagesNavArgs(
+ conversationId = conversationId
+ )
+ every { savedStateHandle.get("searchConversationMessagesState") } returns bundleOf("value" to "")
+ }
+
+ suspend fun withSuccessSearch(
+ searchTerm: String,
+ messages: List
+ ) = apply {
+ coEvery {
+ getSearchMessagesForConversation(eq(searchTerm), eq(conversationId))
+ } returns Either.Right(messages)
+ }
+
+ fun arrange() = this to viewModel
+ }
+}
diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCaseTest.kt
new file mode 100644
index 00000000000..f52b90449dc
--- /dev/null
+++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/usecase/GetConversationMessagesFromSearchUseCaseTest.kt
@@ -0,0 +1,194 @@
+/*
+ * 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.usecase
+
+import com.wire.android.config.CoroutineTestExtension
+import com.wire.android.framework.TestMessage
+import com.wire.android.framework.TestUser
+import com.wire.android.mapper.MessageMapper
+import com.wire.android.model.UserAvatarData
+import com.wire.android.ui.home.conversations.model.MessageBody
+import com.wire.android.ui.home.conversations.model.MessageSource
+import com.wire.android.ui.home.conversations.model.UIMessage
+import com.wire.android.ui.home.conversations.model.UIMessageContent
+import com.wire.android.util.ui.UIText
+import com.wire.kalium.logic.data.id.ConversationId
+import com.wire.kalium.logic.data.message.Message
+import com.wire.kalium.logic.data.message.MessageContent
+import com.wire.kalium.logic.data.user.User
+import com.wire.kalium.logic.data.user.UserAvailabilityStatus
+import com.wire.kalium.logic.data.user.UserId
+import com.wire.kalium.logic.feature.conversation.ObserveUserListByIdUseCase
+import com.wire.kalium.logic.feature.message.GetConversationMessagesFromSearchQueryUseCase
+import com.wire.kalium.logic.functional.Either
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.internal.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import java.util.UUID
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(CoroutineTestExtension::class)
+class GetConversationMessagesFromSearchUseCaseTest {
+
+ @Test
+ fun `given below minimum characters to search, when searching, then return an empty list`() =
+ runTest {
+ // given
+ val (arrangement, useCase) = Arrangement()
+ .arrange()
+
+ // when
+ val result = useCase("a", arrangement.conversationId)
+
+ // then
+ assert(result is Either.Right>)
+ assertEquals(
+ Either.Right(listOf()),
+ result
+ )
+ }
+
+ @Test
+ fun `given search term, when searching messages, then return messages list`() = runTest {
+ // given
+ val (arrangement, useCase) = Arrangement()
+ .withSearchSuccess()
+ .withMemberIdList()
+ .withMemberDetails()
+ .withMappedMessage(
+ user = Arrangement.user1,
+ message = Arrangement.message1
+ )
+ .withMappedMessage(
+ user = Arrangement.user2,
+ message = Arrangement.message2
+ )
+ .arrange()
+
+ // when
+ val result = useCase(arrangement.searchTerm, arrangement.conversationId)
+
+ // then
+ assert(result is Either.Right>)
+ assertEquals(
+ Arrangement.messages.size,
+ (result as Either.Right).value.size
+ )
+ }
+
+ class Arrangement {
+ val searchTerm = "message"
+ val conversationId = ConversationId(
+ value = "some-dummy-value",
+ domain = "some-dummy-domain"
+ )
+
+ @MockK
+ lateinit var getConversationMessagesFromSearch: GetConversationMessagesFromSearchQueryUseCase
+
+ @MockK
+ lateinit var observeMemberDetailsByIds: ObserveUserListByIdUseCase
+
+ @MockK
+ lateinit var messageMapper: MessageMapper
+
+ private val useCase: GetConversationMessagesFromSearchUseCase by lazy {
+ GetConversationMessagesFromSearchUseCase(
+ getConversationMessagesFromSearch,
+ observeMemberDetailsByIds,
+ messageMapper
+ )
+ }
+
+ init {
+ MockKAnnotations.init(this, relaxUnitFun = true)
+ }
+
+ suspend fun withSearchSuccess() = apply {
+ coEvery {
+ getConversationMessagesFromSearch(searchTerm, conversationId)
+ } returns Either.Right(messages)
+ }
+
+ fun withMemberIdList() = apply {
+ every { messageMapper.memberIdList(messages) } returns listOf(
+ message1.senderUserId,
+ message2.senderUserId
+ )
+ }
+
+ suspend fun withMemberDetails() = apply {
+ coEvery { observeMemberDetailsByIds(any()) } returns flowOf(
+ listOf(user1, user2)
+ )
+ }
+
+ fun withMappedMessage(user: User, message: Message.Standalone) = apply {
+ every { messageMapper.toUIMessage(users, message) } returns UIMessage.Regular(
+ userAvatarData = UserAvatarData(
+ asset = null,
+ availabilityStatus = UserAvailabilityStatus.NONE
+ ),
+ source = MessageSource.OtherUser,
+ header = TestMessage.UI_MESSAGE_HEADER.copy(
+ messageId = UUID.randomUUID().toString(),
+ userId = user.id
+ ),
+ messageContent = UIMessageContent.TextMessage(
+ MessageBody(
+ UIText.DynamicString(
+ (message.content as MessageContent.Text).value
+ )
+ )
+ ),
+ messageFooter = com.wire.android.ui.home.conversations.model.MessageFooter(
+ TestMessage.UI_MESSAGE_HEADER.messageId
+ )
+ )
+ }
+
+ fun arrange() = this to useCase
+
+ companion object {
+ val user1 = TestUser.OTHER_USER.copy(
+ id = UserId("user-id1", "domain")
+ )
+ val user2 = TestUser.OTHER_USER.copy(
+ id = UserId("user-id2", "domain")
+ )
+ val users = listOf(user1, user2)
+
+ val message1 = TestMessage.TEXT_MESSAGE.copy(
+ content = MessageContent.Text("message1"),
+ senderUserId = user1.id
+ )
+ val message2 = TestMessage.TEXT_MESSAGE.copy(
+ content = MessageContent.Text("message2"),
+ senderUserId = user2.id
+ )
+ val messages = listOf(message1, message2)
+ }
+ }
+}
diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt
index 336156ca679..1de57abc136 100644
--- a/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/home/conversationslist/ConversationListViewModelTest.kt
@@ -42,6 +42,7 @@ import com.wire.android.ui.home.conversationslist.model.DialogState
import com.wire.android.ui.home.conversationslist.model.Membership
import com.wire.android.util.orDefault
import com.wire.android.util.ui.WireSessionImageLoader
+import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.conversation.MutedConversationStatus
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.user.UserId
@@ -337,11 +338,12 @@ class ConversationListViewModelTest {
conversationItem.conversationId,
conversationItem.conversationInfo.name,
ConversationTypeDetail.Private(null, conversationItem.userId, BlockingState.NOT_BLOCKED),
- !isArchiving
+ !isArchiving,
+ true
)
val archivingTimestamp = 123456789L
- coEvery { updateConversationArchivedStatus(any(), any(), any()) } returns ArchiveStatusUpdateResult.Success
+ coEvery { updateConversationArchivedStatus(any(), any(), any(), any()) } returns ArchiveStatusUpdateResult.Success
conversationListViewModel.homeSnackBarState.test {
conversationListViewModel.moveConversationToArchive(dialogState, archivingTimestamp)
@@ -351,6 +353,7 @@ class ConversationListViewModelTest {
updateConversationArchivedStatus.invoke(
dialogState.conversationId,
!dialogState.isArchived,
+ onlyLocally = false,
archivingTimestamp
)
}
@@ -363,11 +366,12 @@ class ConversationListViewModelTest {
conversationItem.conversationId,
conversationItem.conversationInfo.name,
ConversationTypeDetail.Private(null, conversationItem.userId, BlockingState.NOT_BLOCKED),
- !isArchiving
+ !isArchiving,
+ isMember = true
)
val archivingTimestamp = 123456789L
- coEvery { updateConversationArchivedStatus(any(), any(), any()) } returns ArchiveStatusUpdateResult.Failure
+ coEvery { updateConversationArchivedStatus(any(), any(), any(), any()) } returns ArchiveStatusUpdateResult.Failure
conversationListViewModel.homeSnackBarState.test {
conversationListViewModel.moveConversationToArchive(dialogState, archivingTimestamp)
@@ -377,7 +381,8 @@ class ConversationListViewModelTest {
updateConversationArchivedStatus.invoke(
dialogState.conversationId,
!dialogState.isArchived,
- archivingTimestamp
+ false,
+ archivingTimestamp,
)
}
}
@@ -402,7 +407,9 @@ class ConversationListViewModelTest {
userId = userId,
blockingState = BlockingState.CAN_NOT_BE_BLOCKED,
teamId = null,
- isArchived = false
+ isArchived = false,
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
)
}
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt
index 50eb8b96de6..e59862ddd9e 100644
--- a/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/home/gallery/MediaGalleryViewModelTest.kt
@@ -305,7 +305,8 @@ class MediaGalleryViewModelTest {
userMessageTimer = null,
archived = false,
archivedDateTime = null,
- verificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
),
otherUser = OtherUser(
QualifiedID("other-user-id", "domain-id"),
diff --git a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt
index 3976b38b2a6..f3359c3a3df 100644
--- a/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/home/newconversation/NewConversationViewModelArrangement.kt
@@ -159,7 +159,8 @@ internal class NewConversationViewModelArrangement {
userMessageTimer = null,
archived = false,
archivedDateTime = null,
- verificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
)
val PUBLIC_USER = OtherUser(
diff --git a/app/src/test/kotlin/com/wire/android/ui/home/settings/home/BackupAndRestoreViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/settings/home/BackupAndRestoreViewModelTest.kt
index 105215cddad..95291a62a38 100644
--- a/app/src/test/kotlin/com/wire/android/ui/home/settings/home/BackupAndRestoreViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/home/settings/home/BackupAndRestoreViewModelTest.kt
@@ -21,6 +21,7 @@
package com.wire.android.ui.home.settings.home
import android.net.Uri
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.core.net.toUri
import com.wire.android.config.CoroutineTestExtension
import com.wire.android.config.TestDispatcherProvider
@@ -33,6 +34,8 @@ import com.wire.android.ui.home.settings.backup.PasswordValidation
import com.wire.android.ui.home.settings.backup.RestoreFileValidation
import com.wire.android.util.FileManager
import com.wire.kalium.logic.CoreFailure
+import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
+import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase
import com.wire.kalium.logic.feature.backup.CreateBackupResult
import com.wire.kalium.logic.feature.backup.CreateBackupUseCase
import com.wire.kalium.logic.feature.backup.RestoreBackupResult
@@ -103,6 +106,55 @@ class BackupAndRestoreViewModelTest {
coVerify(exactly = 1) { arrangement.createBackupFile(password = password) }
}
+ @Test
+ fun givenAnEmptyPassword_whenValidating_thenItUpdatePasswordStateToValid() = runTest(dispatcher.default()) {
+ // Given
+ val password = ""
+ val (arrangement, backupAndRestoreViewModel) = Arrangement()
+ .withInvalidPassword()
+ .arrange()
+
+ // When
+ backupAndRestoreViewModel.validateBackupCreationPassword(TextFieldValue(password))
+ advanceUntilIdle()
+
+ // Then
+ assert(backupAndRestoreViewModel.state.passwordValidation.isValid)
+ coVerify(exactly = 0) { arrangement.validatePassword(any()) }
+ }
+
+ @Test
+ fun givenANonEmptyPassword_whenItIsInvalid_thenItUpdatePasswordValidationState() = runTest(dispatcher.default()) {
+ // Given
+ val password = "mayTh3ForceBeWIthYou"
+ val (arrangement, backupAndRestoreViewModel) = Arrangement()
+ .withInvalidPassword()
+ .arrange()
+
+ // When
+ backupAndRestoreViewModel.validateBackupCreationPassword(TextFieldValue(password))
+ advanceUntilIdle()
+
+ // Then
+ assert(!backupAndRestoreViewModel.state.passwordValidation.isValid)
+ }
+
+ @Test
+ fun givenANonEmptyPassword_whenItIsValid_thenItUpdatePasswordValidationState() = runTest(dispatcher.default()) {
+ // Given
+ val password = "mayTh3ForceBeWIthYou_"
+ val (arrangement, backupAndRestoreViewModel) = Arrangement()
+ .withValidPassword()
+ .arrange()
+
+ // When
+ backupAndRestoreViewModel.validateBackupCreationPassword(TextFieldValue(password))
+ advanceUntilIdle()
+
+ // Then
+ assert(backupAndRestoreViewModel.state.passwordValidation.isValid)
+ }
+
@Test
fun givenANonEmptyPassword_whenCreatingABackupWithAGivenError_thenItReturnsAFailure() = runTest(dispatcher.default()) {
// Given
@@ -412,6 +464,9 @@ class BackupAndRestoreViewModelTest {
@MockK
private lateinit var verifyBackup: VerifyBackupUseCase
+ @MockK
+ lateinit var validatePassword: ValidatePasswordUseCase
+
@MockK
lateinit var fileManager: FileManager
@@ -423,7 +478,8 @@ class BackupAndRestoreViewModelTest {
verifyBackup = verifyBackup,
kaliumFileSystem = fakeKaliumFileSystem,
dispatcher = dispatcher,
- fileManager = fileManager
+ fileManager = fileManager,
+ validatePassword = validatePassword
)
fun withSuccessfulCreation(password: String) = apply {
@@ -491,6 +547,14 @@ class BackupAndRestoreViewModelTest {
coEvery { importBackup(any(), any()) } returns error
}
+ fun withValidPassword() = apply {
+ every { validatePassword(any()) } returns ValidatePasswordResult.Valid
+ }
+
+ fun withInvalidPassword() = apply {
+ every { validatePassword(any()) } returns ValidatePasswordResult.Invalid()
+ }
+
fun arrange() = this to viewModel
}
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt
index 657c453b3d5..cb4ef9d9c74 100644
--- a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt
@@ -18,6 +18,8 @@ import com.wire.kalium.logic.feature.client.DeleteClientUseCase
import com.wire.kalium.logic.feature.client.GetClientDetailsResult
import com.wire.kalium.logic.feature.client.ObserveClientDetailsUseCase
import com.wire.kalium.logic.feature.client.UpdateClientVerificationStatusUseCase
+import com.wire.kalium.logic.feature.e2ei.usecase.GetE2EICertificateUseCaseResult
+import com.wire.kalium.logic.feature.e2ei.usecase.GetE2eiCertificateUseCase
import com.wire.kalium.logic.feature.user.GetUserInfoResult
import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase
import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase
@@ -212,6 +214,9 @@ class DeviceDetailsViewModelTest {
@MockK
lateinit var observeUserInfo: ObserveUserInfoUseCase
+ @MockK
+ lateinit var getE2eiCertificate: GetE2eiCertificateUseCase
+
@MockK(relaxed = true)
lateinit var onSuccess: () -> Unit
@@ -226,7 +231,8 @@ class DeviceDetailsViewModelTest {
fingerprintUseCase = deviceFingerprint,
updateClientVerificationStatus = updateClientVerificationStatus,
currentUserId = currentUserId,
- observeUserInfo = observeUserInfo
+ observeUserInfo = observeUserInfo,
+ e2eiCertificate = getE2eiCertificate
)
}
@@ -234,6 +240,7 @@ class DeviceDetailsViewModelTest {
MockKAnnotations.init(this, relaxUnitFun = true)
withFingerprintSuccess()
coEvery { observeUserInfo(any()) } returns flowOf(GetUserInfoResult.Success(TestUser.OTHER_USER, null))
+ coEvery { getE2eiCertificate(any()) } returns GetE2EICertificateUseCaseResult.Failure.NotActivated
}
fun withUserRequiresPasswordResult(result: IsPasswordRequiredUseCase.Result = IsPasswordRequiredUseCase.Result.Success(true)) =
diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt
index a7c48a3c747..61aa56606fa 100644
--- a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileScreenViewModelTest.kt
@@ -194,7 +194,7 @@ class OtherUserProfileScreenViewModelTest {
@Test
fun `given not connected user, then direct conversation is not found`() = runTest {
// given
- val (arrangement, viewModel) = OtherUserProfileViewModelArrangement()
+ val (_, viewModel) = OtherUserProfileViewModelArrangement()
.withUserInfo(
GetUserInfoResult.Success(OTHER_USER.copy(connectionStatus = ConnectionState.NOT_CONNECTED), TEAM)
)
@@ -247,7 +247,8 @@ class OtherUserProfileScreenViewModelTest {
userMessageTimer = null,
archived = false,
archivedDateTime = null,
- verificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
+ mlsVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED,
+ proteusVerificationStatus = Conversation.VerificationStatus.NOT_VERIFIED
)
val CONVERSATION_ROLE_DATA = ConversationRoleData(
"some_name",
diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt
index 39f2fb89b0d..6bf5877728c 100644
--- a/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/other/OtherUserProfileViewModelArrangement.kt
@@ -144,7 +144,7 @@ internal class OtherUserProfileViewModelArrangement {
)
)
coEvery { observeSelfUser() } returns flowOf(TestUser.SELF_USER)
- coEvery { updateConversationArchivedStatus(any(), any()) } returns ArchiveStatusUpdateResult.Success
+ coEvery { updateConversationArchivedStatus(any(), any(), any()) } returns ArchiveStatusUpdateResult.Success
every { userTypeMapper.toMembership(any()) } returns Membership.None
coEvery { getOneToOneConversation(USER_ID) } returns flowOf(
GetOneToOneConversationUseCase.Result.Success(OtherUserProfileScreenViewModelTest.CONVERSATION)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0996e8719c3..a601f24280b 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -38,6 +38,7 @@ androidx-biometric = "1.1.0"
# Compose
composeBom = "2023.10.00" # TODO check if in new version [anchoredDraggable] is available
+compose-activity = "1.8.0"
compose-compiler = "1.5.2"
compose-constraint = "1.0.1"
compose-navigation = "2.7.3" # adjusted to work with compose-destinations "1.9.54"
@@ -159,7 +160,6 @@ hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hilt-work" }
# Compose BOM
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
-compose-activity = { module = "androidx.activity:activity-compose" }
compose-foundation = { module = "androidx.compose.foundation:foundation" }
compose-material-core = { module = "androidx.compose.material:material" }
compose-material-icons = { module = "androidx.compose.material:material-icons-extended" }
@@ -173,6 +173,7 @@ compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-ui-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
# Compose other
+compose-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-activity" }
compose-constraintLayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "compose-constraint" }
compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "compose-navigation" }
compose-destinations-core = { module = "io.github.raamcosta.compose-destinations:animations-core", version.ref = "compose-destinations" }
diff --git a/kalium b/kalium
index c82d8aa7757..07301bd8a12 160000
--- a/kalium
+++ b/kalium
@@ -1 +1 @@
-Subproject commit c82d8aa7757ddb2b22c6ded6120daed6e2c35a2a
+Subproject commit 07301bd8a120b930e057a94c1d80fb99e9edc16f