diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3babe8fe18..ce614343c5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -104,41 +104,33 @@
+ android:label="@string/sessionMessageRequests"/>
+ android:name="org.thoughtcrime.securesms.home.PathActivity"/>
+ android:label="@string/sessionSettings" />
+ android:theme="@style/Theme.Session.DayNight.NoActionBar" />
+ android:name="org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity" />
+ android:label="@string/sessionPrivacy" />
+ android:name="org.thoughtcrime.securesms.preferences.NotificationSettingsActivity" />
+ android:name="org.thoughtcrime.securesms.preferences.ChatSettingsActivity" />
-
+ android:label="@string/sessionHelp"/>
+
@@ -264,7 +240,6 @@
+ android:windowSoftInputMode="stateHidden"/>
+
+ android:theme="@style/Theme.Session.DayNight.NoActionBar"
+ android:configChanges="orientation|screenSize|keyboardHidden|layoutDirection">
diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.kt
index 2a880b273f..4bf93fd13d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.kt
@@ -147,11 +147,11 @@ abstract class BaseActionBarActivity : AppCompatActivity() {
}
override fun onSupportNavigateUp(): Boolean {
- if (super.onSupportNavigateUp()) return true
-
- onBackPressed()
+ if (handleNavigateUp()) return true
+ onBackPressedDispatcher.onBackPressed()
return true
}
+ protected open fun handleNavigateUp(): Boolean = false
private fun initializeScreenshotSecurity(isResume: Boolean) {
if (!isResume) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt
index a3444934fc..ad45bf04fc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.kt
@@ -27,6 +27,7 @@ import android.net.Uri
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
+import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
@@ -100,6 +101,7 @@ import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.FilenameUtils.getFilenameFromUri
import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Companion.showOneTimeWarningDialogOrSave
+import org.thoughtcrime.securesms.util.applySafeInsetsPaddings
import java.io.IOException
import java.util.WeakHashMap
import javax.inject.Inject
@@ -172,8 +174,12 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(),
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime())
windowInsetBottom = insets.bottom
-
- binding.toolbar.updatePadding(top = insets.top)
+
+ binding.toolbar.updatePadding(
+ left = insets.left,
+ top = insets.top,
+ right = insets.right
+ )
binding.mediaPreviewAlbumRailContainer.updatePadding(bottom = insets.bottom)
updateControlsPosition()
@@ -258,9 +264,6 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(),
}
private fun showAlbumRail() {
- // never show the rail in landscape
- if(isLandscape()) return
-
val rail = binding.mediaPreviewAlbumRailContainer
rail.animate().cancel()
rail.visibility = View.VISIBLE
@@ -389,13 +392,11 @@ class MediaPreviewActivity : ScreenLockActionBarActivity(),
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
- // always hide the rail in landscape
- if (isLandscape()) {
- hideAlbumRail()
+
+ if (!isFullscreen) {
+ showAlbumRail()
} else {
- if (!isFullscreen) {
- showAlbumRail()
- }
+ hideAlbumRail()
}
// Re-apply fullscreen if we were already in it
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
index 95b2350001..cc4fe512d5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
@@ -43,6 +43,7 @@ import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
+import androidx.core.view.updatePadding
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
@@ -684,6 +685,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
val navInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
+ val systemBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
+
val keyboardVisible = imeInsets.bottom > 0
if (keyboardVisible != isKeyboardVisible) {
@@ -701,6 +704,11 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
height = if (keyboardVisible) imeInsets.bottom else navInsets.bottom
}
+ binding.contentContainer.updatePadding(
+ left = systemBarsInsets.left,
+ right = systemBarsInsets.right
+ )
+
windowInsets
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
index 5b483b41a2..465801cde8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt
@@ -140,7 +140,7 @@ class ConversationReactionOverlay : FrameLayout {
// Use your existing utility to handle insets
applySafeInsetsPaddings(
- typeMask = WindowInsetsCompat.Type.systemBars(),
+ typeMask = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
consumeInsets = false, // Don't consume so children can also access them
applyTop = false, // Don't apply as padding, just capture the values
applyBottom = false
@@ -211,7 +211,17 @@ class ConversationReactionOverlay : FrameLayout {
val contextMenu = ConversationContextMenu(dropdownAnchor, recipient?.let { getMenuActionItems(messageRecord, it) }.orEmpty())
this.contextMenu = contextMenu
- var endX = if (isMessageOnLeft) scrubberHorizontalMargin.toFloat() else selectedConversationModel.bubbleX - conversationItem.width + selectedConversationModel.bubbleWidth
+ // Visual left/right edges that account for system insets and configured margin.
+ val leftEdge = (systemInsets.left + scrubberHorizontalMargin).toFloat()
+ val rightEdge = (width - systemInsets.right - scrubberHorizontalMargin).toFloat()
+
+ // Start the bubble aligned to the same visual edge as the scrubber.
+ var endX = if (isMessageOnLeft) {
+ leftEdge
+ } else {
+ rightEdge - conversationItem.width
+ }
+
var endY = selectedConversationModel.bubbleY - statusBarHeight
conversationItem.x = endX
conversationItem.y = endY
@@ -312,13 +322,23 @@ class ConversationReactionOverlay : FrameLayout {
// Adjust for system insets
reactionBarBackgroundY = maxOf(reactionBarBackgroundY, systemInsets.top.toFloat() - statusBarHeight)
+
+ // Now that endScale is final, clamp the bubble X so it stays fully within the visual edges.
+ val minBubbleX = leftEdge
+ val maxBubbleX = rightEdge
+ endX = endX.coerceIn(minBubbleX, maxBubbleX)
+ // Ensure initial position is corrected before making the overlay visible.
+ conversationItem.x = endX
+ conversationItem.y = endY
+
hideAnimatorSet.end()
visibility = VISIBLE
+ // Place the scrubber on the same visual edges (accounting for its own width on the right).
val scrubberX = if (isMessageOnLeft) {
- scrubberHorizontalMargin.toFloat()
+ leftEdge
} else {
- (width - scrubberWidth - scrubberHorizontalMargin).toFloat()
+ (rightEdge - scrubberWidth)
}
foregroundView.x = scrubberX
@@ -332,22 +352,37 @@ class ConversationReactionOverlay : FrameLayout {
revealAnimatorSet.start()
if (isWideLayout) {
- val scrubberRight = scrubberX + scrubberWidth
- val offsetX = when {
- isMessageOnLeft -> scrubberRight + menuPadding
- else -> scrubberX - contextMenu.getMaxWidth() - menuPadding
+ val menuXInOverlay = if (isMessageOnLeft) {
+ // Menu to the RIGHT of the scrubber
+ scrubberX + scrubberWidth + menuPadding
+ } else {
+ // Menu to the LEFT of the scrubber - use MENU width here, not scrubber width
+ scrubberX - contextMenu.getMaxWidth() - menuPadding
}
- // Adjust Y position to account for insets
- val adjustedY = minOf(backgroundView.y, (availableHeight - actualMenuHeight).toFloat()).toInt()
- contextMenu.show(offsetX.toInt(), adjustedY)
+
+ val maxMenuYInOverlay = (height - systemInsets.bottom - actualMenuHeight).toFloat()
+ val menuYInOverlay = minOf(backgroundView.y, maxMenuYInOverlay)
+
+ // Convert overlay-local to anchor relative as expected by ConversationContextMenu.show()
+ val (xOffset, yOffset) = toAnchorOffsets(menuXInOverlay, menuYInOverlay)
+ contextMenu.show(xOffset, yOffset)
+
} else {
- val contentX = if (isMessageOnLeft) scrubberHorizontalMargin.toFloat() else selectedConversationModel.bubbleX
- val offsetX = when {
- isMessageOnLeft -> contentX
- else -> -contextMenu.getMaxWidth() + contentX + bubbleWidth
+ val menuXInOverlay = if (isMessageOnLeft) {
+ leftEdge
+ } else {
+ rightEdge - contextMenu.getMaxWidth()
}
+
val menuTop = endApparentTop + conversationItemSnapshot.height * endScale
- contextMenu.show(offsetX.toInt(), (menuTop + menuPadding).toInt())
+ val menuYInOverlay = (menuTop + menuPadding)
+ .coerceIn(
+ systemInsets.top.toFloat(),
+ (height - systemInsets.bottom - actualMenuHeight).toFloat()
+ )
+
+ val (xOffset, yOffset) = toAnchorOffsets(menuXInOverlay, menuYInOverlay)
+ contextMenu.show(xOffset, yOffset)
}
val revealDuration = context.resources.getInteger(R.integer.reaction_scrubber_reveal_duration)
@@ -361,6 +396,12 @@ class ConversationReactionOverlay : FrameLayout {
.setDuration(revealDuration.toLong())
}
+ private fun toAnchorOffsets(xInOverlay: Float, yInOverlay: Float): Pair {
+ val xOffset = (xInOverlay - dropdownAnchor.x).toInt()
+ val yOffset = (yInOverlay - dropdownAnchor.y).toInt()
+ return xOffset to yOffset
+ }
+
private fun getReactionBarOffsetForTouch(itemY: Float,
contextMenuTop: Float,
contextMenuPadding: Float,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt
index 80930dfa7d..9ede279de7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt
@@ -8,7 +8,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
@@ -72,7 +72,7 @@ fun ConversationV2Dialogs(
// delete message(s)
if(dialogsState.deleteEveryone != null){
val data = dialogsState.deleteEveryone
- var deleteForEveryone by remember { mutableStateOf(data.defaultToEveryone)}
+ var deleteForEveryone by retain { mutableStateOf(data.defaultToEveryone)}
AlertDialog(
onDismissRequest = {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsDialogs.kt
index 2492c99893..0585b02992 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsDialogs.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsDialogs.kt
@@ -9,6 +9,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -287,7 +288,7 @@ fun GroupAdminClearMessagesDialog(
groupName: String,
sendCommand: (ConversationSettingsViewModel.Commands) -> Unit,
){
- var deleteForEveryone by remember { mutableStateOf(false) }
+ var deleteForEveryone by retain { mutableStateOf(false) }
val context = LocalContext.current
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt
index 3460c6f325..1a656be1c0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsNavHost.kt
@@ -9,12 +9,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.lifecycle.repeatOnLifecycle
-import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
@@ -26,19 +26,29 @@ import org.session.libsession.utilities.Address
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesViewModel
import org.thoughtcrime.securesms.conversation.disappearingmessages.ui.DisappearingMessagesScreen
-import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.*
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteAllMedia
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteConversationSettings
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteDisappearingMessages
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteGroupMembers
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteInviteAccountIdToGroup
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteInviteToCommunity
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteInviteToGroup
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteManageAdmins
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteManageMembers
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RouteNotifications
+import org.thoughtcrime.securesms.conversation.v2.settings.ConversationSettingsDestination.RoutePromoteMembers
import org.thoughtcrime.securesms.conversation.v2.settings.notification.NotificationSettingsScreen
import org.thoughtcrime.securesms.conversation.v2.settings.notification.NotificationSettingsViewModel
-import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel
import org.thoughtcrime.securesms.groups.GroupMembersViewModel
import org.thoughtcrime.securesms.groups.InviteMembersViewModel
import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel
+import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel
import org.thoughtcrime.securesms.groups.PromoteMembersViewModel
-import org.thoughtcrime.securesms.groups.compose.ManageGroupMembersScreen
import org.thoughtcrime.securesms.groups.compose.GroupMembersScreen
import org.thoughtcrime.securesms.groups.compose.InviteAccountIdScreen
import org.thoughtcrime.securesms.groups.compose.InviteContactsScreen
import org.thoughtcrime.securesms.groups.compose.ManageGroupAdminsScreen
+import org.thoughtcrime.securesms.groups.compose.ManageGroupMembersScreen
import org.thoughtcrime.securesms.groups.compose.PromoteMembersScreen
import org.thoughtcrime.securesms.home.startconversation.newmessage.NewMessageViewModel
import org.thoughtcrime.securesms.home.startconversation.newmessage.State
@@ -46,7 +56,6 @@ import org.thoughtcrime.securesms.media.MediaOverviewScreen
import org.thoughtcrime.securesms.media.MediaOverviewViewModel
import org.thoughtcrime.securesms.ui.NavigationAction
import org.thoughtcrime.securesms.ui.ObserveAsEvents
-import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
import org.thoughtcrime.securesms.ui.UINavigator
import org.thoughtcrime.securesms.ui.handleIntent
import org.thoughtcrime.securesms.ui.horizontalSlideComposable
@@ -155,7 +164,7 @@ fun ConversationSettingsNavHost(
){
SharedTransitionLayout {
val navController = rememberNavController()
- val navigator: UINavigator = remember { UINavigator() }
+ val navigator: UINavigator = retain { UINavigator() }
val handleBack: () -> Unit = {
if (navController.previousBackStackEntry != null) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsScreen.kt
index c2a6015c86..42a859d736 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/settings/ConversationSettingsScreen.kt
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -112,7 +113,7 @@ fun ConversationSettings(
}
)
},
- contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal),
+ contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal),
) { paddings ->
Column(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt
index a23216759c..4a7ab5199f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt
@@ -11,11 +11,15 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
@@ -41,11 +45,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.KeyboardType
@@ -98,11 +104,11 @@ fun DebugMenu(
val datePickerState = rememberDatePickerState()
val timePickerState = rememberTimePickerState()
- var showingDeprecatedDatePicker by remember { mutableStateOf(false) }
- var showingDeprecatedTimePicker by remember { mutableStateOf(false) }
+ var showingDeprecatedDatePicker by retain { mutableStateOf(false) }
+ var showingDeprecatedTimePicker by retain { mutableStateOf(false) }
- var showingDeprecatingStartDatePicker by remember { mutableStateOf(false) }
- var showingDeprecatingStartTimePicker by remember { mutableStateOf(false) }
+ var showingDeprecatingStartDatePicker by retain { mutableStateOf(false) }
+ var showingDeprecatingStartTimePicker by retain { mutableStateOf(false) }
val getPickedTime = {
val localDate = ZonedDateTime.ofInstant(
@@ -121,7 +127,8 @@ fun DebugMenu(
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
- }
+ },
+ contentWindowInsets = WindowInsets.safeDrawing,
) { contentPadding ->
// display a snackbar when required
LaunchedEffect(uiState.snackMessage) {
@@ -173,13 +180,21 @@ fun DebugMenu(
LoadingDialog(title = "Applying changes...")
}
+ val layoutDirection = LocalLayoutDirection.current
+ val safeInsetsPadding = PaddingValues(
+ start = contentPadding.calculateStartPadding(layoutDirection) + LocalDimensions.current.spacing,
+ end = contentPadding.calculateEndPadding(layoutDirection) + LocalDimensions.current.spacing,
+ top = contentPadding.calculateTopPadding(),
+ bottom = contentPadding.calculateBottomPadding(),
+ )
+
Column(
modifier = Modifier
.background(LocalColors.current.background)
- .padding(horizontal = LocalDimensions.current.spacing)
+ .padding(safeInsetsPadding)
.verticalScroll(rememberScrollState())
) {
- Spacer(modifier = Modifier.height(contentPadding.calculateTopPadding()))
+ Spacer(modifier = Modifier.height(LocalDimensions.current.spacing))
// Info pane
val clipboardManager = LocalClipboardManager.current
@@ -284,7 +299,8 @@ fun DebugMenu(
style = LocalType.current.base
)
DropDown(
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier
+ .fillMaxWidth()
.padding(top = LocalDimensions.current.xxsSpacing),
selectedText = uiState.selectedDebugSubscriptionStatus.label,
values = uiState.debugSubscriptionStatuses.map { it.label },
@@ -315,7 +331,8 @@ fun DebugMenu(
style = LocalType.current.base
)
DropDown(
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier
+ .fillMaxWidth()
.padding(top = LocalDimensions.current.xxsSpacing),
selectedText = uiState.selectedDebugProPlanStatus.label,
values = uiState.debugProPlanStatus.map { it.label },
diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuNavHost.kt
index cc4cece795..2de029463a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuNavHost.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuNavHost.kt
@@ -5,6 +5,7 @@ import android.os.Parcelable
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
@@ -40,7 +41,7 @@ fun DebugMenuNavHost(
onBack: () -> Unit
){
val navController = rememberNavController()
- val navigator: UINavigator = remember {
+ val navigator: UINavigator = retain {
UINavigator()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java
deleted file mode 100644
index 67c27f5032..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package org.thoughtcrime.securesms.giph.ui;
-
-import android.annotation.SuppressLint;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.viewpager2.adapter.FragmentStateAdapter;
-
-import com.google.android.material.tabs.TabLayoutMediator;
-
-import org.session.libsession.utilities.MediaTypes;
-import org.session.libsession.utilities.NonTranslatableStringConstants;
-import org.thoughtcrime.securesms.ScreenLockActionBarActivity;
-import org.session.libsignal.utilities.Log;
-import org.thoughtcrime.securesms.providers.BlobUtils;
-import org.session.libsession.utilities.ViewUtil;
-
-import java.io.IOException;
-import java.util.concurrent.ExecutionException;
-
-import network.loki.messenger.R;
-import network.loki.messenger.databinding.GiphyActivityBinding;
-
-public class GiphyActivity extends ScreenLockActionBarActivity
- implements GiphyActivityToolbar.OnLayoutChangedListener,
- GiphyActivityToolbar.OnFilterChangedListener,
- GiphyAdapter.OnItemClickListener
-{
-
- private static final String TAG = GiphyActivity.class.getSimpleName();
-
- public static final String EXTRA_IS_MMS = "extra_is_mms";
- public static final String EXTRA_WIDTH = "extra_width";
- public static final String EXTRA_HEIGHT = "extra_height";
-
- private GiphyGifFragment gifFragment;
- private GiphyStickerFragment stickerFragment;
- private boolean forMms;
-
- private GiphyActivityBinding binding;
-
- private GiphyAdapter.GiphyViewHolder finishingImage;
-
- @Override
- public void onCreate(Bundle bundle, boolean ready) {
- binding = GiphyActivityBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
-
- initializeToolbar();
- initializeResources();
- }
-
- private void initializeToolbar() {
- GiphyActivityToolbar toolbar = ViewUtil.findById(this, R.id.giphy_toolbar);
- toolbar.setOnFilterChangedListener(this);
- toolbar.setOnLayoutChangedListener(this);
- toolbar.setPersistence(GiphyActivityToolbarTextSecurePreferencesPersistence.fromContext(this));
-
- setSupportActionBar(toolbar);
-
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- getSupportActionBar().setHomeButtonEnabled(true);
- }
-
- private void initializeResources() {
- this.gifFragment = new GiphyGifFragment();
- this.stickerFragment = new GiphyStickerFragment();
- this.forMms = getIntent().getBooleanExtra(EXTRA_IS_MMS, false);
-
- binding.giphyPager.setAdapter(new GiphyFragmentPagerAdapter(this));
-
- new TabLayoutMediator(binding.tabLayout, binding.giphyPager, (tab, position) -> {
- tab.setText(position == 0 ? NonTranslatableStringConstants.GIF : getString(R.string.stickers));
- }).attach();
- }
-
- @Override
- public void onFilterChanged(String filter) {
- this.gifFragment.setSearchString(filter);
- this.stickerFragment.setSearchString(filter);
- }
-
- @Override
- public void onLayoutChanged(boolean gridLayout) {
- gifFragment.setLayoutManager(gridLayout);
- stickerFragment.setLayoutManager(gridLayout);
- }
-
- @SuppressLint("StaticFieldLeak")
- @Override
- public void onClick(final GiphyAdapter.GiphyViewHolder viewHolder) {
- if (finishingImage != null) finishingImage.gifProgress.setVisibility(View.GONE);
- finishingImage = viewHolder;
- finishingImage.gifProgress.setVisibility(View.VISIBLE);
-
- new AsyncTask() {
- @Override
- protected Uri doInBackground(Void... params) {
- try {
- byte[] data = viewHolder.getData(forMms);
-
- return BlobUtils.getInstance()
- .forData(data)
- .withMimeType(MediaTypes.IMAGE_GIF)
- .createForSingleSessionOnDisk(GiphyActivity.this, e -> Log.w(TAG, "Failed to write to disk.", e))
- .get();
- } catch (InterruptedException | ExecutionException | IOException e) {
- Log.w(TAG, e);
- return null;
- }
- }
-
- protected void onPostExecute(@Nullable Uri uri) {
- if (uri == null) {
- Toast.makeText(GiphyActivity.this, R.string.errorUnknown, Toast.LENGTH_LONG).show();
- } else if (viewHolder == finishingImage) {
- Intent intent = new Intent();
- intent.setData(uri);
- intent.putExtra(EXTRA_WIDTH, viewHolder.image.getGifWidth());
- intent.putExtra(EXTRA_HEIGHT, viewHolder.image.getGifHeight());
- setResult(RESULT_OK, intent);
- finish();
- } else {
- Log.w(TAG, "Resolved Uri is no longer the selected element...");
- }
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- private class GiphyFragmentPagerAdapter extends FragmentStateAdapter {
-
- private GiphyFragmentPagerAdapter(@NonNull FragmentActivity activity)
- {
- super(activity);
- }
-
- @NonNull
- @Override
- public Fragment createFragment(int position) {
- return position == 0 ? gifFragment : stickerFragment;
- }
-
- @Override
- public int getItemCount() {
- return 2;
- }
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.kt
new file mode 100644
index 0000000000..8e7e7b8ce3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.kt
@@ -0,0 +1,158 @@
+package org.thoughtcrime.securesms.giph.ui;
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.lifecycleScope
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import network.loki.messenger.R
+import network.loki.messenger.databinding.GiphyActivityBinding
+import org.session.libsession.utilities.MediaTypes
+import org.session.libsession.utilities.ViewUtil
+import org.session.libsignal.utilities.Log
+import org.thoughtcrime.securesms.ScreenLockActionBarActivity
+import org.thoughtcrime.securesms.giph.ui.compose.GiphyTabsCompose
+import org.thoughtcrime.securesms.providers.BlobUtils
+import org.thoughtcrime.securesms.ui.setThemedContent
+
+class GiphyActivity :
+ ScreenLockActionBarActivity(),
+ GiphyActivityToolbar.OnLayoutChangedListener,
+ GiphyActivityToolbar.OnFilterChangedListener,
+ GiphyAdapter.OnItemClickListener {
+
+ companion object {
+ private val TAG = GiphyActivity::class.java.simpleName
+
+ const val EXTRA_IS_MMS = "extra_is_mms"
+ const val EXTRA_WIDTH = "extra_width"
+ const val EXTRA_HEIGHT = "extra_height"
+ }
+
+ private lateinit var binding: GiphyActivityBinding
+
+ private lateinit var gifFragment: GiphyGifFragment
+ private lateinit var stickerFragment: GiphyStickerFragment
+ private var forMms: Boolean = false
+
+ private var finishingImage: GiphyAdapter.GiphyViewHolder? = null
+
+ private val titles = listOf(
+ R.string.gif,
+ R.string.stickers
+ )
+
+ override fun onCreate(bundle: Bundle?, ready: Boolean) {
+ binding = GiphyActivityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ initializeToolbar()
+ initializeResources()
+
+ val pager = binding.giphyPager
+
+ binding.composeTabs.apply {
+ setViewCompositionStrategy(
+ ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
+ )
+ setThemedContent {
+ GiphyTabsCompose(pager, titles)
+ }
+ }
+ }
+
+ private fun initializeToolbar() {
+ val toolbar: GiphyActivityToolbar = ViewUtil.findById(this, R.id.giphy_toolbar)
+ toolbar.setOnFilterChangedListener(this)
+ toolbar.setOnLayoutChangedListener(this)
+ toolbar.setPersistence(GiphyActivityToolbarTextSecurePreferencesPersistence.fromContext(this))
+
+ setSupportActionBar(toolbar)
+ supportActionBar?.apply {
+ setDisplayHomeAsUpEnabled(true)
+ setHomeButtonEnabled(true)
+ }
+ }
+
+ private fun initializeResources() {
+ gifFragment = GiphyGifFragment()
+ stickerFragment = GiphyStickerFragment()
+ forMms = intent.getBooleanExtra(EXTRA_IS_MMS, false)
+
+ binding.giphyPager.adapter = GiphyFragmentPagerAdapter(this)
+ }
+
+ // Toolbar -> fragments
+ override fun onFilterChanged(filter: String) {
+ gifFragment.setNewSearchString(filter)
+ stickerFragment.setNewSearchString(filter)
+ }
+
+ override fun onLayoutChanged(gridLayout: Boolean) {
+ gifFragment.setLayoutManager(gridLayout)
+ stickerFragment.setLayoutManager(gridLayout)
+ }
+
+ override fun onClick(viewHolder: GiphyAdapter.GiphyViewHolder) {
+ finishingImage?.gifProgress?.visibility = View.GONE
+ finishingImage = viewHolder
+ finishingImage?.gifProgress?.visibility = View.VISIBLE
+
+ lifecycleScope.launch {
+ val uri: Uri? = withContext(Dispatchers.IO) {
+ try {
+ val data = viewHolder.getData(forMms)
+ BlobUtils.getInstance()
+ .forData(data)
+ .withMimeType(MediaTypes.IMAGE_GIF)
+ .createForSingleSessionOnDisk(
+ this@GiphyActivity
+ ) { e -> Log.w(TAG, "Failed to write to disk.", e) }
+ .get()
+ } catch (t: Throwable) {
+ Log.w(TAG, t)
+ null
+ }
+ }
+
+ if (isFinishing || isDestroyed) return@launch
+ finishingImage?.gifProgress?.visibility = View.GONE
+
+ if (uri == null) {
+ Toast.makeText(this@GiphyActivity, R.string.errorUnknown, Toast.LENGTH_LONG).show()
+ return@launch
+ }
+
+ // Only finish if the same cell is still the "finishing" one
+ if (viewHolder === finishingImage) {
+ val result = Intent().apply {
+ data = uri
+ putExtra(EXTRA_WIDTH, viewHolder.image.gifWidth)
+ putExtra(EXTRA_HEIGHT, viewHolder.image.gifHeight)
+ }
+ setResult(RESULT_OK, result)
+ finish()
+ } else {
+ Log.w(TAG, "Resolved Uri is no longer the selected element...")
+ }
+ }
+ }
+
+ private inner class GiphyFragmentPagerAdapter(activity: FragmentActivity) :
+ FragmentStateAdapter(activity) {
+
+ override fun getItemCount(): Int = 2
+
+ override fun createFragment(position: Int): Fragment {
+ return if (position == 0) gifFragment else stickerFragment
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java
index b884ef6a19..a686d34bf6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java
@@ -38,7 +38,7 @@
import network.loki.messenger.R;
-class GiphyAdapter extends RecyclerView.Adapter {
+public class GiphyAdapter extends RecyclerView.Adapter {
private static final String TAG = GiphyAdapter.class.getSimpleName();
@@ -48,7 +48,7 @@ class GiphyAdapter extends RecyclerView.Adapter {
private List images;
private OnItemClickListener listener;
- class GiphyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, RequestListener {
+ public class GiphyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, RequestListener {
public AspectRatioImageView thumbnail;
public GiphyImage image;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java
deleted file mode 100644
index 753e43b833..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java
+++ /dev/null
@@ -1,148 +0,0 @@
-package org.thoughtcrime.securesms.giph.ui;
-
-import android.os.AsyncTask;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.Loader;
-import androidx.recyclerview.widget.DefaultItemAnimator;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.StaggeredGridLayoutManager;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import org.thoughtcrime.securesms.giph.model.GiphyImage;
-import org.thoughtcrime.securesms.giph.net.GiphyLoader;
-import org.thoughtcrime.securesms.giph.util.InfiniteScrollListener;
-import com.bumptech.glide.Glide;
-import org.session.libsession.utilities.TextSecurePreferences;
-import org.session.libsession.utilities.ViewUtil;
-
-import java.util.LinkedList;
-import java.util.List;
-
-import network.loki.messenger.R;
-
-public abstract class GiphyFragment extends Fragment implements LoaderManager.LoaderCallbacks>, GiphyAdapter.OnItemClickListener {
-
- private static final String TAG = GiphyFragment.class.getSimpleName();
-
- private GiphyAdapter giphyAdapter;
- private RecyclerView recyclerView;
- private View loadingProgress;
- private TextView noResultsView;
-
- protected String searchString;
- private Boolean pendingGridLayout = null;
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
- ViewGroup container = ViewUtil.inflate(inflater, viewGroup, R.layout.giphy_fragment);
- this.recyclerView = ViewUtil.findById(container, R.id.giphy_list);
- this.loadingProgress = ViewUtil.findById(container, R.id.loading_progress);
- this.noResultsView = ViewUtil.findById(container, R.id.no_results);
-
- // Now that views are ready, apply the searchString if it's set
- applySearchStringToUI();
-
- // Apply pending layout if it was set before view was ready
- if (pendingGridLayout != null) {
- setLayoutManager(pendingGridLayout);
- pendingGridLayout = null;
- } else {
- // Or set default
- setLayoutManager(TextSecurePreferences.isGifSearchInGridLayout(getContext()));
- }
-
- return container;
- }
-
- @Override
- public void onActivityCreated(Bundle bundle) {
- super.onActivityCreated(bundle);
-
- this.giphyAdapter = new GiphyAdapter(getActivity(), Glide.with(this), new LinkedList<>());
- this.giphyAdapter.setListener(this);
-
- this.recyclerView.setItemAnimator(new DefaultItemAnimator());
- this.recyclerView.setAdapter(giphyAdapter);
- this.recyclerView.addOnScrollListener(new GiphyScrollListener());
-
- getLoaderManager().initLoader(0, null, this);
- }
-
- @Override
- public void onLoadFinished(@NonNull Loader> loader, @NonNull List data) {
- this.loadingProgress.setVisibility(View.GONE);
-
- if (data.isEmpty()) noResultsView.setVisibility(View.VISIBLE);
- else noResultsView.setVisibility(View.GONE);
-
- this.giphyAdapter.setImages(data);
- }
-
- @Override
- public void onLoaderReset(@NonNull Loader> loader) {
- noResultsView.setVisibility(View.GONE);
- this.giphyAdapter.setImages(new LinkedList());
- }
-
- public void setLayoutManager(boolean gridLayout) {
- if (recyclerView != null) {
- recyclerView.setLayoutManager(getLayoutManager(gridLayout));
- } else {
- pendingGridLayout = gridLayout;
- }
- }
-
- private RecyclerView.LayoutManager getLayoutManager(boolean gridLayout) {
- return gridLayout ? new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
- : new LinearLayoutManager(getActivity());
- }
-
-
- public void setSearchString(@Nullable String searchString) {
- this.searchString = searchString;
- if (this.noResultsView != null) {
- applySearchStringToUI();
- }
- }
-
- private void applySearchStringToUI() {
- if (this.noResultsView != null) {
- this.noResultsView.setVisibility(View.GONE);
- this.getLoaderManager().restartLoader(0, null, this);
- }
- }
-
- @Override
- public void onClick(GiphyAdapter.GiphyViewHolder viewHolder) {
- if (getActivity() instanceof GiphyAdapter.OnItemClickListener) {
- ((GiphyAdapter.OnItemClickListener) getActivity()).onClick(viewHolder);
- }
- }
-
- private class GiphyScrollListener extends InfiniteScrollListener {
- @Override
- public void onLoadMore(final int currentPage) {
- final Loader> loader = getLoaderManager().getLoader(0);
- if (loader == null) return;
-
- new AsyncTask>() {
- @Override
- protected List doInBackground(Void... params) {
- return ((GiphyLoader)loader).loadPage(currentPage * GiphyLoader.PAGE_SIZE);
- }
-
- protected void onPostExecute(List images) {
- giphyAdapter.addImages(images);
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt
new file mode 100644
index 0000000000..03789b89e7
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt
@@ -0,0 +1,163 @@
+package org.thoughtcrime.securesms.giph.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
+import androidx.loader.app.LoaderManager
+import androidx.loader.content.Loader
+import androidx.recyclerview.widget.DefaultItemAnimator
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.StaggeredGridLayoutManager
+import com.bumptech.glide.Glide
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import network.loki.messenger.R
+import org.session.libsession.utilities.TextSecurePreferences
+import org.session.libsession.utilities.ViewUtil
+import org.thoughtcrime.securesms.giph.model.GiphyImage
+import org.thoughtcrime.securesms.giph.net.GiphyLoader
+import org.thoughtcrime.securesms.giph.ui.compose.GiphyOverlayState
+import org.thoughtcrime.securesms.giph.ui.compose.bindGiphyOverlay
+import org.thoughtcrime.securesms.giph.util.InfiniteScrollListener
+import java.util.LinkedList
+import java.util.List
+
+/**
+ * Base fragment for both GIF and Sticker tabs.
+ * Subclasses (Gif/Sticker) only implement onCreateLoader(...).
+ */
+abstract class GiphyFragment :
+ Fragment(),
+ LoaderManager.LoaderCallbacks> {
+
+ private lateinit var giphyAdapter: GiphyAdapter
+ private lateinit var recyclerView: RecyclerView
+ private lateinit var overlayView: ComposeView
+ private val overlayState: MutableStateFlow = MutableStateFlow(
+ GiphyOverlayState.Hidden
+ )
+
+ // Set by toolbar filter via Activity
+ var searchString: String? = null
+
+ // If setLayoutManager is called before views are ready
+ private var pendingGridLayout: Boolean? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?, // nullable per Fragment API
+ savedInstanceState: Bundle?
+ ): View {
+ val root = inflater.inflate(R.layout.giphy_fragment, container, false) as ViewGroup
+
+ // Todo: Make compose
+ recyclerView = root.findViewById(R.id.giphy_list)
+
+ overlayView = ViewUtil.findById(root, R.id.giphy_state_overlay)
+ bindGiphyOverlay(overlayView, overlayState)
+
+ applySearchStringToUI()
+
+ pendingGridLayout?.let {
+ setLayoutManager(it)
+ pendingGridLayout = null
+ } ?: run {
+ setLayoutManager(TextSecurePreferences.isGifSearchInGridLayout(requireContext()))
+ }
+
+ overlayState.value = GiphyOverlayState.Loading
+
+ return root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ giphyAdapter = GiphyAdapter(requireActivity(), Glide.with(this), LinkedList())
+ giphyAdapter.setListener { viewHolder ->
+ (activity as? GiphyAdapter.OnItemClickListener)?.onClick(
+ viewHolder
+ )
+ }
+
+ recyclerView.itemAnimator = DefaultItemAnimator()
+ recyclerView.adapter = giphyAdapter
+ recyclerView.addOnScrollListener(GiphyScrollListener())
+
+ // Initialize the loader (id = 0)
+ LoaderManager.getInstance(this).initLoader(0, null, this)
+ }
+
+ // Loader callbacks (subclasses provide the loader)
+ abstract override fun onCreateLoader(
+ id: Int,
+ args: Bundle?
+ ): Loader>
+
+ override fun onLoadFinished(
+ loader: Loader>,
+ data: List
+ ) {
+ overlayState.value = if (data.isEmpty()) GiphyOverlayState.Empty() else GiphyOverlayState.Hidden
+ giphyAdapter.setImages(data.toMutableList())
+ }
+
+ override fun onLoaderReset(loader: Loader>) {
+ overlayState.value = GiphyOverlayState.Hidden
+ giphyAdapter.setImages(mutableListOf())
+ }
+
+ fun setLayoutManager(gridLayout: Boolean) {
+ if (this::recyclerView.isInitialized) {
+ recyclerView.layoutManager = createLayoutManager(gridLayout)
+ } else {
+ pendingGridLayout = gridLayout
+ }
+ }
+
+ fun setNewSearchString(newSearch: String?) {
+ searchString = newSearch
+ if (isAdded) applySearchStringToUI()
+ }
+
+ private fun createLayoutManager(gridLayout: Boolean): RecyclerView.LayoutManager {
+ return if (gridLayout) {
+ StaggeredGridLayoutManager(
+ 2,
+ StaggeredGridLayoutManager.VERTICAL
+ ).apply {
+ gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
+ }
+ } else {
+ LinearLayoutManager(requireActivity())
+ }
+ }
+
+ private fun applySearchStringToUI() {
+ overlayState.value = GiphyOverlayState.Loading
+ LoaderManager.getInstance(this).restartLoader(0, null, this)
+ }
+
+ // Infinite scroll
+ private inner class GiphyScrollListener : InfiniteScrollListener() {
+ override fun onLoadMore(currentPage: Int) {
+ @Suppress("UNCHECKED_CAST")
+ val loader = LoaderManager.getInstance(this@GiphyFragment)
+ .getLoader>(0) as? GiphyLoader ?: return
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ val images = withContext(Dispatchers.IO) {
+ loader.loadPage(currentPage * GiphyLoader.PAGE_SIZE)
+ }
+ giphyAdapter.addImages(images.toMutableList())
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java
deleted file mode 100644
index 4c8ad66dcf..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.thoughtcrime.securesms.giph.ui;
-
-
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.loader.content.Loader;
-
-import org.thoughtcrime.securesms.giph.model.GiphyImage;
-import org.thoughtcrime.securesms.giph.net.GiphyGifLoader;
-
-import java.util.List;
-
-public class GiphyGifFragment extends GiphyFragment {
-
- @Override
- public @NonNull Loader> onCreateLoader(int id, Bundle args) {
- return new GiphyGifLoader(getActivity(), searchString);
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.kt
new file mode 100644
index 0000000000..537f8b5edf
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.kt
@@ -0,0 +1,21 @@
+package org.thoughtcrime.securesms.giph.ui;
+
+
+import android.os.Bundle;
+import androidx.loader.content.Loader;
+
+import org.thoughtcrime.securesms.giph.model.GiphyImage;
+import org.thoughtcrime.securesms.giph.net.GiphyGifLoader;
+
+import java.util.List;
+
+@Suppress("UNCHECKED_CAST")
+class GiphyGifFragment : GiphyFragment() {
+ override fun onCreateLoader(
+ id: Int,
+ args: Bundle?
+ ): Loader> {
+ return GiphyGifLoader(requireActivity(), searchString)
+ as Loader>
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.java
deleted file mode 100644
index 0b838b4718..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.thoughtcrime.securesms.giph.ui;
-
-
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.loader.content.Loader;
-
-import org.thoughtcrime.securesms.giph.model.GiphyImage;
-import org.thoughtcrime.securesms.giph.net.GiphyStickerLoader;
-
-import java.util.List;
-
-public class GiphyStickerFragment extends GiphyFragment {
- @Override
- public @NonNull Loader> onCreateLoader(int id, Bundle args) {
- return new GiphyStickerLoader(getActivity(), searchString);
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.kt
new file mode 100644
index 0000000000..ec66dcc160
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.kt
@@ -0,0 +1,22 @@
+package org.thoughtcrime.securesms.giph.ui;
+
+
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.loader.content.Loader;
+
+import org.thoughtcrime.securesms.giph.model.GiphyImage;
+import org.thoughtcrime.securesms.giph.net.GiphyStickerLoader;
+
+import java.util.List;
+
+@Suppress("UNCHECKED_CAST")
+class GiphyStickerFragment : GiphyFragment() {
+ override fun onCreateLoader(
+ id: Int,
+ args: Bundle?
+ ): Loader> {
+ return GiphyStickerLoader(requireActivity(), searchString)
+ as Loader>
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/compose/GiphyFragmentCompose.kt b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/compose/GiphyFragmentCompose.kt
new file mode 100644
index 0000000000..bc07d85e92
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/compose/GiphyFragmentCompose.kt
@@ -0,0 +1,68 @@
+package org.thoughtcrime.securesms.giph.ui.compose
+
+import androidx.annotation.StringRes
+import androidx.compose.animation.Crossfade
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.StateFlow
+import network.loki.messenger.R
+import org.thoughtcrime.securesms.ui.theme.LocalDimensions
+import org.thoughtcrime.securesms.ui.theme.LocalType
+
+sealed class GiphyOverlayState {
+ data object Hidden : GiphyOverlayState()
+ data object Loading : GiphyOverlayState()
+ data class Empty(val messageId: Int = R.string.searchMatchesNone) : GiphyOverlayState()
+}
+
+fun bindGiphyOverlay(composeView: ComposeView, stateFlow: StateFlow) {
+ composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ composeView.setContent {
+ val state by stateFlow.collectAsState()
+ GiphyOverlay(state)
+ }
+}
+
+@Composable
+private fun GiphyOverlay(state: GiphyOverlayState) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Crossfade(targetState = state, label = "giphyOverlay") { s ->
+ when (s) {
+ is GiphyOverlayState.Hidden -> {}
+ is GiphyOverlayState.Loading -> {
+ Box(
+ modifier = Modifier.fillMaxWidth().padding(vertical = LocalDimensions.current.spacing),
+ contentAlignment = Alignment.TopCenter
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+ is GiphyOverlayState.Empty -> {
+ Box(
+ modifier = Modifier.fillMaxWidth().padding(LocalDimensions.current.spacing),
+ contentAlignment = Alignment.TopCenter
+ ) {
+ Text(
+ text = stringResource(s.messageId),
+ style = LocalType.current.large
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/compose/GiphyTabsCompose.kt b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/compose/GiphyTabsCompose.kt
new file mode 100644
index 0000000000..8cc874db07
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/compose/GiphyTabsCompose.kt
@@ -0,0 +1,41 @@
+@file:JvmName("GiphyTabsCompose") // lets Java call attachComposeTabs(...)
+package org.thoughtcrime.securesms.giph.ui.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.viewpager2.widget.ViewPager2
+import org.thoughtcrime.securesms.ui.components.SessionTabRow
+
+@Composable
+fun GiphyTabsCompose(
+ pager: ViewPager2,
+ titles: List
+) {
+ var selectedIndex by rememberSaveable {
+ mutableIntStateOf(pager.currentItem.coerceIn(0, titles.lastIndex))
+ }
+
+ // Keep pager -> tabs selection in sync.
+ DisposableEffect(pager) {
+ val callback = object : ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ selectedIndex = position
+ }
+ }
+ pager.registerOnPageChangeCallback(callback)
+ onDispose { pager.unregisterOnPageChangeCallback(callback) }
+ }
+
+ // Tabs -> ViewPager2
+ SessionTabRow(
+ selectedIndex = selectedIndex,
+ titles = titles,
+ onTabSelected = { index ->
+ if (index != pager.currentItem) pager.setCurrentItem(index, true)
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt
index 9d243692d5..7a82213789 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt
@@ -3,22 +3,37 @@ package org.thoughtcrime.securesms.groups.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -29,11 +44,16 @@ import org.thoughtcrime.securesms.groups.ContactItem
import org.thoughtcrime.securesms.groups.GroupMemberState
import org.thoughtcrime.securesms.groups.InviteMembersViewModel
import org.thoughtcrime.securesms.ui.AlertDialog
+import org.thoughtcrime.securesms.ui.CollapsibleFooterAction
+import org.thoughtcrime.securesms.ui.CollapsibleFooterActionData
import org.thoughtcrime.securesms.ui.DialogButtonData
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.ProBadgeText
import org.thoughtcrime.securesms.ui.RadioOption
+import org.thoughtcrime.securesms.ui.SearchBarWithClose
+import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo
import org.thoughtcrime.securesms.ui.components.Avatar
+import org.thoughtcrime.securesms.ui.components.BackAppBar
import org.thoughtcrime.securesms.ui.components.DialogTitledRadioButton
import org.thoughtcrime.securesms.ui.components.RadioButtonIndicator
import org.thoughtcrime.securesms.ui.components.annotatedStringResource
@@ -99,7 +119,9 @@ fun MemberItem(
Avatar(
size = LocalDimensions.current.iconLarge,
data = avatarUIData,
- badge = if (showAsAdmin) { AvatarBadge.ResourceBadge.Admin } else AvatarBadge.None
+ badge = if (showAsAdmin) {
+ AvatarBadge.ResourceBadge.Admin
+ } else AvatarBadge.None
)
Column(
@@ -190,7 +212,7 @@ fun InviteMembersDialog(
onInviteClicked: (Boolean) -> Unit,
onDismiss: () -> Unit,
) {
- var shareHistory by remember { mutableStateOf(false) }
+ var shareHistory by retain { mutableStateOf(false) }
AlertDialog(
modifier = modifier,
@@ -268,6 +290,99 @@ fun ManageMemberItem(
)
}
+@Composable
+fun MembersSearchHeader(
+ searchFocused: Boolean,
+ searchQuery: String,
+ onQueryChange: (String) -> Unit,
+ onClear: () -> Unit,
+ onFocusChanged: (Boolean) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ placeholder: String = LocalResources.current.getString(R.string.search)
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ // important for stickyHeader so rows don't show through
+ .background(LocalColors.current.background)
+ .padding(vertical = LocalDimensions.current.smallSpacing)
+ ) {
+ SearchBarWithClose(
+ query = searchQuery,
+ onValueChanged = onQueryChange,
+ onClear = onClear,
+ placeholder = if (searchFocused) "" else placeholder,
+ enabled = enabled,
+ isFocused = searchFocused,
+ modifier = Modifier.padding(horizontal = LocalDimensions.current.smallSpacing),
+ onFocusChanged = onFocusChanged
+ )
+ }
+}
+
+@Composable
+fun CollapsibleFooterBottomBar(
+ footer: CollapsibleFooterActionData,
+ onToggle: () -> Unit,
+ onClose: () -> Unit,
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom))
+ .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))
+ .imePadding()
+ ) {
+ CollapsibleFooterAction(
+ data = footer,
+ onCollapsedClicked = onToggle,
+ onClosedClicked = onClose
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun BaseManageGroupScreen(
+ title: String,
+ onBack: () -> Unit,
+ enableCollapsingTopBarInLandscape: Boolean,
+ collapseTopBar : Boolean = false,
+ bottomBar: @Composable () -> Unit,
+ content: @Composable (paddingValues: PaddingValues) -> Unit,
+) {
+ val isLandscape = getAdaptiveInfo().isLandscape
+ val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
+
+ val scaffoldModifier =
+ if (enableCollapsingTopBarInLandscape && isLandscape)
+ Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
+ else
+ Modifier
+
+ LaunchedEffect(isLandscape, collapseTopBar) {
+ if (isLandscape && collapseTopBar) {
+ scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit
+ }
+ }
+
+ Scaffold(
+ modifier = scaffoldModifier,
+ topBar = {
+ BackAppBar(
+ title = title,
+ onBack = onBack,
+ scrollBehavior = if (enableCollapsingTopBarInLandscape && isLandscape) scrollBehavior else null
+ )
+ },
+ bottomBar = bottomBar,
+ contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal),
+ ) { paddingValues ->
+ content(paddingValues)
+ }
+}
+
@Preview
@Composable
fun PreviewMemberList() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteAccountIdScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteAccountIdScreen.kt
index e5b937f475..56610cbbe5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteAccountIdScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteAccountIdScreen.kt
@@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.emptyFlow
import org.thoughtcrime.securesms.groups.InviteMembersViewModel
import org.thoughtcrime.securesms.home.startconversation.newmessage.Callbacks
import org.thoughtcrime.securesms.home.startconversation.newmessage.NewMessage
-import org.thoughtcrime.securesms.home.startconversation.newmessage.NewMessageViewModel
import org.thoughtcrime.securesms.home.startconversation.newmessage.State
import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt
index a857eb4065..e6863901c2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteContactsScreen.kt
@@ -4,21 +4,12 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawing
-import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -39,13 +30,10 @@ import org.thoughtcrime.securesms.groups.InviteMembersViewModel.Commands.SearchF
import org.thoughtcrime.securesms.groups.InviteMembersViewModel.Commands.SearchQueryChange
import org.thoughtcrime.securesms.groups.InviteMembersViewModel.Commands.ShowSendInviteDialog
import org.thoughtcrime.securesms.groups.InviteMembersViewModel.Commands.ToggleFooter
-import org.thoughtcrime.securesms.ui.CollapsibleFooterAction
import org.thoughtcrime.securesms.ui.CollapsibleFooterActionData
import org.thoughtcrime.securesms.ui.CollapsibleFooterItemData
import org.thoughtcrime.securesms.ui.GetString
-import org.thoughtcrime.securesms.ui.SearchBarWithClose
-import org.thoughtcrime.securesms.ui.components.BackAppBar
-import org.thoughtcrime.securesms.ui.qaTag
+import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType
@@ -87,6 +75,9 @@ fun InviteContacts(
forCommunity: Boolean = false
) {
+ val searchFocused = uiState.isSearchFocused
+ val isLandscape = getAdaptiveInfo().isLandscape
+
val trayItems = listOf(
CollapsibleFooterItemData(
label = GetString(LocalResources.current.getString(R.string.membersInvite)),
@@ -101,67 +92,57 @@ fun InviteContacts(
val handleBack: () -> Unit = {
when {
- uiState.isSearchFocused -> sendCommand(RemoveSearchState(false))
+ searchFocused -> sendCommand(RemoveSearchState(false))
else -> onBack()
}
}
+ val header: @Composable (Modifier) -> Unit = { modifier ->
+ MembersSearchHeader(
+ searchFocused = searchFocused,
+ searchQuery = searchQuery,
+ onQueryChange = { sendCommand(SearchQueryChange(it)) },
+ onClear = { sendCommand(SearchQueryChange("")) },
+ onFocusChanged = { sendCommand(SearchFocusChange(it)) },
+ modifier = modifier
+ )
+ }
+
+
// Intercept system back
BackHandler(enabled = true) { handleBack() }
- Scaffold(
- contentWindowInsets = WindowInsets.safeDrawing,
- topBar = {
- BackAppBar(
- title = stringResource(id = R.string.membersInvite),
- onBack = handleBack,
- )
- },
+ BaseManageGroupScreen(
+ title = stringResource(id = R.string.membersInvite),
+ onBack = handleBack,
+ enableCollapsingTopBarInLandscape = true,
+ collapseTopBar = searchFocused,
bottomBar = {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom))
- .imePadding()
- ) {
- CollapsibleFooterAction(
- data = CollapsibleFooterActionData(
- title = uiState.footer.footerActionTitle,
- collapsed = uiState.footer.collapsed,
- visible = uiState.footer.visible,
- items = trayItems
- ),
- onCollapsedClicked = { sendCommand(ToggleFooter) },
- onClosedClicked = { sendCommand(CloseFooter) }
- )
- }
+ CollapsibleFooterBottomBar(
+ footer = CollapsibleFooterActionData(
+ title = uiState.footer.footerActionTitle,
+ collapsed = uiState.footer.collapsed,
+ visible = uiState.footer.visible,
+ items = trayItems
+ ),
+ onToggle = { sendCommand(ToggleFooter) },
+ onClose = { sendCommand(CloseFooter) }
+ )
}
- ) { paddings ->
+ ) { paddingValues ->
+
Column(
modifier = Modifier
- .padding(paddings)
- .consumeWindowInsets(paddings),
+ .padding(paddingValues)
+ .consumeWindowInsets(paddingValues),
) {
- Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
- SearchBarWithClose(
- query = searchQuery,
- onValueChanged = { query -> sendCommand(SearchQueryChange(query)) },
- onClear = { sendCommand(SearchQueryChange("")) },
- placeholder = stringResource(R.string.searchContacts),
- modifier = Modifier
- .padding(horizontal = LocalDimensions.current.smallSpacing)
- .qaTag(R.string.AccessibilityId_groupNameSearch),
- backgroundColor = LocalColors.current.backgroundSecondary,
- isFocused = uiState.isSearchFocused,
- onFocusChanged = { isFocused -> sendCommand(SearchFocusChange(isFocused)) },
- enabled = true,
- )
+ if (!isLandscape) {
+ header(Modifier)
+ }
val scrollState = rememberLazyListState()
- Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
-
Box(
modifier = Modifier
.weight(1f)
@@ -180,6 +161,10 @@ fun InviteContacts(
state = scrollState,
contentPadding = PaddingValues(bottom = LocalDimensions.current.spacing),
) {
+ if (isLandscape) {
+ stickyHeader { header(Modifier) }
+ }
+
multiSelectMemberList(
contacts = contacts,
onContactItemClicked = { address -> sendCommand(ContactItemClick(address)) },
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupAdminsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupAdminsScreen.kt
index d1e0f2953d..992fde9f85 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupAdminsScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupAdminsScreen.kt
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.groups.compose
-import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
@@ -8,34 +7,21 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawing
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.layout.windowInsetsBottomHeight
-import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -44,18 +30,21 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import network.loki.messenger.R
import org.thoughtcrime.securesms.groups.GroupMemberState
import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel
-import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel.Commands.*
-import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel
+import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel.Commands.CloseFooter
+import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel.Commands.MemberClick
+import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel.Commands.RemoveSearchState
+import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel.Commands.SearchFocusChange
+import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel.Commands.SearchQueryChange
+import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel.Commands.SelfClick
+import org.thoughtcrime.securesms.groups.ManageGroupAdminsViewModel.Commands.ToggleFooter
import org.thoughtcrime.securesms.ui.Cell
-import org.thoughtcrime.securesms.ui.CollapsibleFooterAction
import org.thoughtcrime.securesms.ui.CollapsibleFooterActionData
import org.thoughtcrime.securesms.ui.CollapsibleFooterItemData
import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.ItemButton
import org.thoughtcrime.securesms.ui.LoadingDialog
-import org.thoughtcrime.securesms.ui.SearchBarWithClose
-import org.thoughtcrime.securesms.ui.components.BackAppBar
+import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo
import org.thoughtcrime.securesms.ui.components.annotatedStringResource
import org.thoughtcrime.securesms.ui.getCellBottomShape
import org.thoughtcrime.securesms.ui.getCellTopShape
@@ -94,6 +83,7 @@ fun ManageAdmins(
) {
val searchFocused = uiState.isSearchFocused
+ val isLandscape = getAdaptiveInfo().isLandscape
val handleBack: () -> Unit = {
when {
@@ -102,37 +92,52 @@ fun ManageAdmins(
}
}
+ val searchLabel: @Composable (Modifier) -> Unit = { modifier ->
+ if (!searchFocused) {
+ Text(
+ modifier = Modifier.padding(
+ start = LocalDimensions.current.mediumSpacing
+ ),
+ text = LocalResources.current.getString(R.string.admins),
+ style = LocalType.current.base,
+ color = LocalColors.current.textSecondary
+ )
+ }
+ }
+
+ val searchHeader: @Composable (Modifier) -> Unit = { modifier ->
+ MembersSearchHeader(
+ searchFocused = searchFocused,
+ searchQuery = searchQuery,
+ onQueryChange = { sendCommand(SearchQueryChange(it)) },
+ onClear = { sendCommand(SearchQueryChange("")) },
+ onFocusChanged = { sendCommand(SearchFocusChange(it)) },
+ modifier = modifier
+ )
+ }
+
// Intercept system back
BackHandler(enabled = true) { handleBack() }
- Scaffold(
- topBar = {
- BackAppBar(
- title = stringResource(id = R.string.manageAdmins),
- onBack = handleBack,
- )
- },
+ BaseManageGroupScreen(
+ title = stringResource(id = R.string.manageAdmins),
+ onBack = handleBack,
+ enableCollapsingTopBarInLandscape = true,
+ collapseTopBar = searchFocused,
bottomBar = {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom))
- .imePadding()
- ) {
- CollapsibleFooterAction(
- data = CollapsibleFooterActionData(
- title = uiState.footer.footerActionTitle,
- collapsed = uiState.footer.collapsed,
- visible = uiState.footer.visible,
- items = uiState.footer.footerActionItems
- ),
- onCollapsedClicked = { sendCommand(ToggleFooter) },
- onClosedClicked = { sendCommand(CloseFooter) }
- )
- }
- },
- contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal),
+ CollapsibleFooterBottomBar(
+ footer = CollapsibleFooterActionData(
+ title = uiState.footer.footerActionTitle,
+ collapsed = uiState.footer.collapsed,
+ visible = uiState.footer.visible,
+ items = uiState.footer.footerActionItems
+ ),
+ onToggle = { sendCommand(ToggleFooter) },
+ onClose = { sendCommand(CloseFooter) }
+ )
+ }
) { paddingValues ->
+
Column(
modifier = Modifier
.padding(paddingValues)
@@ -149,82 +154,31 @@ fun ManageAdmins(
color = LocalColors.current.textSecondary
)
- AnimatedVisibility(
- // show only when add-members is enabled AND search is not focused
- visible = !searchFocused,
- enter = fadeIn(animationSpec = tween(150)) +
- expandVertically(
- animationSpec = tween(200),
- expandFrom = Alignment.Top
- ),
- exit = fadeOut(animationSpec = tween(150)) +
- shrinkVertically(
- animationSpec = tween(180),
- shrinkTowards = Alignment.Top
- )
- ) {
- Column {
- Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
-
- Cell(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = LocalDimensions.current.smallSpacing),
- ) {
- Column {
- uiState.options.forEachIndexed { index, option ->
- ItemButton(
- modifier = Modifier.qaTag(option.qaTag),
- text = annotatedStringResource(option.name),
- iconRes = option.icon,
- shape = when (index) {
- 0 -> getCellTopShape()
- uiState.options.lastIndex -> getCellBottomShape()
- else -> RectangleShape
- },
- onClick = option.onClick,
- )
-
- if (index != uiState.options.lastIndex) Divider()
- }
- }
- }
- }
- }
-
- Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
-
- if (!searchFocused) {
- Text(
- modifier = Modifier.padding(
- start = LocalDimensions.current.mediumSpacing,
- bottom = LocalDimensions.current.smallSpacing
- ),
- text = LocalResources.current.getString(R.string.admins),
- style = LocalType.current.base,
- color = LocalColors.current.textSecondary
+ if (!isLandscape) {
+ OptionsBlock(
+ show = !searchFocused,
+ options = uiState.options
)
+ searchLabel(Modifier)
+ searchHeader(Modifier)
}
- SearchBarWithClose(
- query = searchQuery,
- onValueChanged = { query -> sendCommand(SearchQueryChange(query)) },
- onClear = { sendCommand(SearchQueryChange("")) },
- placeholder = if (searchFocused) "" else LocalResources.current.getString(R.string.search),
- enabled = true,
- isFocused = searchFocused,
- modifier = Modifier.padding(horizontal = LocalDimensions.current.smallSpacing),
- onFocusChanged = { isFocused -> sendCommand(SearchFocusChange(isFocused)) }
- )
-
- Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
-
// List of members
LazyColumn(
modifier = Modifier
.weight(1f)
.imePadding()
) {
+ if (isLandscape) {
+ item {
+ OptionsBlock(
+ show = !searchFocused,
+ options = uiState.options
+ )
+ }
+ item { searchLabel(Modifier) }
+ stickyHeader { searchHeader(Modifier) }
+ }
items(admins) { member ->
// Each member's view
ManageMemberItem(
@@ -237,12 +191,6 @@ fun ManageAdmins(
selected = member in selectedMembers
)
}
-
- item {
- Spacer(
- modifier = Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)
- )
- }
}
}
}
@@ -252,6 +200,52 @@ fun ManageAdmins(
}
}
+@Composable
+private fun OptionsBlock(
+ show: Boolean,
+ options: List,
+) {
+ AnimatedVisibility(
+ visible = show,
+ enter = fadeIn(animationSpec = tween(150)) +
+ expandVertically(
+ animationSpec = tween(200),
+ expandFrom = Alignment.Top
+ ),
+ exit = fadeOut(animationSpec = tween(150)) +
+ shrinkVertically(
+ animationSpec = tween(180),
+ shrinkTowards = Alignment.Top
+ )
+ ) {
+ Column {
+ Cell(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(LocalDimensions.current.smallSpacing),
+ ) {
+ Column {
+ options.forEachIndexed { index, option ->
+ ItemButton(
+ modifier = Modifier.qaTag(option.qaTag),
+ text = annotatedStringResource(option.name),
+ iconRes = option.icon,
+ shape = when (index) {
+ 0 -> getCellTopShape()
+ options.lastIndex -> getCellBottomShape()
+ else -> RectangleShape
+ },
+ onClick = option.onClick,
+ )
+
+ if (index != options.lastIndex) Divider()
+ }
+ }
+ }
+ }
+ }
+
+}
@Preview
@Composable
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupMembersScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupMembersScreen.kt
index 388dc58ffb..09efb0117a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupMembersScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupMembersScreen.kt
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.groups.compose
-import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
@@ -8,38 +7,25 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawing
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.layout.windowInsetsBottomHeight
-import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -47,15 +33,21 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.GroupMember
-import org.session.libsession.utilities.Address
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.groups.GroupMemberState
import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel
import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.CollapsibleFooterState
-import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.*
+import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.CloseFooter
+import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.DismissRemoveMembersDialog
+import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.MemberClick
+import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.RemoveMembers
+import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.RemoveSearchState
+import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.SearchFocusChange
+import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.SearchQueryChange
+import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.ToggleFooter
+import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.OptionsItem
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.Cell
-import org.thoughtcrime.securesms.ui.CollapsibleFooterAction
import org.thoughtcrime.securesms.ui.CollapsibleFooterActionData
import org.thoughtcrime.securesms.ui.CollapsibleFooterItemData
import org.thoughtcrime.securesms.ui.DialogButtonData
@@ -64,8 +56,7 @@ import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.ItemButton
import org.thoughtcrime.securesms.ui.LoadingDialog
import org.thoughtcrime.securesms.ui.RadioOption
-import org.thoughtcrime.securesms.ui.SearchBarWithClose
-import org.thoughtcrime.securesms.ui.components.BackAppBar
+import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo
import org.thoughtcrime.securesms.ui.components.DialogTitledRadioButton
import org.thoughtcrime.securesms.ui.components.annotatedStringResource
import org.thoughtcrime.securesms.ui.getCellBottomShape
@@ -112,6 +103,7 @@ fun ManageMembers(
) {
val searchFocused = uiState.isSearchFocused
+ val isLandscape = getAdaptiveInfo().isLandscape
val handleBack: () -> Unit = {
when {
@@ -120,114 +112,90 @@ fun ManageMembers(
}
}
+ val searchLabel: @Composable (Modifier) -> Unit = { modifier ->
+ if (!searchFocused) {
+ Text(
+ modifier = Modifier.padding(
+ start = LocalDimensions.current.mediumSpacing
+ ),
+ text = LocalResources.current.getString(R.string.membersNonAdmins),
+ style = LocalType.current.base,
+ color = LocalColors.current.textSecondary
+ )
+ }
+ }
+
+ val header: @Composable (Modifier) -> Unit = { modifier ->
+ MembersSearchHeader(
+ searchFocused = searchFocused,
+ searchQuery = searchQuery,
+ onQueryChange = { sendCommand(SearchQueryChange(it)) },
+ onClear = { sendCommand(SearchQueryChange("")) },
+ onFocusChanged = { sendCommand(SearchFocusChange(it)) },
+ modifier = modifier
+ )
+ }
+
// Intercept system back
BackHandler(enabled = true) { handleBack() }
- Scaffold(
- topBar = {
- BackAppBar(
- title = stringResource(id = R.string.manageMembers),
- onBack = handleBack,
- )
- },
+ BaseManageGroupScreen(
+ title = stringResource(id = R.string.manageMembers),
+ onBack = handleBack,
+ enableCollapsingTopBarInLandscape = true,
+ collapseTopBar = searchFocused,
bottomBar = {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom))
- .imePadding()
- ) {
- CollapsibleFooterAction(
- data = CollapsibleFooterActionData(
- title = uiState.footer.footerActionTitle,
- collapsed = uiState.footer.collapsed,
- visible = uiState.footer.visible,
- items = uiState.footer.footerActionItems
- ),
- onCollapsedClicked = { sendCommand(ToggleFooter) },
- onClosedClicked = { sendCommand(CloseFooter) }
- )
- }
- },
- contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal),
+ CollapsibleFooterBottomBar(
+ footer = CollapsibleFooterActionData(
+ title = uiState.footer.footerActionTitle,
+ collapsed = uiState.footer.collapsed,
+ visible = uiState.footer.visible,
+ items = uiState.footer.footerActionItems
+ ),
+ onToggle = { sendCommand(ToggleFooter) },
+ onClose = { sendCommand(CloseFooter) }
+ )
+ }
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
) {
-
- AnimatedVisibility(
- // show only when add-members is enabled AND search is not focused
- visible = showAddMembers && !searchFocused,
- enter = fadeIn(animationSpec = tween(150)) +
- expandVertically(
- animationSpec = tween(200),
- expandFrom = Alignment.Top
- ),
- exit = fadeOut(animationSpec = tween(150)) +
- shrinkVertically(
- animationSpec = tween(180),
- shrinkTowards = Alignment.Top
- )
- ) {
- Cell(
- modifier = Modifier
- .fillMaxWidth()
- .padding(LocalDimensions.current.smallSpacing),
- ) {
- Column {
- uiState.options.forEachIndexed { index, option ->
- ItemButton(
- modifier = Modifier.qaTag(option.qaTag),
- text = annotatedStringResource(option.name),
- iconRes = option.icon,
- shape = when (index) {
- 0 -> getCellTopShape()
- uiState.options.lastIndex -> getCellBottomShape()
- else -> RectangleShape
- },
- onClick = option.onClick,
- )
-
- if (index != uiState.options.lastIndex) Divider()
- }
- }
- }
+ // PORTRAIT: options OUTSIDE scroll
+ if (!isLandscape) {
+ OptionsBlock(
+ show = showAddMembers && !searchFocused,
+ options = uiState.options
+ )
}
if (hasMembers) {
- if (!searchFocused) {
- Text(
- modifier = Modifier.padding(
- start = LocalDimensions.current.mediumSpacing,
- bottom = LocalDimensions.current.smallSpacing
- ),
- text = LocalResources.current.getString(R.string.membersNonAdmins),
- style = LocalType.current.base,
- color = LocalColors.current.textSecondary
- )
+ if (!isLandscape) {
+ searchLabel(Modifier)
+ header(Modifier)
}
- SearchBarWithClose(
- query = searchQuery,
- onValueChanged = { query -> sendCommand(SearchQueryChange(query)) },
- onClear = { sendCommand(SearchQueryChange("")) },
- placeholder = if (searchFocused) "" else LocalResources.current.getString(R.string.search),
- enabled = true,
- isFocused = searchFocused,
- modifier = Modifier.padding(horizontal = LocalDimensions.current.smallSpacing),
- onFocusChanged = { isFocused -> sendCommand(SearchFocusChange(isFocused)) }
- )
-
- Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
-
// List of members
LazyColumn(
modifier = Modifier
.weight(1f)
- .imePadding()
) {
+ // LANDSCAPE: options INSIDE scroll
+ if (isLandscape) {
+ item(key = "options") {
+ OptionsBlock(
+ show = showAddMembers && !searchFocused,
+ options = uiState.options
+ )
+ }
+
+ item { searchLabel(Modifier) }
+ stickyHeader {
+ header(Modifier)
+ }
+ }
+
items(members) { member ->
// Each member's view
ManageMemberItem(
@@ -237,12 +205,6 @@ fun ManageMembers(
selected = member in selectedMembers
)
}
-
- item {
- Spacer(
- modifier = Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)
- )
- }
}
} else {
Text(
@@ -271,13 +233,50 @@ fun ManageMembers(
}
}
+@Composable
+private fun OptionsBlock(
+ show: Boolean,
+ options: List, // use your actual type
+) {
+ AnimatedVisibility(
+ visible = show,
+ enter = fadeIn(animationSpec = tween(150)) +
+ expandVertically(animationSpec = tween(200), expandFrom = Alignment.Top),
+ exit = fadeOut(animationSpec = tween(150)) +
+ shrinkVertically(animationSpec = tween(180), shrinkTowards = Alignment.Top)
+ ) {
+ Cell(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(LocalDimensions.current.smallSpacing),
+ ) {
+ Column {
+ options.forEachIndexed { index, option ->
+ ItemButton(
+ modifier = Modifier.qaTag(option.qaTag),
+ text = annotatedStringResource(option.name),
+ iconRes = option.icon,
+ shape = when (index) {
+ 0 -> getCellTopShape()
+ options.lastIndex -> getCellBottomShape()
+ else -> RectangleShape
+ },
+ onClick = option.onClick
+ )
+ if (index != options.lastIndex) Divider()
+ }
+ }
+ }
+ }
+}
+
@Composable
fun RemoveMembersDialog(
state: ManageGroupMembersViewModel.RemoveMembersDialogState,
modifier: Modifier = Modifier,
sendCommand: (ManageGroupMembersViewModel.Commands) -> Unit
) {
- var deleteMessages by remember { mutableStateOf(false) }
+ var deleteMessages by retain { mutableStateOf(false) }
AlertDialog(
modifier = modifier,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/PromoteMembersScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/PromoteMembersScreen.kt
index dd5335825c..5846d6844a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/PromoteMembersScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/PromoteMembersScreen.kt
@@ -1,26 +1,19 @@
package org.thoughtcrime.securesms.groups.compose
import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
-import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -44,13 +37,11 @@ import org.thoughtcrime.securesms.groups.PromoteMembersViewModel.Commands.ShowCo
import org.thoughtcrime.securesms.groups.PromoteMembersViewModel.Commands.ShowPromoteDialog
import org.thoughtcrime.securesms.groups.PromoteMembersViewModel.Commands.ToggleFooter
import org.thoughtcrime.securesms.ui.AlertDialog
-import org.thoughtcrime.securesms.ui.CollapsibleFooterAction
import org.thoughtcrime.securesms.ui.CollapsibleFooterActionData
import org.thoughtcrime.securesms.ui.CollapsibleFooterItemData
import org.thoughtcrime.securesms.ui.DialogButtonData
import org.thoughtcrime.securesms.ui.GetString
-import org.thoughtcrime.securesms.ui.SearchBarWithClose
-import org.thoughtcrime.securesms.ui.components.BackAppBar
+import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo
import org.thoughtcrime.securesms.ui.components.annotatedStringResource
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
@@ -92,7 +83,9 @@ fun PromoteMembers(
hasActiveMembers: Boolean = false,
onPromoteClicked: (Set) -> Unit
) {
+
val searchFocused = uiState.isSearchFocused
+ val isLandscape = getAdaptiveInfo().isLandscape
val handleBack: () -> Unit = {
when {
@@ -101,45 +94,46 @@ fun PromoteMembers(
}
}
+ val searchHeader: @Composable (Modifier) -> Unit = { modifier ->
+ MembersSearchHeader(
+ searchFocused = searchFocused,
+ searchQuery = searchQuery,
+ onQueryChange = { sendCommand(SearchQueryChange(it)) },
+ onClear = { sendCommand(SearchQueryChange("")) },
+ onFocusChanged = { sendCommand(SearchFocusChange(it)) },
+ modifier = modifier
+ )
+ }
+
// Intercept system back
BackHandler(enabled = true) { handleBack() }
-
- Scaffold(
- topBar = {
- BackAppBar(
- title = pluralStringResource(id = R.plurals.promoteMember, 2),
- onBack = handleBack,
- )
- },
+ BaseManageGroupScreen(
+ title = pluralStringResource(id = R.plurals.promoteMember, 2),
+ onBack = handleBack,
+ enableCollapsingTopBarInLandscape = true,
+ collapseTopBar = searchFocused,
bottomBar = {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom))
- .imePadding()
- ) {
- CollapsibleFooterAction(
- data = CollapsibleFooterActionData(
- title = uiState.footer.footerTitle,
- collapsed = uiState.footer.collapsed,
- visible = uiState.footer.visible,
- items = listOf(
- CollapsibleFooterItemData(
- label = uiState.footer.footerActionLabel,
- buttonLabel = GetString(LocalResources.current.getString(R.string.promote)),
- isDanger = false,
- onClick = { sendCommand(ShowPromoteDialog) }
- )
+ CollapsibleFooterBottomBar(
+ footer = CollapsibleFooterActionData(
+ title = uiState.footer.footerTitle,
+ collapsed = uiState.footer.collapsed,
+ visible = uiState.footer.visible,
+ items = listOf(
+ CollapsibleFooterItemData(
+ label = uiState.footer.footerActionLabel,
+ buttonLabel = GetString(LocalResources.current.getString(R.string.promote)),
+ isDanger = false,
+ onClick = { sendCommand(ShowPromoteDialog) }
)
- ),
- onCollapsedClicked = { sendCommand(ToggleFooter) },
- onClosedClicked = { sendCommand(CloseFooter) }
- )
- }
- },
- contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal),
+ )
+ ),
+ onToggle = { sendCommand(ToggleFooter) },
+ onClose = { sendCommand(CloseFooter) }
+ )
+ }
) { paddingValues ->
+
Column(
modifier = Modifier
.padding(paddingValues)
@@ -150,27 +144,16 @@ fun PromoteMembers(
.padding(horizontal = LocalDimensions.current.mediumSpacing)
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally),
- text = LocalResources.current.getString(if (!hasActiveMembers) R.string.noNonAdminsInGroup else R.string.adminCannotBeDemoted),
+ text = LocalResources.current.getString(if (!hasActiveMembers) R.string.noNonAdminsInGroup else R.string.membersGroupPromotionAcceptInvite),
textAlign = TextAlign.Center,
style = LocalType.current.base,
color = LocalColors.current.textSecondary
)
if (hasActiveMembers) {
- Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
-
- SearchBarWithClose(
- query = searchQuery,
- onValueChanged = { query -> sendCommand(SearchQueryChange(query)) },
- onClear = { sendCommand(SearchQueryChange("")) },
- placeholder = if (searchFocused) "" else LocalResources.current.getString(R.string.search),
- enabled = true,
- isFocused = searchFocused,
- modifier = Modifier.padding(horizontal = LocalDimensions.current.smallSpacing),
- onFocusChanged = { isFocused -> sendCommand(SearchFocusChange(isFocused)) }
- )
-
- Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
+ if (!isLandscape) {
+ searchHeader(Modifier)
+ }
// List of members
LazyColumn(
@@ -178,6 +161,10 @@ fun PromoteMembers(
.weight(1f)
.imePadding()
) {
+ if (isLandscape) {
+ stickyHeader { searchHeader(Modifier) }
+ }
+
items(members) { member ->
// Each member's view
ManageMemberItem(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
index 14cd15f89e..bda537d0c4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
@@ -1,12 +1,18 @@
package org.thoughtcrime.securesms.home
import android.content.Context
+import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
+import androidx.core.view.updatePadding
import androidx.core.widget.TextViewCompat
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
@@ -19,18 +25,17 @@ import org.session.libsession.utilities.withUserConfigs
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.auth.LoginStateRepository
import org.thoughtcrime.securesms.database.GroupDatabase
+import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.NotifyType
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
+import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo
import javax.inject.Inject
@AndroidEntryPoint
-class ConversationOptionsBottomSheet(private val parentContext: Context) : BottomSheetDialogFragment(), View.OnClickListener {
+class ConversationOptionsBottomSheet() : BottomSheetDialogFragment(), View.OnClickListener {
private lateinit var binding: FragmentConversationBottomSheetBinding
- //FIXME AC: Supplying a threadRecord directly into the field from an activity
- // is not the best idea. It doesn't survive configuration change.
- // We should be dealing with IDs and all sorts of serializable data instead
- // if we want to use dialog fragments properly.
+
lateinit var publicKey: String
lateinit var thread: ThreadRecord
var group: GroupRecord? = null
@@ -41,6 +46,8 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
@Inject lateinit var groupDatabase: GroupDatabase
@Inject lateinit var loginStateRepository: LoginStateRepository
+ @Inject lateinit var threadDatabase: ThreadDatabase
+
var onViewDetailsTapped: (() -> Unit?)? = null
var onCopyConversationId: (() -> Unit?)? = null
var onPinTapped: (() -> Unit)? = null
@@ -53,8 +60,43 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
var onNotificationTapped: (() -> Unit)? = null
var onDeleteContactTapped: (() -> Unit)? = null
- override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
- binding = FragmentConversationBottomSheetBinding.inflate(LayoutInflater.from(parentContext), container, false)
+
+ companion object {
+ const val FRAGMENT_TAG = "ConversationOptionsBottomSheet"
+ private const val ARG_PUBLIC_KEY = "arg_public_key"
+ const val ARG_THREAD_ID = "arg_thread_id"
+ const val ARG_ADDRESS = "arg_address"
+
+ fun newInstance(publicKey: String, threadId: Long, address: String): ConversationOptionsBottomSheet {
+ return ConversationOptionsBottomSheet().apply {
+ arguments = Bundle().apply {
+ putString(ARG_PUBLIC_KEY, publicKey)
+ putLong(ARG_THREAD_ID, threadId)
+ putString(ARG_ADDRESS, address)
+ }
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val args = requireArguments()
+ publicKey = requireNotNull(args.getString(ARG_PUBLIC_KEY))
+ requireNotNull(args.getLong(ARG_THREAD_ID))
+ val addressString = requireNotNull(args.getString(ARG_ADDRESS))
+ val address = Address.fromSerialized(addressString)
+ thread = requireNotNull(
+ threadDatabase.getThreads(listOf(address)).firstOrNull()
+ ) { "Thread not found for address: $addressString" }
+ group = groupDatabase.getGroup(thread.recipient.address.toString()).orNull()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentConversationBottomSheetBinding.inflate(inflater, container, false)
return binding.root
}
@@ -219,5 +261,26 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
super.onStart()
val window = dialog?.window ?: return
window.setDimAmount(0.6f)
+
+ val dlg = dialog as? BottomSheetDialog ?: return
+ val sheet = dlg.findViewById(R.id.design_bottom_sheet)
+ ?: return
+
+ if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){
+ val behavior = BottomSheetBehavior.from(sheet)
+ behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
+ }
+
+ ViewCompat.setOnApplyWindowInsetsListener(sheet) { _, insets ->
+ val cut = insets.getInsets(
+ WindowInsetsCompat.Type.systemBars() or
+ WindowInsetsCompat.Type.displayCutout()
+ )
+
+ binding.root.updatePadding(left = cut.left, right = cut.right)
+ insets
+ }
+
+ ViewCompat.requestApplyInsets(sheet)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
index e64503fa73..207c025ba9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
@@ -49,7 +49,7 @@ class ConversationView : LinearLayout {
override fun onFinishInflate() {
super.onFinishInflate()
- layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT)
+ (layoutParams as? RecyclerView.LayoutParams)?.width = RecyclerView.LayoutParams.MATCH_PARENT
}
// endregion
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
index 798385cfa7..385dccf57d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
@@ -9,6 +9,7 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable
@@ -19,7 +20,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.core.graphics.Insets
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
@@ -99,7 +99,7 @@ import org.thoughtcrime.securesms.ui.theme.primaryGreen
import org.thoughtcrime.securesms.util.AvatarBadge
import org.thoughtcrime.securesms.util.AvatarUtils
import org.thoughtcrime.securesms.util.DateUtils
-import org.thoughtcrime.securesms.util.applySafeInsetsMargins
+import org.thoughtcrime.securesms.util.applyBottomInsetMargin
import org.thoughtcrime.securesms.util.applySafeInsetsPaddings
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.fadeIn
@@ -268,6 +268,20 @@ class HomeActivity : ScreenLockActionBarActivity(),
}
binding.sessionToolbar.disableClipping()
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ val searchHandled = homeViewModel.isSearchOpen.value &&
+ binding.globalSearchInputLayout.handleBackPressed()
+ if (searchHandled) return
+
+ if (homeViewModel.onBackPressed()) {
+ return
+ }
+
+ finish()
+ }
+ })
+
lifecycleScope.launch {
homeViewModel.shouldShowCurrentUserProBadge
.collectLatest {
@@ -463,6 +477,8 @@ class HomeActivity : ScreenLockActionBarActivity(),
)
}
+ rewireConversationOptionsCallbacksIfPresent()
+
applyViewInsets()
}
@@ -600,91 +616,105 @@ class HomeActivity : ScreenLockActionBarActivity(),
// endregion
// region Interaction
- @Deprecated("Deprecated in Java")
- override fun onBackPressed() {
- if (homeViewModel.isSearchOpen.value && binding.globalSearchInputLayout.handleBackPressed()) {
- return
- }
-
- if (!homeViewModel.onBackPressed()) {
- super.onBackPressed()
- }
- }
override fun onConversationClick(thread: ThreadRecord) {
push(ConversationActivityV2.createIntent(this, address = thread.recipient.address as Address.Conversable))
}
override fun onLongConversationClick(thread: ThreadRecord) {
- val bottomSheet = ConversationOptionsBottomSheet(this)
- bottomSheet.publicKey = publicKey
- bottomSheet.thread = thread
val threadRecipient = thread.recipient
- bottomSheet.group = groupDatabase.getGroup(threadRecipient.address.toString()).orNull()
- bottomSheet.onViewDetailsTapped = {
- bottomSheet.dismiss()
+ val bottomSheet = ConversationOptionsBottomSheet.newInstance(
+ publicKey = publicKey,
+ threadId = thread.threadId,
+ address = threadRecipient.address.toString()
+ )
+ attachConversationOptionsCallbacks(bottomSheet, thread)
+ bottomSheet.show(supportFragmentManager, ConversationOptionsBottomSheet.FRAGMENT_TAG)
+ }
+
+ /**
+ * If a ConversationOptionsBottomSheet was restored by FragmentManager after a
+ * configuration change, re-attach its callbacks and refresh the ThreadRecord.
+ */
+ private fun rewireConversationOptionsCallbacksIfPresent() {
+ val sheet = supportFragmentManager
+ .findFragmentByTag(ConversationOptionsBottomSheet.FRAGMENT_TAG)
+ as? ConversationOptionsBottomSheet ?: return
+
+ val threadId = sheet.requireArguments()
+ .getLong(ConversationOptionsBottomSheet.ARG_THREAD_ID)
+
+ val threadRecord = homeViewModel.data.value?.items?.asSequence()
+ ?.filterIsInstance()
+ ?.firstOrNull { it.thread.threadId == threadId }?.thread
+
+ threadRecord?.let {
+ attachConversationOptionsCallbacks(sheet, it)
+ }
+ }
+
+ private fun attachConversationOptionsCallbacks(
+ sheet: ConversationOptionsBottomSheet,
+ thread: ThreadRecord
+ ) {
+ val threadRecipient = thread.recipient
+ sheet.onViewDetailsTapped = {
+ sheet.dismiss()
homeViewModel.showUserProfileModal(thread)
}
- bottomSheet.onCopyConversationId = onCopyConversationId@{
- bottomSheet.dismiss()
+ sheet.onCopyConversationId = {
+ sheet.dismiss()
if (threadRecipient.address is Address.WithAccountId && !threadRecipient.isSelf) {
- val clip = ClipData.newPlainText("Account ID", threadRecipient.address.accountId.hexString)
- val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
- manager.setPrimaryClip(clip)
- Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
- }
- else if (threadRecipient.data is RecipientData.Community) {
+ val clip = ClipData.newPlainText(
+ "Account ID",
+ threadRecipient.address.accountId.hexString
+ )
+ (getSystemService(CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(clip)
+ Toast.makeText(this@HomeActivity, R.string.copied, Toast.LENGTH_SHORT).show()
+ } else if (threadRecipient.data is RecipientData.Community) {
val clip = ClipData.newPlainText("Community URL", threadRecipient.data.joinURL)
- val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
- manager.setPrimaryClip(clip)
- Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
+ (getSystemService(CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(clip)
+ Toast.makeText(this@HomeActivity, R.string.copied, Toast.LENGTH_SHORT).show()
}
}
- bottomSheet.onBlockTapped = {
- bottomSheet.dismiss()
- if (!threadRecipient.blocked) {
- blockConversation(thread)
- }
+ sheet.onBlockTapped = {
+ sheet.dismiss()
+ if (!threadRecipient.blocked) blockConversation(thread)
}
- bottomSheet.onUnblockTapped = {
- bottomSheet.dismiss()
- if (threadRecipient.blocked) {
- unblockConversation(thread)
- }
+ sheet.onUnblockTapped = {
+ sheet.dismiss()
+ if (threadRecipient.blocked) unblockConversation(thread)
}
- bottomSheet.onDeleteTapped = {
- bottomSheet.dismiss()
+ sheet.onDeleteTapped = {
+ sheet.dismiss()
deleteConversation(thread)
}
- bottomSheet.onNotificationTapped = {
- bottomSheet.dismiss()
- // go to the notification settings
- val intent = Intent(this, NotificationSettingsActivity::class.java).apply {
+ sheet.onNotificationTapped = {
+ sheet.dismiss()
+ startActivity(Intent(this, NotificationSettingsActivity::class.java).apply {
putExtra(NotificationSettingsActivity.ARG_ADDRESS, threadRecipient.address)
- }
- startActivity(intent)
+ })
}
- bottomSheet.onPinTapped = {
- bottomSheet.dismiss()
+ sheet.onPinTapped = {
+ sheet.dismiss()
setConversationPinned(threadRecipient.address, true)
}
- bottomSheet.onUnpinTapped = {
- bottomSheet.dismiss()
+ sheet.onUnpinTapped = {
+ sheet.dismiss()
setConversationPinned(threadRecipient.address, false)
}
- bottomSheet.onMarkAllAsReadTapped = {
- bottomSheet.dismiss()
+ sheet.onMarkAllAsReadTapped = {
+ sheet.dismiss()
markAllAsRead(thread)
}
- bottomSheet.onMarkAsUnreadTapped = {
- bottomSheet.dismiss()
+ sheet.onMarkAsUnreadTapped = {
+ sheet.dismiss()
markAsUnread(thread)
}
- bottomSheet.onDeleteContactTapped = {
- bottomSheet.dismiss()
+ sheet.onDeleteContactTapped = {
+ sheet.dismiss()
confirmDeleteContact(thread)
}
- bottomSheet.show(supportFragmentManager, bottomSheet.tag)
}
private fun blockConversation(thread: ThreadRecord) {
@@ -936,9 +966,9 @@ class HomeActivity : ScreenLockActionBarActivity(),
}
)
- binding.newConversationButton.applySafeInsetsMargins(
+ binding.newConversationButton.applyBottomInsetMargin(
typeMask = WindowInsetsCompat.Type.navigationBars(),
- additionalInsets = Insets.of(0,0,0, resources.getDimensionPixelSize(R.dimen.new_conversation_button_bottom_offset))
+ extraBottom = resources.getDimensionPixelSize(R.dimen.new_conversation_button_bottom_offset)
)
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt
index e6c3c06383..9831a9f6d2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -16,7 +16,17 @@ import org.session.libsession.utilities.StringSubstitutionConstants
import org.session.libsession.utilities.StringSubstitutionConstants.APP_PRO_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.PRO_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY
-import org.thoughtcrime.securesms.home.HomeViewModel.Commands.*
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.GotoProSettings
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.HandleUserProfileCommand
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.HideDonationCTADialog
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.HideExpiredCTADialog
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.HidePinCTADialog
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.HideSimpleDialog
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.HideUrlDialog
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.HideUserProfileModal
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.OnLinkCopied
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.OnLinkOpened
+import org.thoughtcrime.securesms.home.HomeViewModel.Commands.ShowDonationConfirmation
import org.thoughtcrime.securesms.home.startconversation.StartConversationSheet
import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsDestination
import org.thoughtcrime.securesms.ui.AlertDialog
@@ -108,7 +118,7 @@ fun HomeDialogs(
// we need a delay before displaying this.
// Setting the delay in the VM does not account for render and it seems to appear immediately
- var showExpiring by remember { mutableStateOf(false) }
+ var showExpiring by retain { mutableStateOf(false) }
LaunchedEffect(dialogsState.proExpiringCTA) {
showExpiring = false
if (dialogsState.proExpiringCTA != null) {
@@ -149,7 +159,7 @@ fun HomeDialogs(
// we need a delay before displaying this.
// Setting the delay in the VM does not account for render and it seems to appear immediately
- var showExpired by remember { mutableStateOf(false) }
+ var showExpired by retain { mutableStateOf(false) }
LaunchedEffect(dialogsState.proExpiredCTA) {
showExpired = false
if (dialogsState.proExpiredCTA) {
@@ -190,7 +200,7 @@ fun HomeDialogs(
// we need a delay before displaying this.
// Setting the delay in the VM does not account for render and it seems to appear immediately
- var showDonation by remember { mutableStateOf(false) }
+ var showDonation by retain { mutableStateOf(false) }
LaunchedEffect(dialogsState.donationCTA) {
showDonation = false
if (dialogsState.donationCTA) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt
index 3eafbf1190..c52a770ce1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt
@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import network.loki.messenger.R
import network.loki.messenger.libsession_util.PRIORITY_HIDDEN
import org.session.libsession.database.StorageProtocol
@@ -39,6 +40,7 @@ import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.auth.LoginStateRepository
import org.thoughtcrime.securesms.database.RecipientRepository
+import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.debugmenu.DebugLogGroup
import org.thoughtcrime.securesms.dependencies.ConfigFactory
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/SearchContactActionBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/SearchContactActionBottomSheet.kt
index 4633925d2b..64ad38bee3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/search/SearchContactActionBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/SearchContactActionBottomSheet.kt
@@ -1,15 +1,25 @@
package org.thoughtcrime.securesms.home.search
import android.content.Context
+import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.FrameLayout
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.core.os.BundleCompat
+import androidx.core.view.doOnLayout
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
@@ -52,7 +62,11 @@ class SearchContactActionBottomSheet : BottomSheetDialogFragment() {
savedInstanceState: Bundle?
): View = createThemedComposeView {
Column(
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier
+ .fillMaxWidth()
+ .windowInsetsPadding(
+ WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)
+ )
) {
// Only standard address can be blocked
if (address is Address.Standard) {
@@ -116,6 +130,18 @@ class SearchContactActionBottomSheet : BottomSheetDialogFragment() {
}
}
+ override fun onStart() {
+ super.onStart()
+
+ val dlg = dialog as? BottomSheetDialog ?: return
+ val sheet = dlg.findViewById(R.id.design_bottom_sheet) ?: return
+
+ if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ val behavior = BottomSheetBehavior.from(sheet)
+ behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ }
+ }
+
companion object {
private const val ARG_ADDRESS = "arg_address"
private const val ARG_CONTACT_NAME = "arg_contact_name"
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt
index c426fcfa6b..58a8a99a6d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt
@@ -3,26 +3,28 @@ package org.thoughtcrime.securesms.home.startconversation
import android.annotation.SuppressLint
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.ExperimentalSharedTransitionApi
-import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.retain.retain
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.compose.NavHost
@@ -63,13 +65,26 @@ fun StartConversationSheet(
modifier = modifier,
sheetState = sheetState,
dragHandle = null,
- onDismissRequest = onDismissRequest
+ onDismissRequest = onDismissRequest,
){
- BoxWithConstraints(modifier = modifier) {
+ BoxWithConstraints {
+ val windowWidthDp = with(LocalDensity.current) {
+ LocalWindowInfo.current.containerSize.width.toDp()
+ }
+
+ val isFullWidth = maxWidth >= windowWidthDp
+
+ val horizontalInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)
+ val contentMod = if (isFullWidth) {
+ Modifier.windowInsetsPadding(horizontalInsets)
+ } else {
+ Modifier
+ }
+
val topInset = WindowInsets.safeDrawing.asPaddingValues().calculateTopPadding()
val targetHeight = (this.maxHeight - topInset) * 0.94f // sheet should take up 94% of the height, without the staatus bar
Box(
- modifier = Modifier.height(targetHeight),
+ modifier = contentMod.height(targetHeight),
contentAlignment = Alignment.TopCenter
) {
StartConversationNavHost(
@@ -113,7 +128,7 @@ fun StartConversationNavHost(
){
val navController = rememberNavController()
val navigator: UINavigator =
- remember { UINavigator() }
+ retain { UINavigator() }
ObserveAsEvents(flow = navigator.navigationActions) { action ->
when (action) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/home/StartConversation.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/home/StartConversation.kt
index ce23c5147d..15a2bbb93e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/home/StartConversation.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/home/StartConversation.kt
@@ -1,12 +1,19 @@
package org.thoughtcrime.securesms.home.startconversation.home
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.aspectRatio
+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.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.verticalScroll
@@ -25,11 +32,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.min
import androidx.compose.ui.unit.times
import network.loki.messenger.R
import org.thoughtcrime.securesms.home.startconversation.StartConversationDestination
import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.ItemButton
+import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo
import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon
import org.thoughtcrime.securesms.ui.components.BasicAppBar
import org.thoughtcrime.securesms.ui.components.QrImage
@@ -49,12 +58,17 @@ internal fun StartConversationScreen(
navigateTo: (StartConversationDestination) -> Unit,
onClose: () -> Unit,
) {
- val context = LocalContext.current
+ val isLandscape = getAdaptiveInfo().isLandscape
- Column(modifier = Modifier.background(
- LocalColors.current.backgroundSecondary,
- shape = MaterialTheme.shapes.small.copy(bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp))
- )) {
+ Column(
+ modifier = Modifier.background(
+ LocalColors.current.backgroundSecondary,
+ shape = MaterialTheme.shapes.small.copy(
+ bottomStart = CornerSize(0.dp),
+ bottomEnd = CornerSize(0.dp)
+ )
+ )
+ ) {
BasicAppBar(
title = stringResource(R.string.conversationsStart),
backgroundColor = Color.Transparent, // transparent to show the rounded shape of the container
@@ -65,100 +79,181 @@ internal fun StartConversationScreen(
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()),
color = LocalColors.current.backgroundSecondary
) {
- Column(
- modifier = Modifier.verticalScroll(rememberScrollState())
- ) {
- val dividerIndent: Dp = LocalDimensions.current.itemButtonIconSpacing + 2*LocalDimensions.current.smallSpacing
- val newMessageTitleTxt:String = context.resources.getQuantityString(R.plurals.messageNew, 1, 1)
- val itemHeight = 50.dp
+ if (isLandscape) {
+ LandscapeContent(accountId, navigateTo)
+ } else {
+ PortraitContent(accountId, navigateTo)
+ }
+ }
+ }
+}
- ItemButton(
- text = annotatedStringResource(newMessageTitleTxt),
- textStyle = LocalType.current.xl,
- iconRes = R.drawable.ic_message_square,
- iconSize = LocalDimensions.current.iconMedium,
- modifier = Modifier.qaTag(R.string.AccessibilityId_messageNew),
- minHeight = itemHeight,
- onClick = {
- navigateTo(StartConversationDestination.NewMessage)
- }
- )
- Divider(
- paddingValues = PaddingValues(
- start = dividerIndent,
- end = LocalDimensions.current.smallSpacing
- )
- )
- ItemButton(
- text = annotatedStringResource(R.string.groupCreate),
- textStyle = LocalType.current.xl,
- iconRes = R.drawable.ic_users_group_custom,
- iconSize = LocalDimensions.current.iconMedium,
- modifier = Modifier.qaTag(R.string.AccessibilityId_groupCreate),
- minHeight = itemHeight,
- onClick = {
- navigateTo(StartConversationDestination.CreateGroup)
- }
- )
- Divider(
- paddingValues = PaddingValues(
- start = dividerIndent,
- end = LocalDimensions.current.smallSpacing
- )
- )
- ItemButton(
- text = annotatedStringResource(R.string.communityJoin),
- textStyle = LocalType.current.xl,
- iconRes = R.drawable.ic_globe,
- iconSize = LocalDimensions.current.iconMedium,
- modifier = Modifier.qaTag(R.string.AccessibilityId_communityJoin),
- minHeight = itemHeight,
- onClick = {
- navigateTo(StartConversationDestination.JoinCommunity)
- }
- )
- Divider(
- paddingValues = PaddingValues(
- start = dividerIndent,
- end = LocalDimensions.current.smallSpacing
- )
- )
- ItemButton(
- text = annotatedStringResource(R.string.sessionInviteAFriend),
- textStyle = LocalType.current.xl,
- iconRes = R.drawable.ic_user_round_plus,
- iconSize = LocalDimensions.current.iconMedium,
- modifier = Modifier.qaTag(R.string.AccessibilityId_sessionInviteAFriendButton),
- minHeight = itemHeight,
- onClick = {
- navigateTo(StartConversationDestination.InviteFriend)
- }
+@Composable
+private fun PortraitContent(
+ accountId: String,
+ navigateTo: (StartConversationDestination) -> Unit
+) {
+ Column(
+ modifier = Modifier.verticalScroll(rememberScrollState())
+ ) {
+ ActionList(navigateTo = navigateTo)
+ QrPanel(
+ accountId = accountId,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(LocalDimensions.current.spacing),
+ )
+ }
+}
+
+@Composable
+private fun LandscapeContent(
+ accountId: String,
+ navigateTo: (StartConversationDestination) -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(),
+ ) {
+ // Left: independently scrollable actions list
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .verticalScroll(rememberScrollState())
+ ) {
+ ActionList(navigateTo = navigateTo)
+ }
+
+ // Right: QR panel, vertically centered, with square sizing
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .verticalScroll(rememberScrollState())
+ ) {
+ QrPanel(
+ accountId = accountId,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = LocalDimensions.current.spacing)
+ .padding(bottom = LocalDimensions.current.spacing)
+ )
+ }
+
+ }
+}
+
+@Composable
+private fun QrPanel(
+ accountId: String,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier.widthIn(max = 420.dp),
+ ) {
+ Text(stringResource(R.string.accountIdYours), style = LocalType.current.xl)
+ Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing))
+ Text(
+ text = stringResource(R.string.qrYoursDescription),
+ color = LocalColors.current.textSecondary,
+ style = LocalType.current.small
+ )
+ Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
+ BoxWithConstraints(modifier = Modifier) {
+ val qrModifier = if (getAdaptiveInfo().isLandscape) {
+ val shortest: Dp = min(maxWidth, maxHeight)
+ val qrSide = (shortest * 0.70f).coerceIn(
+ LocalDimensions.current.minContentSize,
+ LocalDimensions.current.maxContentSize
)
- Column(
- modifier = Modifier
- .padding(horizontal = LocalDimensions.current.spacing)
- .padding(top = LocalDimensions.current.spacing)
- .padding(bottom = LocalDimensions.current.spacing)
- ) {
- Text(stringResource(R.string.accountIdYours), style = LocalType.current.xl)
- Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing))
- Text(
- text = stringResource(R.string.qrYoursDescription),
- color = LocalColors.current.textSecondary,
- style = LocalType.current.small
- )
- Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
- QrImage(
- string = accountId,
- Modifier.qaTag(R.string.AccessibilityId_qrCode),
- icon = R.drawable.session
- )
- }
+ Modifier.size(qrSide)
+ } else {
+ Modifier
}
+ QrImage(
+ string = accountId,
+ modifier = qrModifier
+ .qaTag(R.string.AccessibilityId_qrCode)
+ .aspectRatio(1f),
+ icon = R.drawable.session
+ )
}
}
}
+@Composable
+private fun ActionList(navigateTo: (StartConversationDestination) -> Unit) {
+ val context = LocalContext.current
+
+ val dividerIndent: Dp =
+ LocalDimensions.current.itemButtonIconSpacing + 2 * LocalDimensions.current.smallSpacing
+ val newMessageTitleTxt: String = context.resources.getQuantityString(R.plurals.messageNew, 1, 1)
+ val itemHeight = 50.dp
+
+ ItemButton(
+ text = annotatedStringResource(newMessageTitleTxt),
+ textStyle = LocalType.current.xl,
+ iconRes = R.drawable.ic_message_square,
+ iconSize = LocalDimensions.current.iconMedium,
+ minHeight = itemHeight,
+ modifier = Modifier.qaTag(R.string.AccessibilityId_messageNew),
+ onClick = {
+ navigateTo(StartConversationDestination.NewMessage)
+ }
+ )
+ Divider(
+ paddingValues = PaddingValues(
+ start = dividerIndent,
+ end = LocalDimensions.current.smallSpacing
+ )
+ )
+ ItemButton(
+ text = annotatedStringResource(R.string.groupCreate),
+ textStyle = LocalType.current.xl,
+ iconRes = R.drawable.ic_users_group_custom,
+ iconSize = LocalDimensions.current.iconMedium,
+ minHeight = itemHeight,
+ modifier = Modifier.qaTag(R.string.AccessibilityId_groupCreate),
+ onClick = {
+ navigateTo(StartConversationDestination.CreateGroup)
+ }
+ )
+ Divider(
+ paddingValues = PaddingValues(
+ start = dividerIndent,
+ end = LocalDimensions.current.smallSpacing
+ )
+ )
+ ItemButton(
+ text = annotatedStringResource(R.string.communityJoin),
+ textStyle = LocalType.current.xl,
+ iconRes = R.drawable.ic_globe,
+ iconSize = LocalDimensions.current.iconMedium,
+ minHeight = itemHeight,
+ modifier = Modifier.qaTag(R.string.AccessibilityId_communityJoin),
+ onClick = {
+ navigateTo(StartConversationDestination.JoinCommunity)
+ }
+ )
+ Divider(
+ paddingValues = PaddingValues(
+ start = dividerIndent,
+ end = LocalDimensions.current.smallSpacing
+ )
+ )
+ ItemButton(
+ text = annotatedStringResource(R.string.sessionInviteAFriend),
+ textStyle = LocalType.current.xl,
+ iconRes = R.drawable.ic_user_round_plus,
+ iconSize = LocalDimensions.current.iconMedium,
+ minHeight = itemHeight,
+ modifier = Modifier.qaTag(R.string.AccessibilityId_sessionInviteAFriendButton),
+ onClick = {
+ navigateTo(StartConversationDestination.InviteFriend)
+ }
+ )
+
+}
+
@Preview
@Composable
private fun PreviewStartConversationScreen(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt
index 6416061ba4..7e4ad51095 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt
@@ -17,10 +17,9 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -32,7 +31,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -64,8 +63,8 @@ fun MediaOverviewScreen(
val selectionMode by viewModel.inSelectionMode.collectAsState()
val conversationName by viewModel.conversationName.collectAsState()
val topAppBarState = rememberTopAppBarState()
- var showingDeleteConfirmation by remember { mutableStateOf(false) }
- var showingSaveAttachmentWarning by remember { mutableStateOf(false) }
+ var showingDeleteConfirmation by retain { mutableStateOf(false) }
+ var showingSaveAttachmentWarning by retain { mutableStateOf(false) }
val context = LocalContext.current
val requestStoragePermission =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
@@ -138,7 +137,7 @@ fun MediaOverviewScreen(
appBarScrollBehavior = appBarScrollBehavior
)
},
- contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
+ contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)
) { paddings ->
Column(
modifier = Modifier
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXActivity.kt
new file mode 100644
index 0000000000..029ce0e37b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXActivity.kt
@@ -0,0 +1,366 @@
+package org.thoughtcrime.securesms.mediasend
+
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.os.Bundle
+import android.util.Log
+import android.util.Size
+import android.view.View
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.resolutionselector.ResolutionSelector
+import androidx.camera.core.resolutionselector.ResolutionStrategy
+import androidx.camera.view.LifecycleCameraController
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.lifecycleScope
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+import network.loki.messenger.R
+import network.loki.messenger.databinding.ActivityCameraxBinding
+import org.session.libsession.utilities.MediaTypes
+import org.session.libsession.utilities.TextSecurePreferences
+import org.thoughtcrime.securesms.ScreenLockActionBarActivity
+import org.thoughtcrime.securesms.providers.BlobUtils
+import org.thoughtcrime.securesms.util.applySafeInsetsPaddings
+import org.thoughtcrime.securesms.util.setSafeOnClickListener
+import org.thoughtcrime.securesms.webrtc.Orientation
+import org.thoughtcrime.securesms.webrtc.OrientationManager
+import java.io.ByteArrayOutputStream
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class CameraXActivity : ScreenLockActionBarActivity() {
+
+ override val applyDefaultWindowInsets: Boolean
+ get() = false
+
+ companion object {
+ private const val TAG = "CameraXActivity"
+ private const val REQUEST_CODE_PERMISSIONS = 10
+ private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
+
+ const val EXTRA_IMAGE_URI = "extra_image_uri"
+ const val EXTRA_IMAGE_SIZE = "extra_image_size"
+ const val EXTRA_IMAGE_WIDTH = "extra_image_width"
+ const val EXTRA_IMAGE_HEIGHT = "extra_image_height"
+
+ const val KEY_MEDIA_SEND_COUNT ="key_mediasend_count"
+ }
+
+ private lateinit var binding: ActivityCameraxBinding
+
+ private lateinit var cameraController: LifecycleCameraController
+ private lateinit var cameraExecutor: ExecutorService
+ private var cameraInitialized = false
+
+ private var lastRotation: Orientation = Orientation.UNKNOWN
+
+ private val portraitConstraints = ConstraintSet()
+ private val landscapeConstraints = ConstraintSet()
+ private lateinit var rootConstraintLayout: ConstraintLayout
+
+ private var orientationManager = OrientationManager(this)
+
+ @Inject
+ lateinit var prefs: TextSecurePreferences
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityCameraxBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ cameraExecutor = Executors.newSingleThreadExecutor()
+
+ rootConstraintLayout = binding.root
+
+ // 1) Portrait constraints: from a portrait layout
+ portraitConstraints.clone(this, R.layout.activity_camerax_portrait)
+ // 2) Landscape constraints: cloned from a template XML
+ landscapeConstraints.clone(this, R.layout.activity_camerax_landscape)
+
+ setupUi()
+ applyViewInsets()
+ initializeCountButton()
+
+ // Permissions should ideally be handled before launching this Activity,
+ // but keep this as a safety check.
+ if (allPermissionsGranted()) {
+ startCamera()
+ } else {
+ ActivityCompat.requestPermissions(
+ this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
+ )
+ }
+
+ lifecycleScope.launch {
+ orientationManager.orientation.collect { orientation ->
+ if (!orientationManager.isAutoRotateOn()) {
+ updateUiForRotation(orientation)
+ }
+ }
+ }
+ }
+
+ private fun setupUi() {
+ binding.cameraCaptureButton.setSafeOnClickListener { takePhoto() }
+ binding.cameraFlipButton.setSafeOnClickListener { flipCamera() }
+ binding.cameraCloseButton.setSafeOnClickListener {
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ }
+
+ private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
+ ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
+ }
+
+ private fun startCamera() {
+ // Work out a resolution based on available memory
+ val activityManager =
+ getSystemService(ACTIVITY_SERVICE) as android.app.ActivityManager
+ val memoryClassMb = activityManager.memoryClass
+ val preferredResolution: Size = when {
+ memoryClassMb >= 256 -> Size(1920, 1440)
+ memoryClassMb >= 128 -> Size(1280, 960)
+ else -> Size(640, 480)
+ }
+ Log.d(
+ TAG,
+ "Selected resolution: $preferredResolution based on memory class: $memoryClassMb MB"
+ )
+
+ val resolutionSelector = ResolutionSelector.Builder()
+ .setResolutionStrategy(
+ ResolutionStrategy(
+ preferredResolution,
+ ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+ )
+ )
+ .build()
+
+ // Set up camera
+ cameraController = LifecycleCameraController(this).apply {
+ cameraSelector = prefs.getPreferredCameraDirection()
+ setImageCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+ setTapToFocusEnabled(true)
+ setPinchToZoomEnabled(true)
+
+ // Configure image capture resolution
+ setImageCaptureResolutionSelector(resolutionSelector)
+ }
+
+ // Attach it to the view
+ binding.previewView.controller = cameraController
+ cameraController.bindToLifecycle(this)
+
+ // Wait for initialization to complete
+ cameraController.initializationFuture.addListener({
+ cameraInitialized = true
+ updateFlipButtonVisibility()
+ }, ContextCompat.getMainExecutor(this))
+ }
+
+ private fun updateFlipButtonVisibility() {
+ if (!::cameraController.isInitialized) return
+
+ val hasFront = cameraController.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)
+ val hasBack = cameraController.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)
+
+ binding.cameraFlipButton.visibility =
+ if (hasFront && hasBack) View.VISIBLE else View.GONE
+ }
+
+ private fun takePhoto() {
+ val isFrontCamera = cameraController.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA
+
+ cameraController.takePicture(
+ cameraExecutor,
+ object : ImageCapture.OnImageCapturedCallback() {
+ override fun onCaptureSuccess(img: ImageProxy) {
+ try {
+ val buffer = img.planes[0].buffer
+ val originalBytes = ByteArray(buffer.remaining()).also { buffer.get(it) }
+ val rotationDegrees = img.imageInfo.rotationDegrees
+ img.close()
+
+ val bitmap =
+ BitmapFactory.decodeByteArray(originalBytes, 0, originalBytes.size)
+ var correctedBitmap = rotateBitmap(bitmap, rotationDegrees.toFloat())
+ if (isFrontCamera) {
+ correctedBitmap = mirrorBitmap(correctedBitmap)
+ }
+
+ val width = correctedBitmap.width
+ val height = correctedBitmap.height
+
+ val outputStream = ByteArrayOutputStream()
+ correctedBitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
+ val compressedBytes = outputStream.toByteArray()
+
+ // Recycle bitmaps
+ bitmap.recycle()
+ if (correctedBitmap !== bitmap) correctedBitmap.recycle()
+
+ val uri = BlobUtils.getInstance()
+ .forData(compressedBytes)
+ .withMimeType(MediaTypes.IMAGE_JPEG)
+ .createForSingleSessionInMemory()
+
+ val data = Intent().apply {
+ data = uri
+ putExtra(EXTRA_IMAGE_URI, uri.toString())
+ putExtra(EXTRA_IMAGE_SIZE, compressedBytes.size.toLong())
+ putExtra(EXTRA_IMAGE_WIDTH, width)
+ putExtra(EXTRA_IMAGE_HEIGHT, height)
+ }
+
+ setResult(RESULT_OK, data)
+ finish()
+ } catch (t: Throwable) {
+ Log.e(TAG, "capture failed", t)
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ }
+
+ override fun onError(e: ImageCaptureException) {
+ Log.e(TAG, "takePicture error", e)
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ }
+ )
+ }
+
+ private fun mirrorBitmap(src: Bitmap): Bitmap {
+ val matrix = android.graphics.Matrix().apply { preScale(-1f, 1f) }
+ return Bitmap.createBitmap(src, 0, 0, src.width, src.height, matrix, true)
+ }
+
+ private fun rotateBitmap(src: Bitmap, degrees: Float): Bitmap {
+ if (degrees == 0f) return src
+ val matrix = android.graphics.Matrix().apply { postRotate(degrees) }
+ return Bitmap.createBitmap(src, 0, 0, src.width, src.height, matrix, true)
+ }
+
+ private fun flipCamera() {
+ if (!::cameraController.isInitialized) return
+
+ val newSelector =
+ if (cameraController.cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA)
+ CameraSelector.DEFAULT_FRONT_CAMERA
+ else
+ CameraSelector.DEFAULT_BACK_CAMERA
+
+ cameraController.cameraSelector = newSelector
+ prefs.setPreferredCameraDirection(newSelector)
+
+ // Animate icon (simple 180° spin; no manual orientation tracking)
+ binding.cameraFlipButton.animate()
+ .rotationBy(-180f)
+ .setDuration(200)
+ .start()
+ }
+
+ private fun updateUiForRotation(rotation: Orientation = lastRotation) {
+ val rotation =
+ when (rotation) {
+ Orientation.LANDSCAPE -> -90f
+ Orientation.REVERSED_LANDSCAPE -> 90f
+ else -> 0f
+ }
+
+ binding.cameraFlipButton.animate()
+ .rotation(rotation)
+ .setDuration(150)
+ .start()
+ }
+
+ private fun initializeCountButton() {
+ val count = intent.getIntExtra(KEY_MEDIA_SEND_COUNT, 0)
+
+ binding.mediasendCountContainer.mediasendCountButtonText.text = count.toString()
+ binding.mediasendCountContainer.mediasendCountButton.isEnabled = count > 0
+ binding.mediasendCountContainer.mediasendCountButton.visibility = if(count >0) View.VISIBLE else View.INVISIBLE
+ if (count > 0) {
+ binding.mediasendCountContainer.mediasendCountButton.setOnClickListener { v: View? ->
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ } else {
+ binding.mediasendCountContainer.mediasendCountButton.setOnClickListener(null)
+ }
+ }
+
+ private fun applyViewInsets(){
+ binding.cameraCloseButton.applySafeInsetsPaddings()
+ binding.root.applySafeInsetsPaddings(
+ applyTop = false,
+ applyBottom = true,
+ consumeInsets = false
+ )
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+
+ if (!::rootConstraintLayout.isInitialized) return
+
+ val isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
+
+ if (isLandscape) {
+ landscapeConstraints.applyTo(rootConstraintLayout)
+ } else {
+ portraitConstraints.applyTo(rootConstraintLayout)
+ }
+ if (cameraInitialized) {
+ updateFlipButtonVisibility()
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == REQUEST_CODE_PERMISSIONS) {
+ if (allPermissionsGranted()) {
+ startCamera()
+ } else {
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ orientationManager.startOrientationListener()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ orientationManager.stopOrientationListener()
+ }
+
+ override fun onDestroy() {
+ if (::cameraExecutor.isInitialized) {
+ cameraExecutor.shutdown()
+ }
+ super.onDestroy()
+
+ orientationManager.destroy()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt
deleted file mode 100644
index 6faff31f88..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.kt
+++ /dev/null
@@ -1,278 +0,0 @@
-package org.thoughtcrime.securesms.mediasend
-
-import android.Manifest
-import android.content.Context
-import android.content.pm.PackageManager
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.net.Uri
-import android.os.Bundle
-import android.util.Log
-import android.util.Size
-import android.view.LayoutInflater
-import android.view.OrientationEventListener
-import android.view.Surface
-import android.view.View
-import android.view.ViewGroup
-import androidx.camera.core.CameraSelector
-import androidx.camera.core.ImageCapture
-import androidx.camera.core.ImageCaptureException
-import androidx.camera.core.ImageProxy
-import androidx.camera.core.resolutionselector.ResolutionSelector
-import androidx.camera.core.resolutionselector.ResolutionStrategy
-import androidx.camera.view.LifecycleCameraController
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
-import androidx.fragment.app.Fragment
-import dagger.hilt.android.AndroidEntryPoint
-import network.loki.messenger.databinding.CameraxFragmentBinding
-import org.session.libsession.utilities.MediaTypes
-import org.session.libsession.utilities.TextSecurePreferences
-import org.thoughtcrime.securesms.providers.BlobUtils
-import org.thoughtcrime.securesms.util.applySafeInsetsMargins
-import org.thoughtcrime.securesms.util.setSafeOnClickListener
-import java.io.ByteArrayOutputStream
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class CameraXFragment : Fragment() {
-
- interface Controller {
- fun onImageCaptured(imageUri: Uri, size: Long, width: Int, height: Int)
- fun onCameraError()
- }
-
- private lateinit var binding: CameraxFragmentBinding
-
- private var callbacks: Controller? = null
-
- private lateinit var cameraController: LifecycleCameraController
- private lateinit var cameraExecutor: ExecutorService
-
-
- private lateinit var orientationListener: OrientationEventListener
- private var lastRotation: Int = Surface.ROTATION_0
-
- @Inject
- lateinit var prefs: TextSecurePreferences
-
- companion object {
- private const val TAG = "CameraXFragment"
- private const val REQUEST_CODE_PERMISSIONS = 10
- private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
- }
-
- override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- binding = CameraxFragmentBinding.inflate(inflater, container, false)
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- cameraExecutor = Executors.newSingleThreadExecutor()
-
- // permissions should be handled prior to landing in this fragment
- // but this is added for safety
- if (allPermissionsGranted()) {
- startCamera()
- } else {
- ActivityCompat.requestPermissions(
- requireActivity(), REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
- )
- }
-
- binding.cameraControlsSafeArea.applySafeInsetsMargins()
-
- binding.cameraCaptureButton.setSafeOnClickListener { takePhoto() }
- binding.cameraFlipButton.setSafeOnClickListener { flipCamera() }
- binding.cameraCloseButton.setSafeOnClickListener {
- requireActivity().onBackPressedDispatcher.onBackPressed()
- }
-
- // keep track of orientation changes
- orientationListener = object : OrientationEventListener(requireContext()) {
- override fun onOrientationChanged(degrees: Int) {
- if (degrees == ORIENTATION_UNKNOWN) return
-
- val newRotation = when {
- degrees in 45..134 -> Surface.ROTATION_270
- degrees in 135..224 -> Surface.ROTATION_180
- degrees in 225..314 -> Surface.ROTATION_90
- else -> Surface.ROTATION_0
- }
-
- if (newRotation != lastRotation) {
- lastRotation = newRotation
- updateUiForRotation(newRotation)
- }
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
- orientationListener.enable()
- }
-
- override fun onPause() {
- orientationListener.disable()
- super.onPause()
- }
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- if (context is Controller) {
- callbacks = context
- } else {
- throw RuntimeException("$context must implement CameraXFragment.Controller")
- }
- }
-
- private fun updateUiForRotation(rotation: Int = lastRotation) {
- val angle = when (rotation) {
- Surface.ROTATION_0 -> 0f
- Surface.ROTATION_90 -> 90f
- Surface.ROTATION_180 -> 180f
- else -> 270f
- }
-
- binding.cameraFlipButton.animate()
- .rotation(angle)
- .setDuration(150)
- .start()
- }
-
- private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
- ContextCompat.checkSelfPermission(
- requireContext(), it
- ) == PackageManager.PERMISSION_GRANTED
- }
-
- private fun startCamera() {
- // work out a resolution based on available memory
- val activityManager = requireContext().getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
- val memoryClassMb = activityManager.memoryClass // e.g. 128, 256, etc.
- val preferredResolution: Size = when {
- memoryClassMb >= 256 -> Size(1920, 1440)
- memoryClassMb >= 128 -> Size(1280, 960)
- else -> Size(640, 480)
- }
- Log.d(TAG, "Selected resolution: $preferredResolution based on memory class: $memoryClassMb MB")
-
- val resolutionSelector = ResolutionSelector.Builder()
- .setResolutionStrategy(
- ResolutionStrategy(
- preferredResolution,
- ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
- )
- )
- .build()
-
- // set up camera
- cameraController = LifecycleCameraController(requireContext()).apply {
- cameraSelector = prefs.getPreferredCameraDirection()
- setImageCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
- setTapToFocusEnabled(true)
- setPinchToZoomEnabled(true)
-
- // Configure image capture resolution
- setImageCaptureResolutionSelector(resolutionSelector)
- }
-
- // attach it to the view
- binding.previewView.controller = cameraController
- cameraController.bindToLifecycle(viewLifecycleOwner)
-
- // wait for initialisation to complete
- cameraController.initializationFuture.addListener({
- val hasFront = cameraController.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA)
- val hasBack = cameraController.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)
-
- binding.cameraFlipButton.visibility =
- if (hasFront && hasBack) View.VISIBLE else View.GONE
- }, ContextCompat.getMainExecutor(requireContext()))
- }
-
- private fun takePhoto() {
- val isFrontCamera = cameraController.cameraSelector == CameraSelector.DEFAULT_FRONT_CAMERA
- cameraController.takePicture(
- cameraExecutor,
- object : ImageCapture.OnImageCapturedCallback() {
- override fun onCaptureSuccess(img: ImageProxy) {
- try {
- val buffer = img.planes[0].buffer
- val originalBytes = ByteArray(buffer.remaining()).also { buffer.get(it) }
- val rotationDegrees = img.imageInfo.rotationDegrees
- img.close()
-
- // Decode, rotate, mirror if needed
- val bitmap = BitmapFactory.decodeByteArray(originalBytes, 0, originalBytes.size)
- var correctedBitmap = rotateBitmap(bitmap, rotationDegrees.toFloat())
- if (isFrontCamera) {
- correctedBitmap = mirrorBitmap(correctedBitmap)
- }
-
- val outputStream = ByteArrayOutputStream()
- correctedBitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
- val compressedBytes = outputStream.toByteArray()
-
- // Recycle bitmaps
- bitmap.recycle()
- if (correctedBitmap !== bitmap) correctedBitmap.recycle()
-
- val uri = BlobUtils.getInstance()
- .forData(compressedBytes)
- .withMimeType(MediaTypes.IMAGE_JPEG)
- .createForSingleSessionInMemory()
-
- callbacks?.onImageCaptured(uri, compressedBytes.size.toLong(), correctedBitmap.width, correctedBitmap.height)
- } catch (t: Throwable) {
- Log.e(TAG, "capture failed", t)
- callbacks?.onCameraError()
- }
- }
- override fun onError(e: ImageCaptureException) {
- Log.e(TAG, "takePicture error", e)
- callbacks?.onCameraError()
- }
- }
- )
- }
-
- private fun mirrorBitmap(src: Bitmap): Bitmap {
- val matrix = android.graphics.Matrix().apply { preScale(-1f, 1f) }
- return Bitmap.createBitmap(src, 0, 0, src.width, src.height, matrix, true)
- }
-
- private fun rotateBitmap(src: Bitmap, degrees: Float): Bitmap {
- if (degrees == 0f) return src
- val matrix = android.graphics.Matrix().apply { postRotate(degrees) }
- return Bitmap.createBitmap(src, 0, 0, src.width, src.height, matrix, true)
- }
-
- private fun flipCamera() {
- val newSelector =
- if (cameraController.cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA)
- CameraSelector.DEFAULT_FRONT_CAMERA
- else
- CameraSelector.DEFAULT_BACK_CAMERA
-
- cameraController.cameraSelector = newSelector
- prefs.setPreferredCameraDirection(newSelector)
-
- // animate icon
- binding.cameraFlipButton.animate()
- .rotationBy(-180f)
- .setDuration(200)
- .start()
- }
-
- override fun onDestroyView() {
- cameraExecutor.shutdown()
- super.onDestroyView()
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
index f060bf4424..3bdf73322b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerFolderFragment.java
@@ -131,7 +131,7 @@ private void initToolbar(Toolbar toolbar) {
actionBar.setTitle(txt);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
- toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
+ toolbar.setNavigationOnClickListener(v -> requireActivity().getOnBackPressedDispatcher().onBackPressed());
}
initToolbarOptions();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
index 6b0055d633..8af99c9a19 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
@@ -177,7 +177,7 @@ private void initToolbar(Toolbar toolbar) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
- toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
+ toolbar.setNavigationOnClickListener(v -> requireActivity().getOnBackPressedDispatcher().onBackPressed());
}
private void initMediaObserver(@NonNull MediaSendViewModel viewModel) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt
index aaedc5e497..67956e8753 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.kt
@@ -13,6 +13,8 @@ import android.view.animation.DecelerateInterpolator
import android.view.animation.OvershootInterpolator
import android.view.animation.ScaleAnimation
import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.view.ViewGroupCompat
import androidx.fragment.app.Fragment
@@ -33,6 +35,7 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ScreenLockActionBarActivity
import org.thoughtcrime.securesms.database.RecipientRepository
+import org.thoughtcrime.securesms.mediasend.CameraXActivity.Companion.KEY_MEDIA_SEND_COUNT
import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.CountButtonState
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
@@ -50,7 +53,8 @@ import javax.inject.Inject
@AndroidEntryPoint
class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragment.Controller,
MediaPickerItemFragment.Controller, MediaSendFragment.Controller,
- ImageEditorFragment.Controller, CameraXFragment.Controller{
+ ImageEditorFragment.Controller {
+
private var recipient: Recipient? = null
private val viewModel: MediaSendViewModel by viewModels()
@@ -59,6 +63,7 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
@Inject
lateinit var recipientRepository: RecipientRepository
+ private var lastEntryFromCameraCapture: Boolean = false
override val applyDefaultWindowInsets: Boolean
get() = false // we want to handle window insets manually here for fullscreen fragments like the camera screen
@@ -74,44 +79,49 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
ViewGroupCompat.installCompatInsetsDispatch(it.root)
}
- setResult(RESULT_CANCELED)
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ handleBackPressedCompat()
+ }
+ })
- if (savedInstanceState != null) {
- return
- }
+ setResult(RESULT_CANCELED)
// Apply windowInsets for our own UI (not the fragment ones because they will want to do their own things)
binding.mediasendBottomBar.applySafeInsetsPaddings()
- recipient = recipientRepository.getRecipientSync(fromSerialized(
- intent.getStringExtra(KEY_ADDRESS)!!
- ))
+ recipient = recipientRepository.getRecipientSync(
+ fromSerialized(
+ intent.getStringExtra(KEY_ADDRESS)!!
+ )
+ )
viewModel.onBodyChanged(intent.getStringExtra(KEY_BODY)!!)
- val media: List? = intent.getParcelableArrayListExtra(KEY_MEDIA)
- val isCamera = intent.getBooleanExtra(KEY_IS_CAMERA, false)
+ if (savedInstanceState == null) {
+ val media: List? = intent.getParcelableArrayListExtra(KEY_MEDIA)
+ val isCamera = intent.getBooleanExtra(KEY_IS_CAMERA, false)
- if (isCamera) {
- val fragment: Fragment = CameraXFragment()
- supportFragmentManager.beginTransaction()
- .replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
- .commit()
- } else if (!isEmpty(media)) {
- viewModel.onSelectedMediaChanged(this, media!!)
+ if (isCamera) {
+ navigateToCamera()
+ } else if (!isEmpty(media)) {
+ viewModel.onSelectedMediaChanged(this, media!!)
- val fragment: Fragment = MediaSendFragment.newInstance(recipient!!.address)
+ lastEntryFromCameraCapture = false
- supportFragmentManager.beginTransaction()
- .replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
- .commit()
- } else {
- val fragment = MediaPickerFolderFragment.newInstance(
- recipient!!
- )
- supportFragmentManager.beginTransaction()
- .replace(R.id.mediasend_fragment_container, fragment, TAG_FOLDER_PICKER)
- .commit()
+ val fragment: Fragment = MediaSendFragment.newInstance(recipient!!.address)
+
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
+ .commit()
+ } else {
+ val fragment = MediaPickerFolderFragment.newInstance(
+ recipient!!
+ )
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.mediasend_fragment_container, fragment, TAG_FOLDER_PICKER)
+ .commit()
+ }
}
initializeCountButtonObserver()
@@ -129,15 +139,29 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
}
}
- override fun onBackPressed() {
- super.onBackPressed()
+ private fun handleBackPressedCompat() {
+ val fm = supportFragmentManager
+ val isCameraFlow = intent.getBooleanExtra(KEY_IS_CAMERA, false)
+
+ // Special case: we just came from camera, in camera-first flow,
+ // and we're on the editor as the only fragment.
+ if (lastEntryFromCameraCapture && isCameraFlow && fm.backStackEntryCount == 1) {
+ fm.popBackStackImmediate() // remove the editor fragment
+ viewModel.onImageCaptureUndo(this@MediaSendActivity)
+ lastEntryFromCameraCapture = false
+ navigateToCamera()
+ return
+ }
- if (intent.getBooleanExtra(
- KEY_IS_CAMERA,
- false
- ) && supportFragmentManager.backStackEntryCount == 0
- ) {
- viewModel.onImageCaptureUndo(this)
+ // Otherwise: normal fragment back behaviour
+ if (fm.backStackEntryCount > 0) {
+ fm.popBackStack()
+ } else {
+ // Root of the activity
+ if (isCameraFlow) {
+ setResult(RESULT_CANCELED, Intent())
+ }
+ finish()
}
}
@@ -173,8 +197,9 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
override fun onMediaSelected(media: Media) {
try {
viewModel.onSingleMediaSelected(this, media)
+ lastEntryFromCameraCapture = false
navigateToMediaSend(recipient!!.address)
- } catch (e: Exception){
+ } catch (e: Exception) {
Log.e(TAG, "Error selecting media", e)
Toast.makeText(this, R.string.errorUnknown, Toast.LENGTH_LONG).show()
}
@@ -234,67 +259,30 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
fragment?.onTouchEventsNeeded(needed)
}
- override fun onCameraError() {
- lifecycleScope.launch {
- Toast.makeText(applicationContext, R.string.cameraErrorUnavailable, Toast.LENGTH_SHORT).show()
- setResult(RESULT_CANCELED, Intent())
- finish()
- }
- }
-
- override fun onImageCaptured(imageUri: Uri, size: Long, width: Int, height: Int) {
- Log.i(TAG, "Camera image captured.")
- SimpleTask.run(lifecycle, {
- try {
- return@run Media(
- imageUri,
- constructPhotoFilename(this),
- MediaTypes.IMAGE_JPEG,
- System.currentTimeMillis(),
- width,
- height,
- size,
- Media.ALL_MEDIA_BUCKET_ID,
- null
- )
- } catch (e: Exception) {
- return@run null
- }
- }, { media: Media? ->
- if (media == null) {
- onNoMediaAvailable()
- return@run
- }
- Log.i(TAG, "Camera capture stored: " + media.uri.toString())
-
- viewModel.onImageCaptured(media)
- navigateToMediaSend(recipient!!.address)
- })
- }
-
private fun initializeCountButtonObserver() {
viewModel.getCountButtonState().observe(
this
) { buttonState: CountButtonState? ->
if (buttonState == null) return@observe
- binding.mediasendCountButtonText.text = buttonState.count.toString()
- binding.mediasendCountButton.isEnabled = buttonState.isVisible
+ binding.mediasendCountContainer.mediasendCountButtonText.text = buttonState.count.toString()
+ binding.mediasendCountContainer.mediasendCountButton.isEnabled = buttonState.isVisible
animateButtonVisibility(
- binding.mediasendCountButton,
- binding.mediasendCountButton.visibility,
+ binding.mediasendCountContainer.mediasendCountButton,
+ binding.mediasendCountContainer.mediasendCountButton.visibility,
if (buttonState.isVisible) View.VISIBLE else View.GONE
)
if (buttonState.count > 0) {
- binding.mediasendCountButton.setOnClickListener { v: View? ->
+ binding.mediasendCountContainer.mediasendCountButton.setOnClickListener { v: View? ->
+ lastEntryFromCameraCapture = false
navigateToMediaSend(
recipient!!.address
)
}
if (buttonState.isVisible) {
- animateButtonTextChange(binding.mediasendCountButton)
+ animateButtonTextChange(binding.mediasendCountContainer.mediasendCountButton)
}
} else {
- binding.mediasendCountButton.setOnClickListener(null)
+ binding.mediasendCountContainer.mediasendCountButton.setOnClickListener(null)
}
}
}
@@ -384,21 +372,10 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
.request(Manifest.permission.CAMERA)
.withPermanentDenialDialog(permanentDenialTxt)
.onAllGranted {
- val fragment = orCreateCameraFragment
- supportFragmentManager.beginTransaction()
- .setCustomAnimations(
- R.anim.slide_from_right,
- R.anim.slide_to_left,
- R.anim.slide_from_left,
- R.anim.slide_to_right
- )
- .replace(
- R.id.mediasend_fragment_container,
- fragment,
- TAG_CAMERA
- )
- .addToBackStack(null)
- .commit()
+ val countNow = viewModel.getCountButtonState().value?.count ?: 0
+ val intent = Intent(this@MediaSendActivity, CameraXActivity::class.java)
+ .putExtra(KEY_MEDIA_SEND_COUNT, countNow)
+ cameraLauncher.launch(intent)
viewModel.onCameraStarted()
}
@@ -412,14 +389,6 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
.execute()
}
- private val orCreateCameraFragment: CameraXFragment
- get() {
- val fragment =
- supportFragmentManager.findFragmentByTag(TAG_CAMERA) as CameraXFragment?
-
- return fragment ?: CameraXFragment()
- }
-
private fun animateButtonVisibility(button: View, oldVisibility: Int, newVisibility: Int) {
if (oldVisibility == newVisibility) return
@@ -511,6 +480,74 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
}
}
+ private val cameraLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK && result.data != null) {
+ val data = result.data!!
+
+ val uriString = data.getStringExtra(CameraXActivity.EXTRA_IMAGE_URI)
+ val size = data.getLongExtra(CameraXActivity.EXTRA_IMAGE_SIZE, -1L)
+ val width = data.getIntExtra(CameraXActivity.EXTRA_IMAGE_WIDTH, -1)
+ val height = data.getIntExtra(CameraXActivity.EXTRA_IMAGE_HEIGHT, -1)
+
+ val uri = uriString?.let { Uri.parse(it) }
+
+ if (uri == null || size <= 0 || width <= 0 || height <= 0) {
+ handleCameraError()
+ } else {
+ handleCameraImageCaptured(uri, size, width, height)
+ }
+ }else{
+ if(supportFragmentManager.backStackEntryCount == 0){
+ finish()
+ }
+ }
+ }
+
+ private fun handleCameraError() {
+ lifecycleScope.launch {
+ Toast.makeText(applicationContext, R.string.cameraErrorUnavailable, Toast.LENGTH_SHORT)
+ .show()
+ setResult(RESULT_CANCELED, Intent())
+ finish()
+ }
+ }
+
+ private fun handleCameraImageCaptured(imageUri: Uri, size: Long, width: Int, height: Int) {
+ Log.i(TAG, "Camera image captured.")
+ SimpleTask.run(lifecycle, {
+ try {
+ return@run Media(
+ imageUri,
+ constructPhotoFilename(this),
+ MediaTypes.IMAGE_JPEG,
+ System.currentTimeMillis(),
+ width,
+ height,
+ size,
+ Media.ALL_MEDIA_BUCKET_ID,
+ null
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Error constructing Media from camera result", e)
+ return@run null
+ }
+ }, { media: Media? ->
+ if (media == null) {
+ onNoMediaAvailable()
+ return@run
+ }
+ Log.i(TAG, "Camera capture stored: ${media.uri}")
+ viewModel.onImageCaptured(media)
+ lastEntryFromCameraCapture = true
+ navigateToMediaSend(recipient!!.address)
+ })
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ }
+
companion object {
private val TAG: String = MediaSendActivity::class.java.simpleName
@@ -564,3 +601,4 @@ class MediaSendActivity : ScreenLockActionBarActivity(), MediaPickerFolderFragme
}
}
}
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/migration/DatabaseMigrationScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/migration/DatabaseMigrationScreen.kt
index 74fb9332d4..cf83a2e84f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/migration/DatabaseMigrationScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/migration/DatabaseMigrationScreen.kt
@@ -18,8 +18,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -41,8 +41,8 @@ import org.thoughtcrime.securesms.preferences.ShareLogsDialog
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.DialogButtonData
import org.thoughtcrime.securesms.ui.GetString
-import org.thoughtcrime.securesms.ui.components.OutlineButton
import org.thoughtcrime.securesms.ui.components.AccentFillButton
+import org.thoughtcrime.securesms.ui.components.OutlineButton
import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
@@ -89,8 +89,8 @@ private fun DatabaseMigration(
onClearData: () -> Unit = {},
onClearDataWithoutLoggingOut: () -> Unit = {},
) {
- var showingClearDeviceRestoreWarning by remember { mutableStateOf(false) }
- var showingClearDeviceRestartWarning by remember { mutableStateOf(false) }
+ var showingClearDeviceRestoreWarning by retain { mutableStateOf(false) }
+ var showingClearDeviceRestartWarning by retain { mutableStateOf(false) }
Surface(
color = LocalColors.current.background,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt
index 5362632d4a..d2c589a1b6 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt
@@ -24,6 +24,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -70,7 +71,7 @@ internal fun LandingScreen(
var count by remember { mutableStateOf(0) }
val listState = rememberLazyListState()
- var isUrlDialogVisible by remember { mutableStateOf(false) }
+ var isUrlDialogVisible by retain { mutableStateOf(false) }
if (isUrlDialogVisible) {
TCPolicyDialog(
@@ -181,7 +182,7 @@ internal fun LandingScreen(
@Composable
private fun AnimateMessageText(text: String, isOutgoing: Boolean, modifier: Modifier = Modifier) {
- var visible by remember { mutableStateOf(false) }
+ var visible by retain { mutableStateOf(false) }
LaunchedEffect(Unit) { visible = true }
Box {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt
index 9ea4fd2bd6..abab1f68c8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.onboarding.messagenotifications
import android.app.Activity
import android.os.Bundle
+import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -45,6 +46,13 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
setComposeContent { MessageNotificationsScreen() }
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (viewModel.onBackPressed()) return
+ finish()
+ }
+ })
+
lifecycleScope.launch {
viewModel.events.collect {
when (it) {
@@ -55,14 +63,6 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
}
}
- @Deprecated("Deprecated in Java")
- override fun onBackPressed() {
- if (viewModel.onBackPressed()) return
-
- @Suppress("DEPRECATION")
- super.onBackPressed()
- }
-
@Composable
private fun MessageNotificationsScreen() {
val uiState by viewModel.uiStates.collectAsState()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt
index b8018ab9e4..c84cc600a7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.onboarding.pickname
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -39,6 +40,15 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
setComposeContent { DisplayNameScreen(viewModel) }
+ // Predictive back-firendly
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (!viewModel.onBackPressed()) {
+ finish()
+ }
+ }
+ })
+
lifecycleScope.launch(Dispatchers.Main) {
viewModel.events.collect {
when (it) {
@@ -59,14 +69,6 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
quit = { viewModel.dismissDialog(); finish() }
)
}
-
- @Deprecated("Deprecated in Java")
- override fun onBackPressed() {
- if (viewModel.onBackPressed()) return
-
- @Suppress("DEPRECATION")
- super.onBackPressed()
- }
}
fun Context.startPickDisplayNameActivity(loadFailed: Boolean = false, flags: Int = 0) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt
index f9b44b4cb1..02c80c81c8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt
@@ -4,17 +4,26 @@ import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
import android.os.Bundle
import android.view.View
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
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 dagger.hilt.android.AndroidEntryPoint
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.min
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -26,6 +35,7 @@ import org.thoughtcrime.securesms.ScreenLockActionBarActivity
import org.thoughtcrime.securesms.auth.LoginStateRepository
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.permissions.Permissions
+import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo
import org.thoughtcrime.securesms.ui.components.QRScannerScreen
import org.thoughtcrime.securesms.ui.components.QrImage
import org.thoughtcrime.securesms.ui.components.SessionTabRow
@@ -48,7 +58,11 @@ class QRCodeActivity : ScreenLockActionBarActivity() {
override val applyDefaultWindowInsets: Boolean
get() = false
- private val errors = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+ private val errors = MutableSharedFlow(
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
@@ -85,7 +99,11 @@ class QRCodeActivity : ScreenLockActionBarActivity() {
}
}
- override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
@@ -95,11 +113,17 @@ class QRCodeActivity : ScreenLockActionBarActivity() {
private fun Tabs(accountId: String, errors: Flow, onScan: (String) -> Unit) {
val pagerState = rememberPagerState { TITLES.size }
- Column {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(LocalColors.current.background)
+ ) {
SessionTabRow(pagerState, TITLES)
HorizontalPager(
state = pagerState,
- modifier = Modifier.weight(1f)
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
) { page ->
when (TITLES[page]) {
R.string.view -> QrPage(accountId)
@@ -111,6 +135,15 @@ private fun Tabs(accountId: String, errors: Flow, onScan: (String) -> Un
@Composable
fun QrPage(string: String) {
+ if (getAdaptiveInfo().isLandscape) {
+ LandscapeContent(string)
+ } else {
+ PortraitContent(string)
+ }
+}
+
+@Composable
+private fun PortraitContent(string: String) {
Column(
modifier = Modifier
.background(LocalColors.current.background)
@@ -120,7 +153,10 @@ fun QrPage(string: String) {
QrImage(
string = string,
modifier = Modifier
- .padding(top = LocalDimensions.current.mediumSpacing, bottom = LocalDimensions.current.xsSpacing)
+ .padding(
+ top = LocalDimensions.current.mediumSpacing,
+ bottom = LocalDimensions.current.xsSpacing
+ )
.qaTag(R.string.AccessibilityId_qrCode),
icon = R.drawable.session
)
@@ -133,3 +169,45 @@ fun QrPage(string: String) {
)
}
}
+
+@Composable
+private fun LandscapeContent(string: String) {
+ BoxWithConstraints(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(LocalColors.current.background)
+ .padding(horizontal = LocalDimensions.current.mediumSpacing)
+ ) {
+ // Scale QR to the shorter side to avoid overflow in landscape. Clamp for sanity
+ val shortest: Dp = min(maxWidth, maxHeight)
+ val qrSide = (shortest * 0.70f).coerceIn(
+ LocalDimensions.current.minContentSize,
+ LocalDimensions.current.maxContentSize
+ )
+
+ Column(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .verticalScroll(rememberScrollState())
+ .padding(vertical = LocalDimensions.current.spacing), // vertical + horizontal centering
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing)
+
+ ) {
+ QrImage(
+ string = string,
+ modifier = Modifier
+ .size(qrSide)
+ .qaTag(R.string.AccessibilityId_qrCode),
+ icon = R.drawable.session
+ )
+
+ Text(
+ text = stringResource(R.string.accountIdYoursDescription),
+ color = LocalColors.current.textSecondary,
+ textAlign = TextAlign.Center,
+ style = LocalType.current.small
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt
index 5d29d66d54..56008a2fa8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
@@ -42,6 +43,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
@@ -212,7 +214,7 @@ fun Settings(
}
)
},
- contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal),
+ contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal),
) { paddings ->
// MAIN SCREEN CONTENT
Column(
@@ -347,7 +349,7 @@ fun Settings(
)
Spacer(modifier = Modifier.height(LocalDimensions.current.spacing))
- Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
+ Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
// DIALOGS AND SHEETS
@@ -699,7 +701,7 @@ fun ShowClearDataDialog(
modifier: Modifier = Modifier,
sendCommand: (SettingsViewModel.Commands) -> Unit
) {
- var deleteOnNetwork by remember { mutableStateOf(false)}
+ var deleteOnNetwork by retain { mutableStateOf(false)}
val context = LocalContext.current
AlertDialog(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt
index 2e50075736..2f3fcd54b5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt
@@ -12,10 +12,13 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.widthIn
@@ -36,6 +39,7 @@ import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -103,20 +107,24 @@ fun BaseProSettingsScreen(
onBack = onBack,
)
}} else {{}},
- contentWindowInsets = WindowInsets.systemBars,
+ contentWindowInsets = WindowInsets.safeDrawing,
) { paddings ->
+
+ val layoutDirection = LocalLayoutDirection.current
+ val safeInsetsPadding = PaddingValues(
+ start = paddings.calculateStartPadding(layoutDirection) + LocalDimensions.current.spacing,
+ end = paddings.calculateEndPadding(layoutDirection)+ LocalDimensions.current.spacing,
+ top = (paddings.calculateTopPadding() - LocalDimensions.current.appBarHeight)
+ .coerceAtLeast(0.dp) + 46.dp,
+ bottom = paddings.calculateBottomPadding() + LocalDimensions.current.spacing
+ )
+
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.consumeWindowInsets(paddings),
state = listState,
- contentPadding = PaddingValues(
- start = LocalDimensions.current.spacing,
- end = LocalDimensions.current.spacing,
- top = (paddings.calculateTopPadding() - LocalDimensions.current.appBarHeight)
- .coerceAtLeast(0.dp) + 46.dp,
- bottom = paddings.calculateBottomPadding() + LocalDimensions.current.spacing
- ),
+ contentPadding = safeInsetsPadding,
horizontalAlignment = CenterHorizontally
) {
item {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt
index 33fe94fe89..5adaa79e21 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsNavHost.kt
@@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
@@ -66,7 +67,7 @@ fun ProSettingsNavHost(
onBack: () -> Unit
){
val navController = rememberNavController()
- val navigator: UINavigator = remember {
+ val navigator: UINavigator = retain {
UINavigator()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt
index 7889463fd8..8fcdd4ca1f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt
@@ -2,21 +2,22 @@ package org.thoughtcrime.securesms.recoverypassword
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -31,6 +32,7 @@ import org.thoughtcrime.securesms.ui.Cell
import org.thoughtcrime.securesms.ui.DialogButtonData
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.SessionShieldIcon
+import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo
import org.thoughtcrime.securesms.ui.border
import org.thoughtcrime.securesms.ui.components.QrImage
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
@@ -70,7 +72,7 @@ private fun RecoveryPasswordCell(
seed: String?,
copyMnemonic:() -> Unit = {}
) {
- var showQr by remember {
+ var showQr by retain {
mutableStateOf(false)
}
@@ -104,13 +106,34 @@ private fun RecoveryPasswordCell(
showQr,
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
- QrImage(
- seed,
- modifier = Modifier
- .padding(vertical = LocalDimensions.current.spacing)
- .qaTag(R.string.AccessibilityId_qrCode),
- icon = R.drawable.ic_recovery_password_custom
- )
+ val config = getAdaptiveInfo()
+ val isLandscape = config.isLandscape
+
+ BoxWithConstraints {
+ val availableWidth = maxWidth
+ val widthTarget = availableWidth * if (isLandscape) 0.80f else 1f
+ val heightCap = if (isLandscape) config.heightDp.dp * 0.70f else config.heightDp.dp * 1f
+
+ val qrSide = widthTarget
+ .coerceAtMost(heightCap)
+ .coerceIn(
+ LocalDimensions.current.minContentSizeMedium,
+ LocalDimensions.current.maxContentSizeMedium
+ )
+
+ QrImage(
+ seed,
+ modifier = Modifier
+ .padding(
+ top = LocalDimensions.current.spacing,
+ bottom = LocalDimensions.current.smallSpacing
+ )
+ .size(qrSide)
+ .qaTag(R.string.AccessibilityId_qrCode),
+ icon = R.drawable.ic_recovery_password_custom
+ )
+
+ }
}
AnimatedVisibility(!showQr) {
@@ -158,8 +181,8 @@ private fun RecoveryPassword(mnemonic: String) {
private fun HideRecoveryPasswordCell(
confirmHideRecovery:() -> Unit
) {
- var showHideRecoveryDialog by remember { mutableStateOf(false) }
- var showHideRecoveryConfirmationDialog by remember { mutableStateOf(false) }
+ var showHideRecoveryDialog by retain { mutableStateOf(false) }
+ var showHideRecoveryConfirmationDialog by retain { mutableStateOf(false) }
Cell {
Row(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/StickerSelectActivity.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/StickerSelectActivity.java
index 8afc241a56..d3a32b84b8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/StickerSelectActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/StickerSelectActivity.java
@@ -28,6 +28,8 @@
import com.google.android.material.tabs.TabLayoutMediator;
+import org.thoughtcrime.securesms.util.ViewUtilitiesKt;
+
import network.loki.messenger.R;
import network.loki.messenger.databinding.ScribbleSelectStickerActivityBinding;
@@ -51,6 +53,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
setContentView(binding.getRoot());
binding.cameraStickerPager.setAdapter(new StickerPagerAdapter(this, this));
+ ViewUtilitiesKt.applySafeInsetsPaddings(binding.getRoot());
new TabLayoutMediator(
binding.cameraStickerTabs,
@@ -64,7 +67,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
- onBackPressed();
+ getOnBackPressedDispatcher().onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPage.kt b/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPage.kt
index 0c1ba1b02f..9f7fd33963 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPage.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPage.kt
@@ -6,6 +6,7 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -17,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
@@ -36,11 +38,11 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.TopCenter
@@ -71,6 +73,7 @@ import org.session.libsession.utilities.StringSubstitutionConstants.TOKEN_NAME_S
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
import org.thoughtcrime.securesms.ui.SpeechBubbleTooltip
+import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo
import org.thoughtcrime.securesms.ui.components.AccentOutlineButtonRect
import org.thoughtcrime.securesms.ui.components.BackAppBar
import org.thoughtcrime.securesms.ui.components.BlurredImage
@@ -115,7 +118,7 @@ fun TokenPage(
.qaTag("Page heading")
)
},
- contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal),
+ contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal),
) { contentPadding ->
PullToRefreshBox(
@@ -179,7 +182,8 @@ fun TokenPage(
val hasNoScroll = scrollState.maxValue == 0 || scrollState.maxValue == Int.MAX_VALUE
Column(
- modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)
+ modifier = Modifier
+ .padding(horizontal = LocalDimensions.current.spacing)
.then(
if (hasNoScroll) {
Modifier.weight(1f)
@@ -247,7 +251,7 @@ fun SessionNetworkInfoSection(modifier: Modifier = Modifier) {
)
// Note: We apply the link to the entire box so the user doesn't have to click exactly on the highlighted text.
- var showTheOpenUrlModal by remember { mutableStateOf(false) }
+ var showTheOpenUrlModal by retain { mutableStateOf(false) }
Text(
modifier = Modifier
.clickable { showTheOpenUrlModal = true }
@@ -276,7 +280,6 @@ fun StatsImageBox(
) {
Box(
modifier = modifier
- .fillMaxWidth()
.aspectRatio(1.15f)
.border(
width = 1.dp,
@@ -474,32 +477,53 @@ fun StatsSection(
priceDataPopupText: String,
modifier: Modifier = Modifier
) {
- // First row contains the `StatsImageBox` with the number of nodes in your swap and the text
- // details with that number and the number of nodes securing your messages.
- Row(modifier = modifier.fillMaxWidth()) {
- // On the left we have the node image showing how many nodes are in the user's swarm..
- val (linesDrawable, circlesDrawable) = getNodeImageForSwarmSize(currentSessionNodesInSwarm)
- StatsImageBox(
- showNodeCountsAsRefreshing = showNodeCountsAsRefreshing,
- lineDrawableId = linesDrawable,
- circlesDrawableId = circlesDrawable,
- modifier = Modifier
- .fillMaxWidth(0.45f)
- .qaTag("Swarm image")
- )
+ val screenInfo = getAdaptiveInfo()
+ BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
+ // Left pane target width = min(45% of row, height cap * aspect)
+ val leftMaxWidth = maxWidth * 0.45f
+ val aspect = 1.15f
- Spacer(modifier = Modifier.width(LocalDimensions.current.xsSpacing))
+ val heightCap =
+ if (screenInfo.isLandscape) screenInfo.heightDp.dp * 0.40f else screenInfo.heightDp.dp * 0.50f
- // ..and on the right we have the text details of num nodes in swarm and total nodes securing your messages.
- NodeDetailsBox(
- showNodeCountsAsRefreshing = showNodeCountsAsRefreshing,
- numNodesInSwarm = currentSessionNodesInSwarm.toString(),
- numNodesSecuringMessages = currentSessionNodesSecuringMessages.toString(),
- modifier = Modifier
- .fillMaxWidth(1.0f)
- .align(Alignment.CenterVertically)
- )
+ val targetWidth = leftMaxWidth
+ .coerceAtMost(heightCap * aspect)
+ .coerceIn(
+ LocalDimensions.current.minContentSizeSmall,
+ LocalDimensions.current.maxContentSizeSmall
+ ) // hard cap to keep tidy on very wide screens
+
+
+ // First row contains the `StatsImageBox` with the number of nodes in your swap and the text
+ // details with that number and the number of nodes securing your messages.
+ Row(modifier = modifier.fillMaxWidth()) {
+
+ // On the left we have the node image showing how many nodes are in the user's swarm..
+ val (linesDrawable, circlesDrawable) = getNodeImageForSwarmSize(
+ currentSessionNodesInSwarm
+ )
+ StatsImageBox(
+ showNodeCountsAsRefreshing = showNodeCountsAsRefreshing,
+ lineDrawableId = linesDrawable,
+ circlesDrawableId = circlesDrawable,
+ modifier = Modifier
+ .width(targetWidth)
+ .qaTag("Swarm image")
+ )
+
+ Spacer(modifier = Modifier.width(LocalDimensions.current.xsSpacing))
+
+ // ..and on the right we have the text details of num nodes in swarm and total nodes securing your messages.
+ NodeDetailsBox(
+ showNodeCountsAsRefreshing = showNodeCountsAsRefreshing,
+ numNodesInSwarm = currentSessionNodesInSwarm.toString(),
+ numNodesSecuringMessages = currentSessionNodesSecuringMessages.toString(),
+ modifier = Modifier
+ .fillMaxWidth(1.0f)
+ .align(Alignment.CenterVertically)
+ )
+ }
}
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
@@ -580,7 +604,8 @@ fun StatsSection(
setTwoLineTwo,
setTwoLineThree,
qaTag = "Network secured amount",
- modifier = Modifier.fillMaxWidth(1.0f)
+ modifier = Modifier
+ .fillMaxWidth(1.0f)
.onGloballyPositioned { coordinates ->
// Calculate this cell's height in dp
val heightInDp = with(density) { coordinates.size.height.toDp() }
@@ -705,7 +730,7 @@ fun SessionTokenSection(
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
// Finally, add a button that links us to the staging page to learn more
- var showTheOpenUrlModal by remember { mutableStateOf(false) }
+ var showTheOpenUrlModal by retain { mutableStateOf(false) }
AccentOutlineButtonRect(
text = LocalContext.current.getString(R.string.sessionNetworkLearnAboutStaking),
modifier = Modifier
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
index e5911a54f3..58200ebb14 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
@@ -75,13 +75,13 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.onFocusChanged
@@ -99,7 +99,6 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.graphics.shadow.Shadow
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
@@ -118,7 +117,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import kotlinx.coroutines.CoroutineScope
@@ -1156,8 +1154,8 @@ fun ExpandableText(
expandButtonText: String = stringResource(id = R.string.viewMore),
collapseButtonText: String = stringResource(id = R.string.viewLess),
) {
- var expanded by remember { mutableStateOf(false) }
- var showButton by remember { mutableStateOf(false) }
+ var expanded by retain { mutableStateOf(false) }
+ var showButton by retain { mutableStateOf(false) }
var maxHeight by remember { mutableStateOf(Dp.Unspecified) }
val density = LocalDensity.current
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Modifiers.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Modifiers.kt
index bb7ecb5716..01ffc16516 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/Modifiers.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Modifiers.kt
@@ -16,11 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithCache
@@ -48,7 +44,6 @@ import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.delay
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt
index 735c418938..4f0b7bb567 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt
@@ -49,6 +49,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -253,8 +254,8 @@ fun SessionProCTA(
// We should avoid internal state in a composable but having the bottom sheet
// here avoids re-defining the sheet in multiple places in the app
- var showDialog by remember { mutableStateOf(true) }
- var showProSheet by remember { mutableStateOf(false) }
+ var showDialog by retain { mutableStateOf(true) }
+ var showProSheet by retain { mutableStateOf(false) }
// default handling of the upgrade button
val defaultUpgrade: () -> Unit = {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/adaptive/AdaptiveLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/adaptive/AdaptiveLayout.kt
new file mode 100644
index 0000000000..22f1c64567
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/adaptive/AdaptiveLayout.kt
@@ -0,0 +1,30 @@
+package org.thoughtcrime.securesms.ui.adaptive
+
+import android.annotation.SuppressLint
+import android.content.res.Configuration
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.platform.LocalConfiguration
+
+/**
+ * Immutable, @Stable container for adaptive layout info.
+ * Safe to hoist and pass to child composables without causing unnecessary recompositions.
+ */
+@Stable
+data class AdaptiveInfo(
+ val widthDp: Int,
+ val heightDp: Int,
+ val isLandscape: Boolean
+)
+
+/**
+ * Returns a stable snapshot of the window/adaptive state for the current composition.
+ * Currently we use this for landscape
+ */
+@SuppressLint("ConfigurationScreenWidthHeight")
+@Composable
+fun getAdaptiveInfo(): AdaptiveInfo {
+ val configuration = LocalConfiguration.current
+ val landscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+ return AdaptiveInfo(configuration.screenWidthDp, configuration.screenHeightDp, landscape)
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/ConversationAppBar.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/ConversationAppBar.kt
index 220fabdbf0..cad643778f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/ConversationAppBar.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/ConversationAppBar.kt
@@ -12,14 +12,18 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
@@ -107,7 +111,8 @@ fun ConversationAppBar(
if (data.pagerData.isNotEmpty()) {
// Settings content pager
ConversationSettingsPager(
- modifier = Modifier.padding(top = 2.dp)
+ modifier = Modifier
+ .padding(top = 2.dp)
.fillMaxWidth(0.8f),
pages = data.pagerData,
pagerState = pagerState
@@ -142,14 +147,18 @@ fun ConversationAppBar(
// Avatar
if (data.showAvatar) {
Avatar(
- modifier = Modifier.qaTag(R.string.qa_conversation_avatar)
+ modifier = Modifier
+ .qaTag(R.string.qa_conversation_avatar)
.padding(
- start = if(data.showCall) 0.dp else LocalDimensions.current.xsSpacing,
+ start = if (data.showCall) 0.dp else LocalDimensions.current.xsSpacing,
end = LocalDimensions.current.xsSpacing
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
- indication = ripple(bounded = false, radius = LocalDimensions.current.iconLargeAvatar/2),
+ indication = ripple(
+ bounded = false,
+ radius = LocalDimensions.current.iconLargeAvatar / 2
+ ),
onClick = onAvatarPressed
),
size = LocalDimensions.current.iconLargeAvatar,
@@ -164,7 +173,11 @@ fun ConversationAppBar(
true -> {
Row(
modifier = Modifier
- .statusBarsPadding()
+ .windowInsetsPadding(
+ WindowInsets.safeDrawing.only(
+ WindowInsetsSides.Top + WindowInsetsSides.Horizontal
+ )
+ )
.padding(horizontal = LocalDimensions.current.smallSpacing)
.heightIn(min = LocalDimensions.current.appBarHeight),
verticalAlignment = Alignment.CenterVertically,
@@ -180,7 +193,8 @@ fun ConversationAppBar(
onValueChanged = onSearchQueryChanged,
onClear = onSearchQueryClear,
placeholder = stringResource(R.string.search),
- modifier = Modifier.weight(1f)
+ modifier = Modifier
+ .weight(1f)
.focusRequester(focusRequester),
backgroundColor = LocalColors.current.backgroundSecondary,
)
@@ -188,7 +202,8 @@ fun ConversationAppBar(
Spacer(Modifier.width(LocalDimensions.current.xsSpacing))
Text(
- modifier = Modifier.qaTag(R.string.qa_conversation_search_cancel)
+ modifier = Modifier
+ .qaTag(R.string.qa_conversation_search_cancel)
.clickable {
onSearchCanceled()
},
@@ -240,7 +255,8 @@ private fun ConversationSettingsPager(
modifier = modifier,
) { page ->
Row (
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier
+ .fillMaxWidth()
.qaTag(pages[page].qaTag ?: pages[page].title)
.clickable {
pages[page].action()
@@ -281,7 +297,9 @@ private fun ConversationSettingsPager(
// '>' icon
if(pages.size > 1) {
Image(
- modifier = Modifier.size(12.dp).rotate(180f),
+ modifier = Modifier
+ .size(12.dp)
+ .rotate(180f),
painter = painterResource(id = R.drawable.ic_chevron_left),
colorFilter = ColorFilter.tint(LocalColors.current.text),
contentDescription = null,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/DropDown.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/DropDown.kt
index 47ecd00418..1807c34ccd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/DropDown.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/DropDown.kt
@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -52,7 +53,7 @@ fun DropDown(
labeler: (T?) -> String,
allowSelectingNullValue: Boolean
) {
- var expanded by remember { mutableStateOf(false) }
+ var expanded by retain { mutableStateOf(false) }
ExposedDropdownMenuBox(
modifier = modifier,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt
index a0454ff985..19a7d991e5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt
@@ -36,6 +36,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.retain.retain
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -101,7 +102,7 @@ fun QRScannerScreen(
val permission = Manifest.permission.CAMERA
val cameraPermissionState = rememberPermissionState(permission)
- var showCameraPermissionDialog by remember { mutableStateOf(false) }
+ var showCameraPermissionDialog by retain { mutableStateOf(false) }
if (cameraPermissionState.status.isGranted) {
ScanQrCode(errors, onScan)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionTabRow.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionTabRow.kt
index 632cb49b0c..c66abaf0f9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionTabRow.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/SessionTabRow.kt
@@ -30,26 +30,65 @@ private val TITLES = listOf(R.string.sessionRecoveryPassword, R.string.qrScan)
@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun SessionTabRow(pagerState: PagerState, titles: List) {
+fun SessionTabRow(
+ pagerState: PagerState,
+ titles: List
+) {
+ val animationScope = rememberCoroutineScope()
+ BaseSessionTabRow(
+ selectedIndex = pagerState.currentPage,
+ titles = titles,
+ onTabClick = { i ->
+ animationScope.launch { pagerState.animateScrollToPage(i) }
+ }
+ )
+}
+
+/** For ViewPager2 integration
+ * I created this initially as a workaround for GiphyTabs,
+ * which requires quite a lot of changes to be a fully composable screen.
+ *
+ * Also marked this for deletion once the screens are fully composable.
+ * */
+@Deprecated("To be deleted when screens that use viewpager2 are refactored to HorizontalPager")
+@Composable
+fun SessionTabRow(
+ selectedIndex: Int,
+ titles: List,
+ onTabSelected: (Int) -> Unit
+) {
+ BaseSessionTabRow(
+ selectedIndex = selectedIndex.coerceIn(0, titles.lastIndex),
+ titles = titles,
+ onTabClick = onTabSelected
+ )
+}
+
+/** Shared implementation */
+@Composable
+private fun BaseSessionTabRow(
+ selectedIndex: Int,
+ titles: List,
+ onTabClick: (Int) -> Unit
+) {
TabRow(
- containerColor = Color.Unspecified,
- selectedTabIndex = pagerState.currentPage,
- contentColor = LocalColors.current.text,
- indicator = { tabPositions ->
- TabRowDefaults.SecondaryIndicator(
- Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]),
- color = LocalColors.current.accent,
- height = LocalDimensions.current.indicatorHeight
- )
- },
- divider = { HorizontalDivider(color = LocalColors.current.borders) }
+ containerColor = Color.Unspecified,
+ selectedTabIndex = selectedIndex,
+ contentColor = LocalColors.current.text,
+ indicator = { tabPositions ->
+ TabRowDefaults.SecondaryIndicator(
+ Modifier.tabIndicatorOffset(tabPositions[selectedIndex]),
+ color = LocalColors.current.accent,
+ height = LocalDimensions.current.indicatorHeight
+ )
+ },
+ divider = { HorizontalDivider(color = LocalColors.current.borders) }
) {
- val animationScope = rememberCoroutineScope()
titles.forEachIndexed { i, it ->
Tab(
modifier = Modifier.heightIn(min = 48.dp),
- selected = i == pagerState.currentPage,
- onClick = { animationScope.launch { pagerState.animateScrollToPage(i) } },
+ selected = i == selectedIndex,
+ onClick = { onTabClick(i) },
selectedContentColor = LocalColors.current.text,
unselectedContentColor = LocalColors.current.text,
) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt
index 21f4f410e9..18746d21f5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Dimensions.kt
@@ -46,4 +46,11 @@ data class Dimensions(
val maxContentWidth: Dp = 410.dp,
val maxDialogWidth: Dp = 560.dp,
val maxTooltipWidth: Dp = 280.dp,
+
+ val minContentSizeSmall: Dp = 60.dp,
+ val maxContentSizeSmall: Dp = 420.dp,
+ val minContentSize: Dp = 80.dp,
+ val maxContentSize: Dp = 520.dp,
+ val minContentSizeMedium: Dp = 160.dp,
+ val maxContentSizeMedium: Dp = 620.dp
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt
index 9703ab1bf1..a65588d413 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewUtilities.kt
@@ -119,7 +119,7 @@ fun EditText.addTextChangedListener(listener: (String) -> Unit) {
@JvmOverloads
fun View.applySafeInsetsPaddings(
@InsetsType
- typeMask: Int = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime(),
+ typeMask: Int = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() or WindowInsetsCompat.Type.displayCutout() ,
consumeInsets: Boolean = true,
applyTop: Boolean = true,
applyBottom: Boolean = true,
@@ -154,7 +154,7 @@ fun View.applySafeInsetsMargins(
consumeInsets: Boolean = true,
@InsetsType
typeMask: Int = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime(),
- additionalInsets : Insets = Insets.NONE // for additional offsets
+ additionalInsets : Insets = Insets.NONE, // for additional offsets
) {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
// Get system bars insets
@@ -163,7 +163,12 @@ fun View.applySafeInsetsMargins(
// Update view margins to account for system bars
val lp = view.layoutParams as? MarginLayoutParams
if (lp != null) {
- lp.setMargins(additionalInsets.left + systemBarsInsets.left, additionalInsets.top + systemBarsInsets.top, additionalInsets.right + systemBarsInsets.right, additionalInsets.bottom + systemBarsInsets.bottom)
+ lp.setMargins(
+ additionalInsets.left + systemBarsInsets.left,
+ additionalInsets.top + systemBarsInsets.top,
+ additionalInsets.right + systemBarsInsets.right,
+ additionalInsets.bottom + systemBarsInsets.bottom
+ )
view.layoutParams = lp
if (consumeInsets) {
@@ -179,6 +184,28 @@ fun View.applySafeInsetsMargins(
}
}
+/**
+ * Independent helper for applying inset safe bottom margin to
+ * so we don't contradict [applySafeInsetsMargins] with apply* flags
+ */
+fun View.applyBottomInsetMargin(
+ @InsetsType typeMask: Int = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime(),
+ extraBottom: Int = 0,
+ consumeInsets: Boolean = true
+) {
+ ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
+ val insets = windowInsets.getInsets(typeMask)
+ val lp = view.layoutParams as? MarginLayoutParams
+
+ if (lp != null) {
+ lp.bottomMargin = insets.bottom + extraBottom
+ view.layoutParams = lp
+ }
+
+ if (consumeInsets) WindowInsetsCompat.CONSUMED else windowInsets
+ }
+}
+
/**
* Applies the system insets to a RecyclerView or ScrollView. The inset will apply as margin
* at the top and padding at the bottom. For ScrollView, the bottom insets will be applied to the first child.
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt
index b6a5285f6e..b94ea035f7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt
@@ -825,9 +825,9 @@ class CallManager @Inject constructor(
// apply the rotation to the streams
peerConnection?.setDeviceRotation(rotation)
- remoteRotationSink?.rotation = abs(rotation) // abs as we never need the remote video to be inverted
+ remoteRotationSink?.rotation =
+ abs(rotation) // abs as we never need the remote video to be inverted
}
-
fun handleWiredHeadsetChanged(present: Boolean) {
if (currentConnectionState in arrayOf(CallState.Connected,
CallState.LocalRing,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt
index a712a3b876..df89f1e94a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallViewModel.kt
@@ -89,10 +89,7 @@ class CallViewModel @Inject constructor(
get() = callManager.videoState
var deviceOrientation: Orientation = Orientation.UNKNOWN
- set(value) {
- field = value
- callManager.setDeviceOrientation(value)
- }
+ set(value) { field = value }
val currentCallState get() = callManager.currentCallState
@@ -238,6 +235,13 @@ class CallViewModel @Inject constructor(
fun hangUp() = rtcCallBridge.handleLocalHangup(null)
+ fun setDeviceOrientation(orientation: Orientation, autoRotateOn: Boolean) {
+ deviceOrientation = orientation
+
+ if(!autoRotateOn){
+ callManager.setDeviceOrientation(deviceOrientation)
+ }
+ }
data class CallState(
val callLabelTitle: String?,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/OrientationManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/OrientationManager.kt
index c16c9f93f5..83e0f9eefa 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/OrientationManager.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/OrientationManager.kt
@@ -44,12 +44,6 @@ class OrientationManager(private val context: Context): SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
- // if auto-rotate is off, bail and send UNKNOWN
- if (!isAutoRotateOn()) {
- _orientation.value = Orientation.UNKNOWN
- return
- }
-
// Get the quaternion from the rotation vector sensor
val quaternion = FloatArray(4)
SensorManager.getQuaternionFromVector(quaternion, event.values)
@@ -74,7 +68,7 @@ class OrientationManager(private val context: Context): SensorEventListener {
}
//Function to check if Android System Auto-rotate is on or off
- private fun isAutoRotateOn(): Boolean {
+ fun isAutoRotateOn(): Boolean {
return Settings.System.getInt(
context.contentResolver,
Settings.System.ACCELEROMETER_ROTATION, 0
diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallActivity.kt
index 105e3fdb24..c379ae5a9e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallActivity.kt
@@ -1,10 +1,13 @@
package org.thoughtcrime.securesms.webrtc
import android.Manifest
+import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.pm.ActivityInfo
import android.content.res.ColorStateList
+import android.content.res.Configuration
import android.graphics.Outline
import android.media.AudioManager
import android.os.Build
@@ -15,9 +18,12 @@ import android.view.ViewOutlineProvider
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.compose.runtime.collectAsState
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.IntentCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
+import androidx.transition.TransitionManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -79,6 +85,10 @@ class WebRtcCallActivity : ScreenLockActionBarActivity() {
*/
private var orientationManager = OrientationManager(this)
+ private val portraitConstraints = ConstraintSet()
+ private val landscapeConstraints = ConstraintSet()
+ private lateinit var rootConstraintLayout: ConstraintLayout
+
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
finish()
@@ -92,12 +102,20 @@ class WebRtcCallActivity : ScreenLockActionBarActivity() {
handleIntent(intent)
}
+ @SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
binding = ActivityWebrtcBinding.inflate(layoutInflater)
setContentView(binding.root)
+ rootConstraintLayout = binding.root
+
+ // 1) Portrait constraints: from a portrait layout
+ portraitConstraints.clone(this, R.layout.activity_webrtc_portrait_template)
+ // 2) Landscape constraints: cloned from a template XML
+ landscapeConstraints.clone(this, R.layout.activity_webrtc_landscape_template)
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
@@ -154,8 +172,11 @@ class WebRtcCallActivity : ScreenLockActionBarActivity() {
lifecycleScope.launch {
orientationManager.orientation.collect { orientation ->
- viewModel.deviceOrientation = orientation
- updateControlsRotation()
+ viewModel.setDeviceOrientation(orientation, orientationManager.isAutoRotateOn())
+ if(!orientationManager.isAutoRotateOn()){
+ // let system handle it
+ updateControlsRotation()
+ }
}
}
@@ -243,12 +264,13 @@ class WebRtcCallActivity : ScreenLockActionBarActivity() {
}
private fun updateControlsRotation() {
- with (binding) {
- val rotation = when(viewModel.deviceOrientation){
- Orientation.LANDSCAPE -> -90f
- Orientation.REVERSED_LANDSCAPE -> 90f
- else -> 0f
- }
+ with(binding) {
+ val rotation =
+ when (viewModel.deviceOrientation) {
+ Orientation.LANDSCAPE -> -90f
+ Orientation.REVERSED_LANDSCAPE -> 90f
+ else -> 0f
+ }
userAvatar.animate().cancel()
userAvatar.animate().rotation(rotation).start()
@@ -269,6 +291,9 @@ class WebRtcCallActivity : ScreenLockActionBarActivity() {
endCallButton.animate().cancel()
endCallButton.animate().rotation(rotation).start()
+
+ backArrow.animate().cancel()
+ backArrow.animate().rotation(rotation).start()
}
}
@@ -358,54 +383,58 @@ class WebRtcCallActivity : ScreenLockActionBarActivity() {
// handle video state
launch {
viewModel.videoState.collect { state ->
- binding.floatingRenderer.removeAllViews()
- binding.fullscreenRenderer.removeAllViews()
-
- // handle fullscreen video window
- if(state.showFullscreenVideo()){
- viewModel.fullscreenRenderer?.let { surfaceView ->
- binding.fullscreenRenderer.addView(surfaceView)
- binding.fullscreenRenderer.isVisible = true
- hideAvatar()
- }
- } else {
- binding.fullscreenRenderer.isVisible = false
- showAvatar(state.swapped)
- }
-
- // handle floating video window
- if(state.showFloatingVideo()){
- viewModel.floatingRenderer?.let { surfaceView ->
- binding.floatingRenderer.addView(surfaceView)
- binding.floatingRenderer.isVisible = true
- binding.swapViewIcon.bringToFront()
- }
- } else {
- binding.floatingRenderer.isVisible = false
- }
-
- // the floating video inset (empty or not) should be shown
- // the moment we have either of the video streams
- val showFloatingContainer = state.userVideoEnabled || state.remoteVideoEnabled
- binding.floatingRendererContainer.isVisible = showFloatingContainer
- binding.swapViewIcon.isVisible = showFloatingContainer
-
- // make sure to default to the contact's avatar if the floating container is not visible
- if (!showFloatingContainer) showAvatar(false)
-
- // handle buttons
- binding.enableCameraButton.isSelected = state.userVideoEnabled
- binding.switchCameraButton.isEnabled = state.userVideoEnabled
- binding.switchCameraButton.imageTintList =
- ColorStateList.valueOf(
- if(state.userVideoEnabled) buttonColorEnabled
- else buttonColorDisabled
- )
+ renderVideoState(state)
}
}
}
}
+ private fun renderVideoState(state : VideoState){
+ binding.floatingRenderer.removeAllViews()
+ binding.fullscreenRenderer.removeAllViews()
+
+ // handle fullscreen video window
+ if(state.showFullscreenVideo()){
+ viewModel.fullscreenRenderer?.let { surfaceView ->
+ binding.fullscreenRenderer.addView(surfaceView)
+ binding.fullscreenRenderer.isVisible = true
+ hideAvatar()
+ }
+ } else {
+ binding.fullscreenRenderer.isVisible = false
+ showAvatar(state.swapped)
+ }
+
+ // handle floating video window
+ if(state.showFloatingVideo()){
+ viewModel.floatingRenderer?.let { surfaceView ->
+ binding.floatingRenderer.addView(surfaceView)
+ binding.floatingRenderer.isVisible = true
+ binding.swapViewIcon.bringToFront()
+ }
+ } else {
+ binding.floatingRenderer.isVisible = false
+ }
+
+ // the floating video inset (empty or not) should be shown
+ // the moment we have either of the video streams
+ val showFloatingContainer = state.userVideoEnabled || state.remoteVideoEnabled
+ binding.floatingRendererContainer.isVisible = showFloatingContainer
+ binding.swapViewIcon.isVisible = showFloatingContainer
+
+ // make sure to default to the contact's avatar if the floating container is not visible
+ if (!showFloatingContainer) showAvatar(false)
+
+ // handle buttons
+ binding.enableCameraButton.isSelected = state.userVideoEnabled
+ binding.switchCameraButton.isEnabled = state.userVideoEnabled
+ binding.switchCameraButton.imageTintList =
+ ColorStateList.valueOf(
+ if(state.userVideoEnabled) buttonColorEnabled
+ else buttonColorDisabled
+ )
+ }
+
/**
* Shows the avatar image.
* If @showUserAvatar is true, the user's avatar is shown, otherwise the contact's avatar is shown.
@@ -426,4 +455,21 @@ class WebRtcCallActivity : ScreenLockActionBarActivity() {
binding.fullscreenRenderer.removeAllViews()
binding.floatingRenderer.removeAllViews()
}
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+
+ if (!::rootConstraintLayout.isInitialized) return
+
+ val isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE
+
+ if (isLandscape) {
+ landscapeConstraints.applyTo(rootConstraintLayout)
+ } else {
+ portraitConstraints.applyTo(rootConstraintLayout)
+ }
+
+ updateControls(viewModel.callState.value)
+ renderVideoState(viewModel.videoState.value)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/layout-land/activity_camerax.xml b/app/src/main/res/layout-land/activity_camerax.xml
new file mode 100644
index 0000000000..395f0b31a0
--- /dev/null
+++ b/app/src/main/res/layout-land/activity_camerax.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-land/activity_conversation_v2.xml b/app/src/main/res/layout-land/activity_conversation_v2.xml
new file mode 100644
index 0000000000..a52ed11166
--- /dev/null
+++ b/app/src/main/res/layout-land/activity_conversation_v2.xml
@@ -0,0 +1,331 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-land/activity_webrtc.xml b/app/src/main/res/layout-land/activity_webrtc.xml
new file mode 100644
index 0000000000..e6d1847e29
--- /dev/null
+++ b/app/src/main/res/layout-land/activity_webrtc.xml
@@ -0,0 +1,282 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-land/image_editor_hud.xml b/app/src/main/res/layout-land/image_editor_hud.xml
new file mode 100644
index 0000000000..e451c64d93
--- /dev/null
+++ b/app/src/main/res/layout-land/image_editor_hud.xml
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_camerax.xml b/app/src/main/res/layout/activity_camerax.xml
new file mode 100644
index 0000000000..5832556841
--- /dev/null
+++ b/app/src/main/res/layout/activity_camerax.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_camerax_landscape.xml b/app/src/main/res/layout/activity_camerax_landscape.xml
new file mode 100644
index 0000000000..6a8aeea90e
--- /dev/null
+++ b/app/src/main/res/layout/activity_camerax_landscape.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_camerax_portrait.xml b/app/src/main/res/layout/activity_camerax_portrait.xml
new file mode 100644
index 0000000000..bc1a08cc91
--- /dev/null
+++ b/app/src/main/res/layout/activity_camerax_portrait.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml
index 202a67a80a..b6ec3e1a83 100644
--- a/app/src/main/res/layout/activity_conversation_v2.xml
+++ b/app/src/main/res/layout/activity_conversation_v2.xml
@@ -1,312 +1,316 @@
-
-
-
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/conversationAppBar">
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ android:layout_height="20dp"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:background="@drawable/rounded_rectangle"
+ android:backgroundTint="?backgroundSecondary"
+ android:maxWidth="40dp"
+ android:paddingLeft="4dp"
+ android:paddingRight="4dp">
+
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
+ android:padding="@dimen/medium_spacing"
+ android:visibility="gone"
+ app:layout_constraintBottom_toTopOf="@+id/bottomWidgetBarrier"
+ app:layout_constraintTop_toBottomOf="@+id/messageRequestBar"
+ tools:visibility="visible">
+
+
-
+
-
+
-
+
+
+
+
+
+
+
+
-
-
-
-
+ android:layout="@layout/conversation_reaction_scrubber" />
-
diff --git a/app/src/main/res/layout/activity_webrtc.xml b/app/src/main/res/layout/activity_webrtc.xml
index 6af9ac9e20..ce050d06d8 100644
--- a/app/src/main/res/layout/activity_webrtc.xml
+++ b/app/src/main/res/layout/activity_webrtc.xml
@@ -31,6 +31,8 @@
app:layout_constraintVertical_bias="0.4"
android:layout_width="@dimen/extra_large_profile_picture_size"
android:layout_height="@dimen/extra_large_profile_picture_size"
+ app:layout_constraintWidth_min="@dimen/extra_large_profile_picture_size"
+ app:layout_constraintHeight_min="@dimen/extra_large_profile_picture_size"
android:visibility="gone"/>
+ android:layout_height="@dimen/extra_large_profile_picture_size"
+ app:layout_constraintWidth_min="@dimen/extra_large_profile_picture_size"
+ app:layout_constraintHeight_min="@dimen/extra_large_profile_picture_size"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_webrtc_portrait_template.xml b/app/src/main/res/layout/activity_webrtc_portrait_template.xml
new file mode 100644
index 0000000000..9afa79da15
--- /dev/null
+++ b/app/src/main/res/layout/activity_webrtc_portrait_template.xml
@@ -0,0 +1,279 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/camerax_fragment.xml b/app/src/main/res/layout/camerax_fragment.xml
deleted file mode 100644
index 80f32b041e..0000000000
--- a/app/src/main/res/layout/camerax_fragment.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml b/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml
index b4b15d9d50..b4a19bde9e 100644
--- a/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml
+++ b/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml
@@ -1,6 +1,5 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/giphy_activity.xml b/app/src/main/res/layout/giphy_activity.xml
index 6d884f0214..858818f0e7 100644
--- a/app/src/main/res/layout/giphy_activity.xml
+++ b/app/src/main/res/layout/giphy_activity.xml
@@ -23,13 +23,10 @@
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"/>
-
+
diff --git a/app/src/main/res/layout/giphy_fragment.xml b/app/src/main/res/layout/giphy_fragment.xml
index da2e83cd74..452407b717 100644
--- a/app/src/main/res/layout/giphy_fragment.xml
+++ b/app/src/main/res/layout/giphy_fragment.xml
@@ -1,9 +1,7 @@
-
+ android:layout_height="match_parent">
-
-
-
+ android:layout_height="match_parent"
+ android:clickable="false" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/image_editor_hud.xml b/app/src/main/res/layout/image_editor_hud.xml
index 54f5fbf777..27d8097c19 100644
--- a/app/src/main/res/layout/image_editor_hud.xml
+++ b/app/src/main/res/layout/image_editor_hud.xml
@@ -1,13 +1,12 @@
-
+ tools:background="@color/core_grey_60"
+ tools:parentTag="android.widget.LinearLayout">
+ android:src="@drawable/ic_trash_2"
+ app:tint="?android:textColorPrimary" />
+ android:src="@drawable/ic_rotate_ccw"
+ app:tint="?android:textColorPrimary" />
-
+ android:src="@drawable/ic_case_sensitive"
+ app:tint="?android:textColorPrimary" />
+
+ android:src="@drawable/ic_pencil"
+ app:tint="?android:textColorPrimary" />
+ android:src="@drawable/ic_brush"
+ app:tint="?android:textColorPrimary" />
+ android:src="@drawable/ic_emoji_custom"
+ app:tint="?android:textColorPrimary" />
+ android:src="@drawable/ic_crop"
+ app:tint="?android:textColorPrimary" />
+ android:src="@drawable/ic_circle_check"
+ app:tint="?android:textColorPrimary" />
@@ -138,42 +137,42 @@
android:id="@+id/scribble_crop_flip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:padding="8dp"
- app:tint="?android:textColorPrimary"
android:layout_gravity="center_vertical"
android:layout_marginBottom="8dp"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:padding="8dp"
android:src="@drawable/ic_flip_horizontal_2"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toEndOf="@+id/scribble_crop_rotate" />
+ app:layout_constraintStart_toEndOf="@+id/scribble_crop_rotate"
+ app:tint="?android:textColorPrimary" />
+ app:layout_constraintStart_toStartOf="parent"
+ app:tint="?android:textColorPrimary" />
+ app:layout_constraintEnd_toEndOf="parent"
+ app:tint="?android:textColorPrimary" />
diff --git a/app/src/main/res/layout/media_preview_activity.xml b/app/src/main/res/layout/media_preview_activity.xml
index f475507d35..e741b1bc61 100644
--- a/app/src/main/res/layout/media_preview_activity.xml
+++ b/app/src/main/res/layout/media_preview_activity.xml
@@ -18,7 +18,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBottom_toTopOf="@+id/media_preview_album_rail_container"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:clickable="true" />
diff --git a/app/src/main/res/layout/mediasend_activity.xml b/app/src/main/res/layout/mediasend_activity.xml
index 2a439c9b5b..ef4ee1ad81 100644
--- a/app/src/main/res/layout/mediasend_activity.xml
+++ b/app/src/main/res/layout/mediasend_activity.xml
@@ -15,49 +15,9 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
-
-
-
-
-
-
-
-
+
diff --git a/app/src/main/res/layout/view_conversation.xml b/app/src/main/res/layout/view_conversation.xml
index 64e4073606..fc943c31c6 100644
--- a/app/src/main/res/layout/view_conversation.xml
+++ b/app/src/main/res/layout/view_conversation.xml
@@ -23,7 +23,8 @@
android:layout_marginBottom="@dimen/medium_spacing" />
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 1f3f2a743e..e29c6d5ecb 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -21,6 +21,7 @@
26dp
46dp
128dp
+ 80dp
190dp
14dp
1dp
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1b46fff643..22cd1d56dc 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,7 +1,7 @@
[versions]
androidMinSdkVersion = "26"
-androidTargetSdkVersion = "35"
-androidCompileSdkVersion = "35"
+androidTargetSdkVersion = "36"
+androidCompileSdkVersion = "36"
accompanistPermissionsVersion = "0.37.3"
activityKtxVersion = "1.10.1"
androidImageCropperVersion = "4.7.0"