From 4e31b4e7a777de0483cd74c18dce84c0f1f85a1e Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 1 Sep 2025 13:40:18 +0800 Subject: [PATCH 001/103] removed portrait lock for most activities, retained for WebRtcCallActivity --- app/src/main/AndroidManifest.xml | 48 +++++++------------------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a97d1610f8..44d553edab 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"/> + @@ -256,7 +231,6 @@ Date: Mon, 1 Sep 2025 16:40:44 +0800 Subject: [PATCH 002/103] Initial landscape, compact, and flip compatibility --- .../securesms/onboarding/landing/Landing.kt | 252 ++++++++++++------ 1 file changed, 175 insertions(+), 77 deletions(-) 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 d66338ca3f..65b25ae89e 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 @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.onboarding.landing +import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween @@ -9,11 +10,19 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight +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.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -29,11 +38,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign 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 com.google.common.math.LinearTransformation.horizontal import com.squareup.phrase.Phrase import kotlinx.coroutines.delay import network.loki.messenger.R @@ -71,6 +83,9 @@ internal fun LandingScreen( openTerms: () -> Unit, openPrivacyPolicy: () -> Unit, ) { + val cfg: Configuration = LocalConfiguration.current + val useTwoPane = shouldUseTwoPane(cfg) + var count by remember { mutableStateOf(0) } val listState = rememberLazyListState() @@ -97,103 +112,153 @@ internal fun LandingScreen( LaunchedEffect(Unit) { delay(500.milliseconds) - while(count < MESSAGES.size) { + while (count < MESSAGES.size) { count += 1 listState.animateScrollToItem(0.coerceAtLeast((count - 1))) delay(1500L) } } - Column { - Column(modifier = Modifier - .weight(1f) - .padding(horizontal = LocalDimensions.current.mediumSpacing) + if (useTwoPane) { + // WIDE / LANDSCAPE: side-by-side + Row( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = LocalDimensions.current.mediumSpacing) + .windowInsetsPadding(WindowInsets.systemBars), + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.mediumSpacing) ) { - Spacer(modifier = Modifier.weight(1f)) - Text( - stringResource(R.string.onboardingBubblePrivacyInYourPocket), - modifier = Modifier.align(Alignment.CenterHorizontally), - style = LocalType.current.h4, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - - LazyColumn( - state = listState, + // LEFT: title + messages + Column( modifier = Modifier - .heightIn(min = 200.dp) - .fillMaxWidth() - .weight(3f), - verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) + .weight(1f) + .fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.spacing) ) { - items( - MESSAGES.take(count), - key = { it.stringId } - ) { item -> - // Perform string substitution only in the bubbles that require it - val bubbleTxt = when (item.stringId) { - R.string.onboardingBubbleWelcomeToSession -> { - Phrase.from(stringResource(item.stringId)) - .put(APP_NAME_KEY, stringResource(R.string.app_name)) - .put(EMOJI_KEY, "\uD83D\uDC4B") // this hardcoded emoji might be moved to NonTranslatableConstants eventually - .format().toString() - } - R.string.onboardingBubbleSessionIsEngineered -> { - Phrase.from(stringResource(item.stringId)) - .put(APP_NAME_KEY, stringResource(R.string.app_name)) - .format().toString() - } - R.string.onboardingBubbleCreatingAnAccountIsEasy -> { - Phrase.from(stringResource(item.stringId)) - .put(EMOJI_KEY, "\uD83D\uDC47") // this hardcoded emoji might be moved to NonTranslatableConstants eventually - .format().toString() - } - else -> { - stringResource(item.stringId) - } - } + Text( + stringResource(R.string.onboardingBubblePrivacyInYourPocket), + style = LocalType.current.h4, + textAlign = TextAlign.Start + ) + + Spacer(modifier = Modifier.weight(1f)) - AnimateMessageText( - bubbleTxt, - item.isOutgoing - ) + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) + ) { + items(MESSAGES.take(count), key = { it.stringId }) { item -> + val bubbleTxt = resolveBubbleText(item.stringId) + AnimateMessageText(bubbleTxt, item.isOutgoing) + } } } - Spacer(modifier = Modifier.weight(1f)) - } - - Column(modifier = Modifier.padding(horizontal = LocalDimensions.current.xlargeSpacing)) { - AccentFillButton( - text = stringResource(R.string.onboardingAccountCreate), - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally) - .qaTag(R.string.AccessibilityId_onboardingAccountCreate), - onClick = createAccount + // RIGHT: actions rail + ActionsColumn( + createAccount = createAccount, + loadAccount = loadAccount, + openDialog = { isUrlDialogVisible = true }, + maxWidth = 360.dp, + modifier = Modifier.align(Alignment.CenterVertically) ) - Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) - AccentOutlineButton( - stringResource(R.string.onboardingAccountExists), + } + } else { + // COMPACT / DEFAULT: your current single-column + Column(modifier = Modifier.padding(horizontal = LocalDimensions.current.mediumSpacing)) { + Column( modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally) - .qaTag(R.string.AccessibilityId_onboardingAccountExists), - onClick = loadAccount - ) - BorderlessHtmlButton( - textId = R.string.onboardingTosPrivacy, + .weight(1f) + ) { + Spacer(modifier = Modifier.weight(1f)) + + Text( + stringResource(R.string.onboardingBubblePrivacyInYourPocket), + modifier = Modifier.align(Alignment.CenterHorizontally), + style = LocalType.current.h4, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + LazyColumn( + state = listState, + modifier = Modifier + .heightIn(min = 200.dp) + .fillMaxWidth() + .weight(3f), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) + ) { + items( + MESSAGES.take(count), + key = { it.stringId } + ) { item -> + val bubbleTxt = resolveBubbleText(item.stringId) + AnimateMessageText(bubbleTxt, item.isOutgoing) + } + } + + Spacer(modifier = Modifier.weight(1f)) + } + + ActionsColumn( + createAccount = createAccount, + loadAccount = loadAccount, + openDialog = { isUrlDialogVisible = true }, + maxWidth = 360.dp, modifier = Modifier - .fillMaxWidth() .align(Alignment.CenterHorizontally) - .qaTag(R.string.AccessibilityId_urlOpenBrowser), - onClick = { isUrlDialogVisible = true } + .fillMaxWidth() // NEW: align within Column scope ) - Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing)) } } } +@Composable +private fun ActionsColumn( + createAccount: () -> Unit, + loadAccount: () -> Unit, + openDialog: () -> Unit, + modifier: Modifier = Modifier, + maxWidth: Dp? = null +) { + val base = modifier + .imePadding() + + val widthMod = if (maxWidth != null) base.widthIn(max = maxWidth) else base + + Column( + modifier = widthMod, + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) + ) { + AccentFillButton( + text = stringResource(R.string.onboardingAccountCreate), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .qaTag(R.string.AccessibilityId_onboardingAccountCreate), + onClick = createAccount + ) + AccentOutlineButton( + stringResource(R.string.onboardingAccountExists), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .qaTag(R.string.AccessibilityId_onboardingAccountExists), + onClick = loadAccount + ) + BorderlessHtmlButton( + textId = R.string.onboardingTosPrivacy, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .qaTag(R.string.AccessibilityId_urlOpenBrowser), + onClick = openDialog + ) + Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing)) + } +} + @Composable private fun AnimateMessageText(text: String, isOutgoing: Boolean, modifier: Modifier = Modifier) { var visible by remember { mutableStateOf(false) } @@ -214,7 +279,7 @@ private fun AnimateMessageText(text: String, isOutgoing: Boolean, modifier: Modi @Composable private fun MessageText(text: String, isOutgoing: Boolean, modifier: Modifier) { - Box(modifier = modifier then Modifier.fillMaxWidth()) { + Box(modifier = modifier.fillMaxWidth()) { MessageText( text, color = if (isOutgoing) LocalColors.current.accent else LocalColors.current.backgroundBubbleReceived, @@ -232,7 +297,8 @@ private fun MessageText( textColor: Color = Color.Unspecified ) { Box( - modifier = modifier.fillMaxWidth(0.666f) + modifier = modifier + .fillMaxWidth(0.666f) .background(color = color, shape = MaterialTheme.shapes.small) ) { Text( @@ -258,3 +324,35 @@ private val MESSAGES = listOf( TextData(R.string.onboardingBubbleNoPhoneNumber), TextData(R.string.onboardingBubbleCreatingAnAccountIsEasy, isOutgoing = true) ) + +// helper for substitutions +@Composable +private fun resolveBubbleText(@StringRes id: Int): String { + return when (id) { + R.string.onboardingBubbleWelcomeToSession -> + Phrase.from(stringResource(id)) + .put(APP_NAME_KEY, stringResource(R.string.app_name)) + .put(EMOJI_KEY, "👋") + .format().toString() + + R.string.onboardingBubbleSessionIsEngineered -> + Phrase.from(stringResource(id)) + .put(APP_NAME_KEY, stringResource(R.string.app_name)) + .format().toString() + + R.string.onboardingBubbleCreatingAnAccountIsEasy -> + Phrase.from(stringResource(id)) + .put(EMOJI_KEY, "👇") + .format().toString() + + else -> stringResource(id) + } +} + +// landscape/wide switch logic using the real platform Configuration +private fun shouldUseTwoPane(configuration: Configuration): Boolean { + val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val widthDp = configuration.screenWidthDp + // Favor two-pane when landscape AND reasonably wide, or whenever width >= 600dp. + return widthDp >= 600 || (isLandscape && widthDp >= 480) +} From c85a724416aa4786fd90c4ce123e162edddc826a Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 1 Sep 2025 16:52:34 +0800 Subject: [PATCH 003/103] Landing screen initial landscape compat with flip, tablet and phone --- .../securesms/onboarding/landing/Landing.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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 65b25ae89e..51adc57545 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 @@ -160,8 +160,8 @@ internal fun LandingScreen( createAccount = createAccount, loadAccount = loadAccount, openDialog = { isUrlDialogVisible = true }, - maxWidth = 360.dp, - modifier = Modifier.align(Alignment.CenterVertically) +// maxWidth = 360.dp, + modifier = Modifier.align(Alignment.CenterVertically).weight(1f) ) } } else { @@ -351,8 +351,13 @@ private fun resolveBubbleText(@StringRes id: Int): String { // landscape/wide switch logic using the real platform Configuration private fun shouldUseTwoPane(configuration: Configuration): Boolean { +// val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE +// val widthDp = configuration.screenWidthDp +// // Favor two-pane when landscape AND reasonably wide, or whenever width >= 600dp. +// return widthDp >= 600 || (isLandscape && widthDp >= 480) + + val w = configuration.screenWidthDp val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - val widthDp = configuration.screenWidthDp - // Favor two-pane when landscape AND reasonably wide, or whenever width >= 600dp. - return widthDp >= 600 || (isLandscape && widthDp >= 480) + // Two-pane when: landscape & ≥480dp (phones/flip), or portrait but truly wide (≥840dp) + return (isLandscape && w >= 480) || (w >= 840) } From 1f494a2bda56a7fba86635fc539971db6c4ef329 Mon Sep 17 00:00:00 2001 From: jbsession Date: Thu, 11 Sep 2025 13:06:26 +0800 Subject: [PATCH 004/103] QR code activity landscape --- .../securesms/preferences/QRCodeActivity.kt | 107 ++++++++++++++---- 1 file changed, 87 insertions(+), 20 deletions(-) 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 3585a51fa6..333de04ffd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -1,19 +1,29 @@ package org.thoughtcrime.securesms.preferences import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP +import android.content.res.Configuration 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.layout.widthIn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -44,6 +54,7 @@ class QRCodeActivity : ScreenLockActionBarActivity() { private val errors = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) @@ -89,11 +100,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) @@ -105,25 +122,75 @@ private fun Tabs(accountId: String, errors: Flow, onScan: (String) -> Un @Composable fun QrPage(string: String) { - Column( - modifier = Modifier - .background(LocalColors.current.background) - .padding(horizontal = LocalDimensions.current.mediumSpacing) - .fillMaxSize() - ) { - QrImage( - string = string, + val cfg = LocalConfiguration.current + val isTwoPane = shouldUseTwoPane(cfg) + + if(isTwoPane){ + BoxWithConstraints( modifier = Modifier - .padding(top = LocalDimensions.current.mediumSpacing, bottom = LocalDimensions.current.xsSpacing) - .qaTag(R.string.AccessibilityId_qrCode), - icon = R.drawable.session - ) + .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 = if (maxWidth < maxHeight) maxWidth else maxHeight + val qrSide = (shortest * 0.72f).coerceIn(160.dp, 520.dp) + + Column( + modifier = Modifier + .align(Alignment.Center), // vertical + horizontal centering +// .widthIn(max = 640.dp), // this is optional. Maybe we can use for tablets. + 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 + ) + } + } + }else{ + Column( + modifier = Modifier + .background(LocalColors.current.background) + .padding(horizontal = LocalDimensions.current.mediumSpacing) + .fillMaxSize() + ) { + QrImage( + string = string, + modifier = Modifier + .padding( + top = LocalDimensions.current.mediumSpacing, + bottom = LocalDimensions.current.xsSpacing + ) + .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 - ) + Text( + text = stringResource(R.string.accountIdYoursDescription), + color = LocalColors.current.textSecondary, + textAlign = TextAlign.Center, + style = LocalType.current.small + ) + } } } + +// TODO: make util or helper for this +private fun shouldUseTwoPane(cfg: Configuration): Boolean { + val w = cfg.screenWidthDp + val isLandscape = cfg.orientation == Configuration.ORIENTATION_LANDSCAPE + // Two-pane only when: landscape & ≥480dp, or portrait but truly wide (≥840dp) + return (isLandscape && w >= 480) || (w >= 840) +} From ed86d27647d831ecc59428ece39968452b06f72a Mon Sep 17 00:00:00 2001 From: jbsession Date: Thu, 11 Sep 2025 14:35:14 +0800 Subject: [PATCH 005/103] Initial layout for Start Conversation sheet --- .../home/StartConversation.kt | 238 ++++++++++++------ 1 file changed, 163 insertions(+), 75 deletions(-) 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 5fe2aaba9c..d7202551fd 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 android.content.res.Configuration 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.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +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 @@ -15,9 +22,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource @@ -49,7 +58,8 @@ internal fun StartConversationScreen( navigateTo: (StartConversationDestination) -> Unit, onClose: () -> Unit, ) { - val context = LocalContext.current + val cfg = LocalConfiguration.current + val isTwoPane = shouldUseTwoPane(cfg) Column(modifier = Modifier.background( LocalColors.current.backgroundSecondary, @@ -65,90 +75,160 @@ 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) - ItemButton( - text = annotatedStringResource(newMessageTitleTxt), - textStyle = LocalType.current.xl, - iconRes = R.drawable.ic_message_square, - 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, - 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, - modifier = Modifier.qaTag(R.string.AccessibilityId_communityJoin), - onClick = { - navigateTo(StartConversationDestination.JoinCommunity) + + if(isTwoPane){ + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = LocalDimensions.current.spacing), + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.spacing) + ) { + // Left: independently scrollable actions list + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + ) { + ActionList(navigateTo = navigateTo) } - ) - Divider( - paddingValues = PaddingValues( - start = dividerIndent, - end = LocalDimensions.current.smallSpacing + + // Right: QR panel, vertically centered, with square sizing + QrPanel( + accountId = accountId, + modifier = Modifier + .widthIn(max = 420.dp) + .align(Alignment.CenterVertically) ) - ) - ItemButton( - text = annotatedStringResource(R.string.sessionInviteAFriend), - textStyle = LocalType.current.xl, - iconRes = R.drawable.ic_user_round_plus, - modifier = Modifier.qaTag(R.string.AccessibilityId_sessionInviteAFriendButton), - onClick = { - navigateTo(StartConversationDestination.InviteFriend) - } - ) + } + } else { Column( - modifier = Modifier - .padding(horizontal = LocalDimensions.current.spacing) - .padding(top = LocalDimensions.current.spacing) - .padding(bottom = LocalDimensions.current.spacing) + modifier = Modifier.verticalScroll(rememberScrollState()) ) { - 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 - ) + ActionList(navigateTo = navigateTo) + 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 + ) + } } } } } } +@Composable +private fun QrPanel( + accountId: String, + modifier: Modifier = Modifier +) { + BoxWithConstraints( + modifier = modifier + ) { + // Fit the QR inside available height/width of the right rail + val shortest: Dp = if (maxWidth < maxHeight) maxWidth else maxHeight + val qrSide = (shortest * 0.72f).coerceIn(160.dp, 520.dp) + + Column( + modifier = Modifier.widthIn(max = 420.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + 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 = Modifier + .size(qrSide) + .qaTag(R.string.AccessibilityId_qrCode), + 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) + ItemButton( + text = annotatedStringResource(newMessageTitleTxt), + textStyle = LocalType.current.xl, + iconRes = R.drawable.ic_message_square, + 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, + 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, + 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, + modifier = Modifier.qaTag(R.string.AccessibilityId_sessionInviteAFriendButton), + onClick = { + navigateTo(StartConversationDestination.InviteFriend) + } + ) + +} + @Preview @Composable private fun PreviewStartConversationScreen( @@ -162,3 +242,11 @@ private fun PreviewStartConversationScreen( ) } } + +// TODO: make util or helper for this +private fun shouldUseTwoPane(cfg: Configuration): Boolean { + val w = cfg.screenWidthDp + val isLandscape = cfg.orientation == Configuration.ORIENTATION_LANDSCAPE + // Two-pane only when: landscape & ≥480dp, or portrait but truly wide (≥840dp) + return (isLandscape && w >= 480) || (w >= 840) +} From 29fe922dbe03937aa4d7401dcd4114f2d63ae61f Mon Sep 17 00:00:00 2001 From: jbsession Date: Thu, 11 Sep 2025 15:02:53 +0800 Subject: [PATCH 006/103] Added Adaptive layout helper --- .../securesms/ui/adaptive/AdaptiveLayout.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/ui/adaptive/AdaptiveLayout.kt 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..cd44809689 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/adaptive/AdaptiveLayout.kt @@ -0,0 +1,63 @@ +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.runtime.remember +import androidx.compose.ui.platform.LocalConfiguration + +object AdaptiveBreakpoints { + const val TWO_PANE_LANDSCAPE_MIN_WIDTH_DP = 480 + const val TWO_PANE_PORTRAIT_MIN_WIDTH_DP = 840 +} + +fun shouldUseTwoPane(widthDp: Int, isLandscape: Boolean): Boolean { + return (isLandscape && widthDp >= AdaptiveBreakpoints.TWO_PANE_LANDSCAPE_MIN_WIDTH_DP) || + (widthDp >= AdaptiveBreakpoints.TWO_PANE_PORTRAIT_MIN_WIDTH_DP) +} + +/** + * Convenience helper that returns only the two-pane decision. + * Equivalent to `rememberAdaptiveInfo().isTwoPane`, but cheaper when you only need the boolean. + */ +@SuppressLint("ConfigurationScreenWidthHeight") +@Composable +fun rememberTwoPane(): Boolean { + val configuration = LocalConfiguration.current + return remember(configuration.orientation, configuration.screenWidthDp) { + val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + shouldUseTwoPane(configuration.screenWidthDp, isLandscape) + } +} + +/** + * 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, + val isTwoPane: Boolean +) + +/** + * Returns a stable snapshot of the window/adaptive state for the current composition. + * + * We can use this when multiple layout decisions depend on size/orientation (e.g., choose Row vs Column, + * cap bubble width, scale a QR square). + * + * Prefer `rememberTwoPane()` if you only need the boolean. + */ +@SuppressLint("ConfigurationScreenWidthHeight") +@Composable +fun rememberAdaptiveInfo(): AdaptiveInfo { + val configuration = LocalConfiguration.current + val landscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val twoPane = remember(configuration.orientation, configuration.screenWidthDp) { + shouldUseTwoPane(configuration.screenWidthDp, landscape) + } + return AdaptiveInfo(configuration.screenWidthDp, configuration.screenHeightDp, landscape, twoPane) +} \ No newline at end of file From 0e007e914098119003c36d3cdf54b401554f9d04 Mon Sep 17 00:00:00 2001 From: jbsession Date: Thu, 11 Sep 2025 15:15:28 +0800 Subject: [PATCH 007/103] Revert "Initial landscape, compact, and flip compatibility" This reverts commit 20a5fa9862a09f4be5763ac824d6a68cb2b39dde. --- .../securesms/onboarding/landing/Landing.kt | 257 ++++++------------ 1 file changed, 77 insertions(+), 180 deletions(-) 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 51adc57545..d66338ca3f 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 @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.onboarding.landing -import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween @@ -10,19 +9,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxHeight -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.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -38,14 +29,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign 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 com.google.common.math.LinearTransformation.horizontal import com.squareup.phrase.Phrase import kotlinx.coroutines.delay import network.loki.messenger.R @@ -83,9 +71,6 @@ internal fun LandingScreen( openTerms: () -> Unit, openPrivacyPolicy: () -> Unit, ) { - val cfg: Configuration = LocalConfiguration.current - val useTwoPane = shouldUseTwoPane(cfg) - var count by remember { mutableStateOf(0) } val listState = rememberLazyListState() @@ -112,153 +97,103 @@ internal fun LandingScreen( LaunchedEffect(Unit) { delay(500.milliseconds) - while (count < MESSAGES.size) { + while(count < MESSAGES.size) { count += 1 listState.animateScrollToItem(0.coerceAtLeast((count - 1))) delay(1500L) } } - if (useTwoPane) { - // WIDE / LANDSCAPE: side-by-side - Row( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = LocalDimensions.current.mediumSpacing) - .windowInsetsPadding(WindowInsets.systemBars), - horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.mediumSpacing) + Column { + Column(modifier = Modifier + .weight(1f) + .padding(horizontal = LocalDimensions.current.mediumSpacing) ) { - // LEFT: title + messages - Column( + Spacer(modifier = Modifier.weight(1f)) + Text( + stringResource(R.string.onboardingBubblePrivacyInYourPocket), + modifier = Modifier.align(Alignment.CenterHorizontally), + style = LocalType.current.h4, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + LazyColumn( + state = listState, modifier = Modifier - .weight(1f) - .fillMaxHeight(), - verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.spacing) + .heightIn(min = 200.dp) + .fillMaxWidth() + .weight(3f), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) ) { - Text( - stringResource(R.string.onboardingBubblePrivacyInYourPocket), - style = LocalType.current.h4, - textAlign = TextAlign.Start - ) - - Spacer(modifier = Modifier.weight(1f)) - - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) - ) { - items(MESSAGES.take(count), key = { it.stringId }) { item -> - val bubbleTxt = resolveBubbleText(item.stringId) - AnimateMessageText(bubbleTxt, item.isOutgoing) + items( + MESSAGES.take(count), + key = { it.stringId } + ) { item -> + // Perform string substitution only in the bubbles that require it + val bubbleTxt = when (item.stringId) { + R.string.onboardingBubbleWelcomeToSession -> { + Phrase.from(stringResource(item.stringId)) + .put(APP_NAME_KEY, stringResource(R.string.app_name)) + .put(EMOJI_KEY, "\uD83D\uDC4B") // this hardcoded emoji might be moved to NonTranslatableConstants eventually + .format().toString() + } + R.string.onboardingBubbleSessionIsEngineered -> { + Phrase.from(stringResource(item.stringId)) + .put(APP_NAME_KEY, stringResource(R.string.app_name)) + .format().toString() + } + R.string.onboardingBubbleCreatingAnAccountIsEasy -> { + Phrase.from(stringResource(item.stringId)) + .put(EMOJI_KEY, "\uD83D\uDC47") // this hardcoded emoji might be moved to NonTranslatableConstants eventually + .format().toString() + } + else -> { + stringResource(item.stringId) + } } + + AnimateMessageText( + bubbleTxt, + item.isOutgoing + ) } } - // RIGHT: actions rail - ActionsColumn( - createAccount = createAccount, - loadAccount = loadAccount, - openDialog = { isUrlDialogVisible = true }, -// maxWidth = 360.dp, - modifier = Modifier.align(Alignment.CenterVertically).weight(1f) - ) + Spacer(modifier = Modifier.weight(1f)) } - } else { - // COMPACT / DEFAULT: your current single-column - Column(modifier = Modifier.padding(horizontal = LocalDimensions.current.mediumSpacing)) { - Column( - modifier = Modifier - .weight(1f) - ) { - Spacer(modifier = Modifier.weight(1f)) - - Text( - stringResource(R.string.onboardingBubblePrivacyInYourPocket), - modifier = Modifier.align(Alignment.CenterHorizontally), - style = LocalType.current.h4, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) - LazyColumn( - state = listState, - modifier = Modifier - .heightIn(min = 200.dp) - .fillMaxWidth() - .weight(3f), - verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) - ) { - items( - MESSAGES.take(count), - key = { it.stringId } - ) { item -> - val bubbleTxt = resolveBubbleText(item.stringId) - AnimateMessageText(bubbleTxt, item.isOutgoing) - } - } - - Spacer(modifier = Modifier.weight(1f)) - } - - ActionsColumn( - createAccount = createAccount, - loadAccount = loadAccount, - openDialog = { isUrlDialogVisible = true }, - maxWidth = 360.dp, + Column(modifier = Modifier.padding(horizontal = LocalDimensions.current.xlargeSpacing)) { + AccentFillButton( + text = stringResource(R.string.onboardingAccountCreate), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .qaTag(R.string.AccessibilityId_onboardingAccountCreate), + onClick = createAccount + ) + Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) + AccentOutlineButton( + stringResource(R.string.onboardingAccountExists), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .qaTag(R.string.AccessibilityId_onboardingAccountExists), + onClick = loadAccount + ) + BorderlessHtmlButton( + textId = R.string.onboardingTosPrivacy, modifier = Modifier + .fillMaxWidth() .align(Alignment.CenterHorizontally) - .fillMaxWidth() // NEW: align within Column scope + .qaTag(R.string.AccessibilityId_urlOpenBrowser), + onClick = { isUrlDialogVisible = true } ) + Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing)) } } } -@Composable -private fun ActionsColumn( - createAccount: () -> Unit, - loadAccount: () -> Unit, - openDialog: () -> Unit, - modifier: Modifier = Modifier, - maxWidth: Dp? = null -) { - val base = modifier - .imePadding() - - val widthMod = if (maxWidth != null) base.widthIn(max = maxWidth) else base - - Column( - modifier = widthMod, - verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) - ) { - AccentFillButton( - text = stringResource(R.string.onboardingAccountCreate), - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally) - .qaTag(R.string.AccessibilityId_onboardingAccountCreate), - onClick = createAccount - ) - AccentOutlineButton( - stringResource(R.string.onboardingAccountExists), - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally) - .qaTag(R.string.AccessibilityId_onboardingAccountExists), - onClick = loadAccount - ) - BorderlessHtmlButton( - textId = R.string.onboardingTosPrivacy, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally) - .qaTag(R.string.AccessibilityId_urlOpenBrowser), - onClick = openDialog - ) - Spacer(modifier = Modifier.height(LocalDimensions.current.xxsSpacing)) - } -} - @Composable private fun AnimateMessageText(text: String, isOutgoing: Boolean, modifier: Modifier = Modifier) { var visible by remember { mutableStateOf(false) } @@ -279,7 +214,7 @@ private fun AnimateMessageText(text: String, isOutgoing: Boolean, modifier: Modi @Composable private fun MessageText(text: String, isOutgoing: Boolean, modifier: Modifier) { - Box(modifier = modifier.fillMaxWidth()) { + Box(modifier = modifier then Modifier.fillMaxWidth()) { MessageText( text, color = if (isOutgoing) LocalColors.current.accent else LocalColors.current.backgroundBubbleReceived, @@ -297,8 +232,7 @@ private fun MessageText( textColor: Color = Color.Unspecified ) { Box( - modifier = modifier - .fillMaxWidth(0.666f) + modifier = modifier.fillMaxWidth(0.666f) .background(color = color, shape = MaterialTheme.shapes.small) ) { Text( @@ -324,40 +258,3 @@ private val MESSAGES = listOf( TextData(R.string.onboardingBubbleNoPhoneNumber), TextData(R.string.onboardingBubbleCreatingAnAccountIsEasy, isOutgoing = true) ) - -// helper for substitutions -@Composable -private fun resolveBubbleText(@StringRes id: Int): String { - return when (id) { - R.string.onboardingBubbleWelcomeToSession -> - Phrase.from(stringResource(id)) - .put(APP_NAME_KEY, stringResource(R.string.app_name)) - .put(EMOJI_KEY, "👋") - .format().toString() - - R.string.onboardingBubbleSessionIsEngineered -> - Phrase.from(stringResource(id)) - .put(APP_NAME_KEY, stringResource(R.string.app_name)) - .format().toString() - - R.string.onboardingBubbleCreatingAnAccountIsEasy -> - Phrase.from(stringResource(id)) - .put(EMOJI_KEY, "👇") - .format().toString() - - else -> stringResource(id) - } -} - -// landscape/wide switch logic using the real platform Configuration -private fun shouldUseTwoPane(configuration: Configuration): Boolean { -// val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE -// val widthDp = configuration.screenWidthDp -// // Favor two-pane when landscape AND reasonably wide, or whenever width >= 600dp. -// return widthDp >= 600 || (isLandscape && widthDp >= 480) - - val w = configuration.screenWidthDp - val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - // Two-pane when: landscape & ≥480dp (phones/flip), or portrait but truly wide (≥840dp) - return (isLandscape && w >= 480) || (w >= 840) -} From ecaf47ad3fddee2628590d1c4ade3e06e8077827 Mon Sep 17 00:00:00 2001 From: jbsession Date: Thu, 11 Sep 2025 15:31:52 +0800 Subject: [PATCH 008/103] Use adaptive layout --- .../startconversation/home/StartConversation.kt | 14 ++------------ .../securesms/preferences/QRCodeActivity.kt | 16 ++-------------- 2 files changed, 4 insertions(+), 26 deletions(-) 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 d7202551fd..4da7ca71ad 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,6 +1,5 @@ package org.thoughtcrime.securesms.home.startconversation.home -import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints @@ -26,7 +25,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource @@ -39,6 +37,7 @@ 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.rememberTwoPane import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon import org.thoughtcrime.securesms.ui.components.BasicAppBar import org.thoughtcrime.securesms.ui.components.QrImage @@ -58,8 +57,7 @@ internal fun StartConversationScreen( navigateTo: (StartConversationDestination) -> Unit, onClose: () -> Unit, ) { - val cfg = LocalConfiguration.current - val isTwoPane = shouldUseTwoPane(cfg) + val isTwoPane = rememberTwoPane() Column(modifier = Modifier.background( LocalColors.current.backgroundSecondary, @@ -242,11 +240,3 @@ private fun PreviewStartConversationScreen( ) } } - -// TODO: make util or helper for this -private fun shouldUseTwoPane(cfg: Configuration): Boolean { - val w = cfg.screenWidthDp - val isLandscape = cfg.orientation == Configuration.ORIENTATION_LANDSCAPE - // Two-pane only when: landscape & ≥480dp, or portrait but truly wide (≥840dp) - return (isLandscape && w >= 480) || (w >= 840) -} 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 333de04ffd..5b1ebbff70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.preferences import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP -import android.content.res.Configuration import android.os.Bundle import android.view.View import androidx.compose.foundation.background @@ -12,14 +11,12 @@ 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.layout.widthIn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp @@ -35,6 +32,7 @@ import org.session.libsignal.utilities.PublicKeyValidation import org.thoughtcrime.securesms.ScreenLockActionBarActivity import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.permissions.Permissions +import org.thoughtcrime.securesms.ui.adaptive.rememberTwoPane import org.thoughtcrime.securesms.ui.components.QRScannerScreen import org.thoughtcrime.securesms.ui.components.QrImage import org.thoughtcrime.securesms.ui.components.SessionTabRow @@ -122,8 +120,7 @@ private fun Tabs(accountId: String, errors: Flow, onScan: (String) -> Un @Composable fun QrPage(string: String) { - val cfg = LocalConfiguration.current - val isTwoPane = shouldUseTwoPane(cfg) + val isTwoPane = rememberTwoPane() if(isTwoPane){ BoxWithConstraints( @@ -139,7 +136,6 @@ fun QrPage(string: String) { Column( modifier = Modifier .align(Alignment.Center), // vertical + horizontal centering -// .widthIn(max = 640.dp), // this is optional. Maybe we can use for tablets. horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) ) { @@ -186,11 +182,3 @@ fun QrPage(string: String) { } } } - -// TODO: make util or helper for this -private fun shouldUseTwoPane(cfg: Configuration): Boolean { - val w = cfg.screenWidthDp - val isLandscape = cfg.orientation == Configuration.ORIENTATION_LANDSCAPE - // Two-pane only when: landscape & ≥480dp, or portrait but truly wide (≥840dp) - return (isLandscape && w >= 480) || (w >= 840) -} From 21fcff0ae4b1c3f535c018652c0dd5160c394110 Mon Sep 17 00:00:00 2001 From: jbsession Date: Thu, 11 Sep 2025 15:43:23 +0800 Subject: [PATCH 009/103] made sholdUseTwoPane private --- .../org/thoughtcrime/securesms/ui/adaptive/AdaptiveLayout.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index cd44809689..40f8e871de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/adaptive/AdaptiveLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/adaptive/AdaptiveLayout.kt @@ -12,7 +12,7 @@ object AdaptiveBreakpoints { const val TWO_PANE_PORTRAIT_MIN_WIDTH_DP = 840 } -fun shouldUseTwoPane(widthDp: Int, isLandscape: Boolean): Boolean { +private fun shouldUseTwoPane(widthDp: Int, isLandscape: Boolean): Boolean { return (isLandscape && widthDp >= AdaptiveBreakpoints.TWO_PANE_LANDSCAPE_MIN_WIDTH_DP) || (widthDp >= AdaptiveBreakpoints.TWO_PANE_PORTRAIT_MIN_WIDTH_DP) } From 955dec0e38e2ef5469e81ead30c2064de8d15d0f Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 12 Sep 2025 09:28:22 +0800 Subject: [PATCH 010/103] removed padding for back button --- app/src/main/res/layout/session_logo_action_bar_content.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/session_logo_action_bar_content.xml b/app/src/main/res/layout/session_logo_action_bar_content.xml index 22398083ce..93ca0b4d1d 100644 --- a/app/src/main/res/layout/session_logo_action_bar_content.xml +++ b/app/src/main/res/layout/session_logo_action_bar_content.xml @@ -17,7 +17,7 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_margin="@dimen/small_spacing" - android:padding="@dimen/small_spacing" + android:paddingHorizontal="@dimen/small_spacing" android:background="@drawable/circle_touch_highlight_background" android:clickable="true" android:focusable="true"> From 368f97b2ded670ab0d6488112f1404e06cf15a9e Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 12 Sep 2025 12:48:32 +0800 Subject: [PATCH 011/103] GiphyActivity initial compose --- .../securesms/giph/ui/GiphyActivity.java | 18 ++- .../giph/ui/compose/GiphyTabsCompose.kt | 114 ++++++++++++++++++ app/src/main/res/layout/giphy_activity.xml | 19 +-- 3 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/giph/ui/compose/GiphyTabsCompose.kt 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 index 67c27f5032..cc407bbc05 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java @@ -20,10 +20,12 @@ import org.session.libsession.utilities.NonTranslatableStringConstants; import org.thoughtcrime.securesms.ScreenLockActionBarActivity; import org.session.libsignal.utilities.Log; +import org.thoughtcrime.securesms.giph.ui.compose.GiphyTabsCompose; import org.thoughtcrime.securesms.providers.BlobUtils; import org.session.libsession.utilities.ViewUtil; import java.io.IOException; +import java.util.Arrays; import java.util.concurrent.ExecutionException; import network.loki.messenger.R; @@ -77,9 +79,19 @@ private void initializeResources() { 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(); +// new TabLayoutMediator(binding.tabLayout, binding.giphyPager, (tab, position) -> { +// tab.setText(position == 0 ? NonTranslatableStringConstants.GIF : getString(R.string.stickers)); +// }).attach(); + + // NEW: Compose tabs controlling existing ViewPager2 + GiphyTabsCompose.attachComposeTabs( + binding.composeTabs, + binding.giphyPager, + Arrays.asList( + org.session.libsession.utilities.NonTranslatableStringConstants.GIF, + getString(network.loki.messenger.R.string.stickers) + ) + ); } @Override 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..e0e5022be3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/compose/GiphyTabsCompose.kt @@ -0,0 +1,114 @@ +@file:JvmName("GiphyTabsCompose") // lets Java call attachComposeTabs(...) +package org.thoughtcrime.securesms.giph.ui.compose + +import android.util.TypedValue +import androidx.annotation.AttrRes +import androidx.appcompat.R +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.viewpager2.widget.ViewPager2 + +fun attachComposeTabs( + composeView: ComposeView, + pager: ViewPager2, + titles: List +) { + composeView.setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + + composeView.setContent { + var selectedIndex by remember { mutableIntStateOf(pager.currentItem) } + + // Keep pager -> tabs selection in sync. + DisposableEffect(pager) { + val cb = object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { selectedIndex = position } + } + pager.registerOnPageChangeCallback(cb) + onDispose { pager.unregisterOnPageChangeCallback(cb) } + } + + TabsRow( + titles = titles, + selectedIndex = selectedIndex, + onSelect = { index -> + if (index != pager.currentItem) pager.setCurrentItem(index, true) + selectedIndex = index + } + ) + } +} + +@Composable +private fun TabsRow( + titles: List, + selectedIndex: Int, + onSelect: (Int) -> Unit +) { + val indicatorColor = colorFromAttrOr( + R.attr.colorAccent, + MaterialTheme.colorScheme.secondary + ) + // NEW: text color that matches your toolbar’s foreground + val appBarTextColor = colorFromAttrOr( + android.R.attr.textColorPrimary, + MaterialTheme.colorScheme.onPrimary + ) + + TabRow( + selectedTabIndex = selectedIndex, + modifier = Modifier.fillMaxWidth(), + containerColor = Color.Transparent, + contentColor = appBarTextColor, + divider = {}, + indicator = { positions -> + TabRowDefaults.SecondaryIndicator( + modifier = Modifier.tabIndicatorOffset(positions[selectedIndex]), + color = indicatorColor, + height = 2.dp + ) + } + ) { + titles.forEachIndexed { index, title -> + Tab( + selected = selectedIndex == index, + onClick = { onSelect(index) }, + selectedContentColor = appBarTextColor, + unselectedContentColor = appBarTextColor.copy(alpha = 0.70f), + text = { Text(title) } + ) + } + } +} + +@Composable +private fun colorFromAttrOr(@AttrRes attrResId: Int, fallback: Color): Color { + val context = LocalContext.current + val tv = TypedValue() + val resolved = context.theme.resolveAttribute(attrResId, tv, /* resolveRefs = */ true) + if (!resolved) return fallback + return if (tv.resourceId != 0) { + Color(ContextCompat.getColor(context, tv.resourceId)) + } else { + Color(tv.data) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/giphy_activity.xml b/app/src/main/res/layout/giphy_activity.xml index 6d884f0214..fb06a63fdc 100644 --- a/app/src/main/res/layout/giphy_activity.xml +++ b/app/src/main/res/layout/giphy_activity.xml @@ -23,13 +23,18 @@ android:background="?attr/colorPrimary" app:layout_scrollFlags="scroll|enterAlways"/> - + + + + + + + + + From f837831c1d6a7421208e07c9f8a63e78ccb7f364 Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 12 Sep 2025 13:17:05 +0800 Subject: [PATCH 012/103] GiphyFragment compose --- .../securesms/giph/ui/GiphyFragment.java | 18 +++++-- .../giph/ui/compose/GiphyFragmentCompose.kt | 48 +++++++++++++++++++ app/src/main/res/layout/giphy_fragment.xml | 36 +++++++++----- 3 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/giph/ui/compose/GiphyFragmentCompose.kt 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 index 753e43b833..ed75154884 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java @@ -4,6 +4,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.compose.ui.platform.ComposeView; import androidx.fragment.app.Fragment; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; @@ -14,10 +15,10 @@ 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.ui.compose.GiphyFragmentCompose; import org.thoughtcrime.securesms.giph.util.InfiniteScrollListener; import com.bumptech.glide.Glide; import org.session.libsession.utilities.TextSecurePreferences; @@ -32,10 +33,10 @@ public abstract class GiphyFragment extends Fragment implements LoaderManager.Lo private static final String TAG = GiphyFragment.class.getSimpleName(); - private GiphyAdapter giphyAdapter; - private RecyclerView recyclerView; - private View loadingProgress; - private TextView noResultsView; + private GiphyAdapter giphyAdapter; + private RecyclerView recyclerView; + private ComposeView loadingProgress; + private ComposeView noResultsView; protected String searchString; private Boolean pendingGridLayout = null; @@ -47,6 +48,13 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, this.loadingProgress = ViewUtil.findById(container, R.id.loading_progress); this.noResultsView = ViewUtil.findById(container, R.id.no_results); + if (loadingProgress != null) { + GiphyFragmentCompose.setGiphyLoading(loadingProgress); + } + if (noResultsView != null) { + GiphyFragmentCompose.setGiphyNoResults(noResultsView, R.string.searchMatchesNone); + } + // Now that views are ready, apply the searchString if it's set applySearchStringToUI(); 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..c098a259f0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/compose/GiphyFragmentCompose.kt @@ -0,0 +1,48 @@ +@file:JvmName("GiphyFragmentCompose") // lets Java call GiphyFragmentCompose.setGiphyLoading(...) +package org.thoughtcrime.securesms.giph.ui.compose + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Box +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.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 network.loki.messenger.R + +/** Called from Java to set Compose content for the loading view. */ +fun setGiphyLoading(composeView: ComposeView) { + composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + composeView.setContent { GiphyLoading() } +} + +/** Called from Java to set Compose content for the empty/no-results view. */ +fun setGiphyNoResults(composeView: ComposeView, @StringRes messageId: Int = R.string.searchMatchesNone) { + composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + composeView.setContent { GiphyNoResults(messageId) } +} + +@Composable +private fun GiphyLoading() { + Box( + modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), + contentAlignment = Alignment.Center + ) { CircularProgressIndicator() } +} + +@Composable +private fun GiphyNoResults(@StringRes messageId: Int) { + Box( + modifier = Modifier.fillMaxWidth().padding(24.dp), + contentAlignment = Alignment.Center + ) { + Text(text = stringResource(messageId), style = MaterialTheme.typography.bodyMedium) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/giphy_fragment.xml b/app/src/main/res/layout/giphy_fragment.xml index da2e83cd74..45eac52513 100644 --- a/app/src/main/res/layout/giphy_fragment.xml +++ b/app/src/main/res/layout/giphy_fragment.xml @@ -11,24 +11,36 @@ android:layout_height="match_parent" android:scrollbars="vertical"/> - + + + + + + + + + + + + + + + + + + + + + android:visibility="gone" /> - + android:visibility="gone" /> \ No newline at end of file From 968d55597452e73132dd35d90735ac9cef615d4c Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 12 Sep 2025 13:56:02 +0800 Subject: [PATCH 013/103] GiphyFragment initial kotlin --- .../securesms/giph/ui/GiphyActivity.java | 4 +- .../securesms/giph/ui/GiphyFragment.java | 156 ----------------- .../securesms/giph/ui/GiphyFragment.kt | 162 ++++++++++++++++++ .../securesms/giph/ui/GiphyGifFragment.java | 2 +- .../giph/ui/GiphyStickerFragment.java | 2 +- 5 files changed, 166 insertions(+), 160 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt 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 index cc407bbc05..3beb5d1d6e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java @@ -96,8 +96,8 @@ private void initializeResources() { @Override public void onFilterChanged(String filter) { - this.gifFragment.setSearchString(filter); - this.stickerFragment.setSearchString(filter); + this.gifFragment.setNewSearchString(filter); + this.stickerFragment.setNewSearchString(filter); } @Override 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 ed75154884..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java +++ /dev/null @@ -1,156 +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.compose.ui.platform.ComposeView; -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 org.thoughtcrime.securesms.giph.model.GiphyImage; -import org.thoughtcrime.securesms.giph.net.GiphyLoader; -import org.thoughtcrime.securesms.giph.ui.compose.GiphyFragmentCompose; -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 ComposeView loadingProgress; - private ComposeView 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); - - if (loadingProgress != null) { - GiphyFragmentCompose.setGiphyLoading(loadingProgress); - } - if (noResultsView != null) { - GiphyFragmentCompose.setGiphyNoResults(noResultsView, R.string.searchMatchesNone); - } - - // 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..be68565e27 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt @@ -0,0 +1,162 @@ +package org.thoughtcrime.securesms.giph.ui + +import android.annotation.SuppressLint +import android.os.AsyncTask +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +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 com.bumptech.glide.Glide +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.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 loadingProgress: View + private lateinit var noResultsView: View + + // 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 { + // ViewUtil.inflate expects a non-null parent: assert non-null like Java did. + val root = inflater.inflate(R.layout.giphy_fragment, container,false) as ViewGroup + + recyclerView = ViewUtil.findById(root, R.id.giphy_list) + loadingProgress = ViewUtil.findById(root, R.id.loading_progress) + noResultsView = ViewUtil.findById(root, R.id.no_results) + + // Apply search (if already set) and default/pending layout + applySearchStringToUI() + + pendingGridLayout?.let { + setLayoutManager(it) + pendingGridLayout = null + } ?: run { + setLayoutManager(TextSecurePreferences.isGifSearchInGridLayout(requireContext())) + } + + return root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + giphyAdapter = GiphyAdapter(requireActivity(), Glide.with(this), LinkedList()) + // IMPORTANT: Set listener via anonymous object to avoid exposing package-private ViewHolder type. + giphyAdapter.setListener(object : GiphyAdapter.OnItemClickListener { + override fun onClick(viewHolder: GiphyAdapter.GiphyViewHolder) { + (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 + ) { + loadingProgress.visibility = View.GONE + noResultsView.visibility = if (data.isEmpty()) View.VISIBLE else View.GONE + giphyAdapter.setImages(data.toMutableList()) + } + + override fun onLoaderReset(loader: Loader>) { + noResultsView.visibility = View.GONE + giphyAdapter.setImages(mutableListOf()) + } + + // ---- Public API used by the Activity ---- + fun setLayoutManager(gridLayout: Boolean) { + if (this::recyclerView.isInitialized) { + recyclerView.layoutManager = createLayoutManager(gridLayout) + } else { + pendingGridLayout = gridLayout + } + } + + fun setNewSearchString(newSearch: String?) { + searchString = newSearch + if (this::noResultsView.isInitialized) { + applySearchStringToUI() + } + } + + // ---- UI helpers ---- + private fun createLayoutManager(gridLayout: Boolean): RecyclerView.LayoutManager { + return if (gridLayout) { + // Keep the same default span (2). If you added a resource override, read it here. + StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL).apply { + gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS + } + } else { + LinearLayoutManager(requireActivity()) + } + } + + private fun applySearchStringToUI() { + noResultsView.visibility = View.GONE + LoaderManager.getInstance(this).restartLoader(0, null, this) + } + + // ---- Infinite scroll ---- + private inner class GiphyScrollListener : InfiniteScrollListener() { + @SuppressLint("StaticFieldLeak") + override fun onLoadMore(currentPage: Int) { + @Suppress("UNCHECKED_CAST") + val loader = LoaderManager.getInstance(this@GiphyFragment) + .getLoader>(0) as? GiphyLoader ?: return + + object : AsyncTask>() { + override fun doInBackground(vararg p: Void?): List { + return loader.loadPage(currentPage * GiphyLoader.PAGE_SIZE) as List + } + + override fun onPostExecute(images: List) { + giphyAdapter.addImages(images.toMutableList()) + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + } + } +} \ 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 index 4c8ad66dcf..1a3e792fbd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java @@ -14,7 +14,7 @@ public class GiphyGifFragment extends GiphyFragment { @Override public @NonNull Loader> onCreateLoader(int id, Bundle args) { - return new GiphyGifLoader(getActivity(), searchString); + return new GiphyGifLoader(getActivity(), getSearchString()); } } 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 index 0b838b4718..0fbbd617d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.java @@ -13,6 +13,6 @@ public class GiphyStickerFragment extends GiphyFragment { @Override public @NonNull Loader> onCreateLoader(int id, Bundle args) { - return new GiphyStickerLoader(getActivity(), searchString); + return new GiphyStickerLoader(getActivity(), getSearchString()); } } From 73cd00001f6f05942f7ca5cb79a4a735dd1bf963 Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 12 Sep 2025 13:56:49 +0800 Subject: [PATCH 014/103] cleanup --- app/src/main/res/layout/giphy_activity.xml | 8 ------- app/src/main/res/layout/giphy_fragment.xml | 27 ++-------------------- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/app/src/main/res/layout/giphy_activity.xml b/app/src/main/res/layout/giphy_activity.xml index fb06a63fdc..858818f0e7 100644 --- a/app/src/main/res/layout/giphy_activity.xml +++ b/app/src/main/res/layout/giphy_activity.xml @@ -23,14 +23,6 @@ android:background="?attr/colorPrimary" app:layout_scrollFlags="scroll|enterAlways"/> - - - - - - - - - + android:layout_height="match_parent"> - - - - - - - - - - - - - - - - - - - - - Date: Fri, 12 Sep 2025 14:10:47 +0800 Subject: [PATCH 015/103] GiphyFragment to kotlin --- .../securesms/giph/ui/GiphyFragment.kt | 19 +++++++++++++------ .../giph/ui/compose/GiphyFragmentCompose.kt | 1 - 2 files changed, 13 insertions(+), 7 deletions(-) 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 index be68565e27..a5b5b3b59a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt @@ -7,6 +7,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.loader.app.LoaderManager import androidx.loader.content.Loader @@ -20,6 +21,8 @@ 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.setGiphyLoading +import org.thoughtcrime.securesms.giph.ui.compose.setGiphyNoResults import org.thoughtcrime.securesms.giph.util.InfiniteScrollListener import java.util.LinkedList import java.util.List @@ -34,8 +37,8 @@ abstract class GiphyFragment : private lateinit var giphyAdapter: GiphyAdapter private lateinit var recyclerView: RecyclerView - private lateinit var loadingProgress: View - private lateinit var noResultsView: View + private lateinit var loadingProgress: ComposeView + private lateinit var noResultsView: ComposeView // Set by toolbar filter via Activity var searchString: String? = null @@ -49,11 +52,15 @@ abstract class GiphyFragment : savedInstanceState: Bundle? ): View { // ViewUtil.inflate expects a non-null parent: assert non-null like Java did. - val root = inflater.inflate(R.layout.giphy_fragment, container,false) as ViewGroup + val root = inflater.inflate(R.layout.giphy_fragment, container, false) as ViewGroup - recyclerView = ViewUtil.findById(root, R.id.giphy_list) - loadingProgress = ViewUtil.findById(root, R.id.loading_progress) - noResultsView = ViewUtil.findById(root, R.id.no_results) + // Todo: Make compose + recyclerView = root.findViewById(R.id.giphy_list) + loadingProgress = root.findViewById(R.id.loading_progress) + noResultsView = root.findViewById(R.id.no_results) + + setGiphyLoading(loadingProgress) + setGiphyNoResults(noResultsView, R.string.searchMatchesNone) // Apply search (if already set) and default/pending layout applySearchStringToUI() 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 index c098a259f0..f53959e161 100644 --- 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 @@ -1,4 +1,3 @@ -@file:JvmName("GiphyFragmentCompose") // lets Java call GiphyFragmentCompose.setGiphyLoading(...) package org.thoughtcrime.securesms.giph.ui.compose import androidx.annotation.StringRes From 93baba230f6299c136ec5c17be34da0cd5602847 Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 12 Sep 2025 14:18:46 +0800 Subject: [PATCH 016/103] Cleanup --- .../securesms/giph/ui/GiphyFragment.kt | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) 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 index a5b5b3b59a..185bf018d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt @@ -51,7 +51,6 @@ abstract class GiphyFragment : container: ViewGroup?, // nullable per Fragment API savedInstanceState: Bundle? ): View { - // ViewUtil.inflate expects a non-null parent: assert non-null like Java did. val root = inflater.inflate(R.layout.giphy_fragment, container, false) as ViewGroup // Todo: Make compose @@ -62,7 +61,6 @@ abstract class GiphyFragment : setGiphyLoading(loadingProgress) setGiphyNoResults(noResultsView, R.string.searchMatchesNone) - // Apply search (if already set) and default/pending layout applySearchStringToUI() pendingGridLayout?.let { @@ -79,12 +77,11 @@ abstract class GiphyFragment : super.onViewCreated(view, savedInstanceState) giphyAdapter = GiphyAdapter(requireActivity(), Glide.with(this), LinkedList()) - // IMPORTANT: Set listener via anonymous object to avoid exposing package-private ViewHolder type. - giphyAdapter.setListener(object : GiphyAdapter.OnItemClickListener { - override fun onClick(viewHolder: GiphyAdapter.GiphyViewHolder) { - (activity as? GiphyAdapter.OnItemClickListener)?.onClick(viewHolder) - } - }) + giphyAdapter.setListener { viewHolder -> + (activity as? GiphyAdapter.OnItemClickListener)?.onClick( + viewHolder + ) + } recyclerView.itemAnimator = DefaultItemAnimator() recyclerView.adapter = giphyAdapter @@ -114,7 +111,6 @@ abstract class GiphyFragment : giphyAdapter.setImages(mutableListOf()) } - // ---- Public API used by the Activity ---- fun setLayoutManager(gridLayout: Boolean) { if (this::recyclerView.isInitialized) { recyclerView.layoutManager = createLayoutManager(gridLayout) @@ -130,11 +126,12 @@ abstract class GiphyFragment : } } - // ---- UI helpers ---- private fun createLayoutManager(gridLayout: Boolean): RecyclerView.LayoutManager { return if (gridLayout) { - // Keep the same default span (2). If you added a resource override, read it here. - StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL).apply { + StaggeredGridLayoutManager( + 2, + StaggeredGridLayoutManager.VERTICAL + ).apply { gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS } } else { @@ -147,7 +144,7 @@ abstract class GiphyFragment : LoaderManager.getInstance(this).restartLoader(0, null, this) } - // ---- Infinite scroll ---- + // Infinite scroll private inner class GiphyScrollListener : InfiniteScrollListener() { @SuppressLint("StaticFieldLeak") override fun onLoadMore(currentPage: Int) { From 009413069c6367ae6bbf297003d844b616eb2405 Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 12 Sep 2025 14:23:42 +0800 Subject: [PATCH 017/103] Converted async task to coroutine --- .../securesms/giph/ui/GiphyFragment.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 index 185bf018d4..cae05e90cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyFragment.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import android.widget.TextView 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 @@ -16,6 +17,9 @@ 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.launch +import kotlinx.coroutines.withContext import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.ViewUtil @@ -146,21 +150,17 @@ abstract class GiphyFragment : // Infinite scroll private inner class GiphyScrollListener : InfiniteScrollListener() { - @SuppressLint("StaticFieldLeak") override fun onLoadMore(currentPage: Int) { @Suppress("UNCHECKED_CAST") val loader = LoaderManager.getInstance(this@GiphyFragment) .getLoader>(0) as? GiphyLoader ?: return - object : AsyncTask>() { - override fun doInBackground(vararg p: Void?): List { - return loader.loadPage(currentPage * GiphyLoader.PAGE_SIZE) as List + viewLifecycleOwner.lifecycleScope.launch { + val images = withContext(Dispatchers.IO) { + loader.loadPage(currentPage * GiphyLoader.PAGE_SIZE) } - - override fun onPostExecute(images: List) { - giphyAdapter.addImages(images.toMutableList()) - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + giphyAdapter.addImages(images.toMutableList()) + } } } } \ No newline at end of file From 28fc0d3a9688d18189478c4f1ba63824ddff8fbd Mon Sep 17 00:00:00 2001 From: jbsession Date: Fri, 12 Sep 2025 14:34:11 +0800 Subject: [PATCH 018/103] Giphy fragment and sticket to kotlin --- .../securesms/giph/ui/GiphyGifFragment.java | 20 ----------------- .../securesms/giph/ui/GiphyGifFragment.kt | 21 ++++++++++++++++++ .../giph/ui/GiphyStickerFragment.java | 18 --------------- .../securesms/giph/ui/GiphyStickerFragment.kt | 22 +++++++++++++++++++ 4 files changed, 43 insertions(+), 38 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.kt 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 1a3e792fbd..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(), getSearchString()); - } - -} 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 0fbbd617d8..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(), getSearchString()); - } -} 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 From ea6c10d19ef1302ef8247d3441f932b3305d6be8 Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 22 Sep 2025 13:21:03 +0800 Subject: [PATCH 019/103] Separate compose fun --- .../securesms/preferences/QRCodeActivity.kt | 97 +++++++++++-------- 1 file changed, 57 insertions(+), 40 deletions(-) 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 5b1ebbff70..ba7d806932 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -50,7 +50,10 @@ 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) { @@ -88,7 +91,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) } @@ -122,53 +129,63 @@ private fun Tabs(accountId: String, errors: Flow, onScan: (String) -> Un fun QrPage(string: String) { val isTwoPane = rememberTwoPane() - if(isTwoPane){ - 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 = if (maxWidth < maxHeight) maxWidth else maxHeight - val qrSide = (shortest * 0.72f).coerceIn(160.dp, 520.dp) + if (isTwoPane) { + TwoPaneContent(string) + } else { + PortraitContent(string) + } +} - Column( - modifier = Modifier - .align(Alignment.Center), // 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 +@Composable +fun PortraitContent(string: String) { + Column( + modifier = Modifier + .background(LocalColors.current.background) + .padding(horizontal = LocalDimensions.current.mediumSpacing) + .fillMaxSize() + ) { + QrImage( + string = string, + modifier = Modifier + .padding( + top = LocalDimensions.current.mediumSpacing, + bottom = LocalDimensions.current.xsSpacing ) + .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 + ) + } +} + +@Composable +fun TwoPaneContent(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 = if (maxWidth < maxHeight) maxWidth else maxHeight + val qrSide = (shortest * 0.72f).coerceIn(160.dp, 520.dp) - Text( - text = stringResource(R.string.accountIdYoursDescription), - color = LocalColors.current.textSecondary, - textAlign = TextAlign.Center, - style = LocalType.current.small - ) - } - } - }else{ Column( modifier = Modifier - .background(LocalColors.current.background) - .padding(horizontal = LocalDimensions.current.mediumSpacing) - .fillMaxSize() + .align(Alignment.Center), // vertical + horizontal centering + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) ) { QrImage( string = string, modifier = Modifier - .padding( - top = LocalDimensions.current.mediumSpacing, - bottom = LocalDimensions.current.xsSpacing - ) + .size(qrSide) .qaTag(R.string.AccessibilityId_qrCode), icon = R.drawable.session ) From 5e0f1e5debd34a01b22bc3e0932ec044230a2220 Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 22 Sep 2025 13:23:40 +0800 Subject: [PATCH 020/103] Start conversation cleanup --- .../home/StartConversation.kt | 133 ++++++++++-------- .../securesms/preferences/QRCodeActivity.kt | 4 +- 2 files changed, 79 insertions(+), 58 deletions(-) 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 4da7ca71ad..27b95a9fdb 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 @@ -59,10 +59,15 @@ internal fun StartConversationScreen( ) { val isTwoPane = rememberTwoPane() - 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 @@ -73,62 +78,77 @@ internal fun StartConversationScreen( modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()), color = LocalColors.current.backgroundSecondary ) { - - if(isTwoPane){ - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = LocalDimensions.current.spacing), - horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.spacing) - ) { - // Left: independently scrollable actions list - Column( - modifier = Modifier - .weight(1f) - .verticalScroll(rememberScrollState()) - ) { - ActionList(navigateTo = navigateTo) - } - - // Right: QR panel, vertically centered, with square sizing - QrPanel( - accountId = accountId, - modifier = Modifier - .widthIn(max = 420.dp) - .align(Alignment.CenterVertically) - ) - } + if (isTwoPane) { + TwoPaneContent(accountId, navigateTo) } else { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - ActionList(navigateTo = navigateTo) - 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 - ) - } - } + PortraitContent(accountId, navigateTo) } } } } +@Composable +private fun PortraitContent( + accountId: String, + navigateTo: (StartConversationDestination) -> Unit +) { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + ActionList(navigateTo = navigateTo) + 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 + ) + } + } +} + +@Composable +private fun TwoPaneContent( + accountId: String, + navigateTo: (StartConversationDestination) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = LocalDimensions.current.spacing), + horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.spacing) + ) { + // Left: independently scrollable actions list + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + ) { + ActionList(navigateTo = navigateTo) + } + + // Right: QR panel, vertically centered, with square sizing + QrPanel( + accountId = accountId, + modifier = Modifier + .widthIn(max = 420.dp) + .align(Alignment.CenterVertically) + ) + } +} + @Composable private fun QrPanel( accountId: String, @@ -168,8 +188,9 @@ private fun QrPanel( 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 dividerIndent: Dp = + LocalDimensions.current.itemButtonIconSpacing + 2 * LocalDimensions.current.smallSpacing + val newMessageTitleTxt: String = context.resources.getQuantityString(R.plurals.messageNew, 1, 1) ItemButton( text = annotatedStringResource(newMessageTitleTxt), textStyle = LocalType.current.xl, 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 ba7d806932..b6c14fc609 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -137,7 +137,7 @@ fun QrPage(string: String) { } @Composable -fun PortraitContent(string: String) { +private fun PortraitContent(string: String) { Column( modifier = Modifier .background(LocalColors.current.background) @@ -165,7 +165,7 @@ fun PortraitContent(string: String) { } @Composable -fun TwoPaneContent(string: String) { +private fun TwoPaneContent(string: String) { BoxWithConstraints( modifier = Modifier .fillMaxSize() From 4734623c4cc30fbf493b2d637acec67c71ef89c1 Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 22 Sep 2025 13:30:01 +0800 Subject: [PATCH 021/103] cleanup --- .../java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java | 2 +- .../thoughtcrime/securesms/giph/ui/compose/GiphyTabsCompose.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 3beb5d1d6e..f166657010 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java @@ -83,7 +83,7 @@ private void initializeResources() { // tab.setText(position == 0 ? NonTranslatableStringConstants.GIF : getString(R.string.stickers)); // }).attach(); - // NEW: Compose tabs controlling existing ViewPager2 + // Compose tabs controlling existing ViewPager2 GiphyTabsCompose.attachComposeTabs( binding.composeTabs, binding.giphyPager, 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 index e0e5022be3..d717b735a0 100644 --- 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 @@ -68,7 +68,7 @@ private fun TabsRow( R.attr.colorAccent, MaterialTheme.colorScheme.secondary ) - // NEW: text color that matches your toolbar’s foreground + // text color that matches your toolbar’s foreground val appBarTextColor = colorFromAttrOr( android.R.attr.textColorPrimary, MaterialTheme.colorScheme.onPrimary From cc679cd2f58d6db60dc052ba06d749ac78e0f342 Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 22 Sep 2025 13:32:03 +0800 Subject: [PATCH 022/103] Cleanup --- .../securesms/giph/ui/compose/GiphyTabsCompose.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index d717b735a0..e18380a761 100644 --- 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 @@ -41,7 +41,9 @@ fun attachComposeTabs( // Keep pager -> tabs selection in sync. DisposableEffect(pager) { val cb = object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { selectedIndex = position } + override fun onPageSelected(position: Int) { + selectedIndex = position + } } pager.registerOnPageChangeCallback(cb) onDispose { pager.unregisterOnPageChangeCallback(cb) } @@ -104,7 +106,7 @@ private fun TabsRow( private fun colorFromAttrOr(@AttrRes attrResId: Int, fallback: Color): Color { val context = LocalContext.current val tv = TypedValue() - val resolved = context.theme.resolveAttribute(attrResId, tv, /* resolveRefs = */ true) + val resolved = context.theme.resolveAttribute(attrResId, tv, true) if (!resolved) return fallback return if (tv.resourceId != 0) { Color(ContextCompat.getColor(context, tv.resourceId)) From 3cc3ec039af9a95d17dc77b91262f574398d799a Mon Sep 17 00:00:00 2001 From: jbsession Date: Thu, 2 Oct 2025 10:31:55 +0800 Subject: [PATCH 023/103] notif landscape fix --- .../MessageNotifications.kt | 87 ++++++++++++++----- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt index 3c5bb0eb75..b4b6d2dd42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt @@ -1,5 +1,8 @@ package org.thoughtcrime.securesms.onboarding.messagenotifications +import android.R.attr.checked +import android.R.attr.onClick +import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -9,11 +12,14 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -75,29 +81,70 @@ internal fun MessageNotificationsScreen( Spacer(Modifier.height(LocalDimensions.current.spacing)) } - NotificationRadioButton( - R.string.notificationsFastMode, - if(BuildConfig.FLAVOR == "huawei") R.string.notificationsFastModeDescriptionHuawei - else R.string.notificationsFastModeDescription, - modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsFastMode), - tag = R.string.recommended, - checked = state.pushEnabled, - onClick = { setEnabled(true) } - ) +// NotificationRadioButton( +// R.string.notificationsFastMode, +// if(BuildConfig.FLAVOR == "huawei") R.string.notificationsFastModeDescriptionHuawei +// else R.string.notificationsFastModeDescription, +// modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsFastMode), +// tag = R.string.recommended, +// checked = state.pushEnabled, +// onClick = { setEnabled(true) } +// ) +// +// // spacing between buttons is provided by ripple/downstate of NotificationRadioButton +// +// val explanationTxt = Phrase.from(stringResource(R.string.notificationsSlowModeDescription)) +// .put(APP_NAME_KEY, stringResource(R.string.app_name)) +// .format().toString() +// +// NotificationRadioButton( +// stringResource(R.string.notificationsSlowMode), +// explanationTxt, +// modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsSlowMode), +// checked = state.pushDisabled, +// onClick = { setEnabled(false) } +// ) + + val isLandscape = + LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE + val optionsScroll = rememberScrollState() + + val optionsModifier = if (isLandscape) { + Modifier + .weight(1f) + .verticalScroll(optionsScroll) + } else { + Modifier + } - // spacing between buttons is provided by ripple/downstate of NotificationRadioButton + Column(modifier = optionsModifier) { + NotificationRadioButton( + R.string.notificationsFastMode, + if (BuildConfig.FLAVOR == "huawei") + R.string.notificationsFastModeDescriptionHuawei + else + R.string.notificationsFastModeDescription, + modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsFastMode), + tag = R.string.recommended, + checked = state.pushEnabled, + onClick = { setEnabled(true) } + ) - val explanationTxt = Phrase.from(stringResource(R.string.notificationsSlowModeDescription)) - .put(APP_NAME_KEY, stringResource(R.string.app_name)) - .format().toString() + // spacing between buttons is provided by ripple/downstate of NotificationRadioButton - NotificationRadioButton( - stringResource(R.string.notificationsSlowMode), - explanationTxt, - modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsSlowMode), - checked = state.pushDisabled, - onClick = { setEnabled(false) } - ) + val explanationTxt = + Phrase.from(stringResource(R.string.notificationsSlowModeDescription)) + .put(APP_NAME_KEY, stringResource(R.string.app_name)) + .format().toString() + + NotificationRadioButton( + stringResource(R.string.notificationsSlowMode), + explanationTxt, + modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsSlowMode), + checked = state.pushDisabled, + onClick = { setEnabled(false) } + ) + } Spacer(Modifier.weight(1f)) From 1fc426be91d027b87dffd822ffc1fc44b77d399e Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 6 Oct 2025 10:40:16 +0800 Subject: [PATCH 024/103] Revert "notif landscape fix" This reverts commit 3cc3ec039af9a95d17dc77b91262f574398d799a. --- .../MessageNotifications.kt | 87 +++++-------------- 1 file changed, 20 insertions(+), 67 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt index b4b6d2dd42..3c5bb0eb75 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt @@ -1,8 +1,5 @@ package org.thoughtcrime.securesms.onboarding.messagenotifications -import android.R.attr.checked -import android.R.attr.onClick -import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -12,14 +9,11 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -81,70 +75,29 @@ internal fun MessageNotificationsScreen( Spacer(Modifier.height(LocalDimensions.current.spacing)) } -// NotificationRadioButton( -// R.string.notificationsFastMode, -// if(BuildConfig.FLAVOR == "huawei") R.string.notificationsFastModeDescriptionHuawei -// else R.string.notificationsFastModeDescription, -// modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsFastMode), -// tag = R.string.recommended, -// checked = state.pushEnabled, -// onClick = { setEnabled(true) } -// ) -// -// // spacing between buttons is provided by ripple/downstate of NotificationRadioButton -// -// val explanationTxt = Phrase.from(stringResource(R.string.notificationsSlowModeDescription)) -// .put(APP_NAME_KEY, stringResource(R.string.app_name)) -// .format().toString() -// -// NotificationRadioButton( -// stringResource(R.string.notificationsSlowMode), -// explanationTxt, -// modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsSlowMode), -// checked = state.pushDisabled, -// onClick = { setEnabled(false) } -// ) - - val isLandscape = - LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE - val optionsScroll = rememberScrollState() - - val optionsModifier = if (isLandscape) { - Modifier - .weight(1f) - .verticalScroll(optionsScroll) - } else { - Modifier - } + NotificationRadioButton( + R.string.notificationsFastMode, + if(BuildConfig.FLAVOR == "huawei") R.string.notificationsFastModeDescriptionHuawei + else R.string.notificationsFastModeDescription, + modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsFastMode), + tag = R.string.recommended, + checked = state.pushEnabled, + onClick = { setEnabled(true) } + ) - Column(modifier = optionsModifier) { - NotificationRadioButton( - R.string.notificationsFastMode, - if (BuildConfig.FLAVOR == "huawei") - R.string.notificationsFastModeDescriptionHuawei - else - R.string.notificationsFastModeDescription, - modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsFastMode), - tag = R.string.recommended, - checked = state.pushEnabled, - onClick = { setEnabled(true) } - ) + // spacing between buttons is provided by ripple/downstate of NotificationRadioButton - // spacing between buttons is provided by ripple/downstate of NotificationRadioButton + val explanationTxt = Phrase.from(stringResource(R.string.notificationsSlowModeDescription)) + .put(APP_NAME_KEY, stringResource(R.string.app_name)) + .format().toString() - val explanationTxt = - Phrase.from(stringResource(R.string.notificationsSlowModeDescription)) - .put(APP_NAME_KEY, stringResource(R.string.app_name)) - .format().toString() - - NotificationRadioButton( - stringResource(R.string.notificationsSlowMode), - explanationTxt, - modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsSlowMode), - checked = state.pushDisabled, - onClick = { setEnabled(false) } - ) - } + NotificationRadioButton( + stringResource(R.string.notificationsSlowMode), + explanationTxt, + modifier = Modifier.qaTag(R.string.AccessibilityId_notificationsSlowMode), + checked = state.pushDisabled, + onClick = { setEnabled(false) } + ) Spacer(Modifier.weight(1f)) From 3305b9d6af6f44d9836bca0be4a0ec2d8d9f729f Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 6 Oct 2025 13:11:06 +0800 Subject: [PATCH 025/103] initial scaling for QR in recovery password --- .../recoverypassword/RecoveryPassword.kt | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) 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..dc51d936f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt @@ -1,15 +1,17 @@ package org.thoughtcrime.securesms.recoverypassword +import android.content.res.Configuration 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 @@ -20,6 +22,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -31,6 +34,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.rememberAdaptiveInfo import org.thoughtcrime.securesms.ui.border import org.thoughtcrime.securesms.ui.components.QrImage import org.thoughtcrime.securesms.ui.components.SlimOutlineButton @@ -104,13 +108,31 @@ 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 = rememberAdaptiveInfo() + 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(200.dp, 620.dp) + + 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) { From 373c6ea94d55d4f41b8851e2efac4dc9f36f2637 Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 6 Oct 2025 13:22:39 +0800 Subject: [PATCH 026/103] Updated usage of twopane into landscape check only. --- .../home/startconversation/home/StartConversation.kt | 5 +++-- .../securesms/preferences/QRCodeActivity.kt | 10 ++++------ 2 files changed, 7 insertions(+), 8 deletions(-) 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 4f0392d22f..e3de283126 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 @@ -37,6 +37,7 @@ 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.rememberAdaptiveInfo import org.thoughtcrime.securesms.ui.adaptive.rememberTwoPane import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon import org.thoughtcrime.securesms.ui.components.BasicAppBar @@ -57,7 +58,7 @@ internal fun StartConversationScreen( navigateTo: (StartConversationDestination) -> Unit, onClose: () -> Unit, ) { - val isTwoPane = rememberTwoPane() + val isLandscape = rememberAdaptiveInfo().isLandscape Column( modifier = Modifier.background( @@ -78,7 +79,7 @@ internal fun StartConversationScreen( modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()), color = LocalColors.current.backgroundSecondary ) { - if (isTwoPane) { + if (isLandscape) { TwoPaneContent(accountId, navigateTo) } else { PortraitContent(accountId, navigateTo) 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 b6c14fc609..003825cf80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -32,7 +32,7 @@ import org.session.libsignal.utilities.PublicKeyValidation import org.thoughtcrime.securesms.ScreenLockActionBarActivity import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.permissions.Permissions -import org.thoughtcrime.securesms.ui.adaptive.rememberTwoPane +import org.thoughtcrime.securesms.ui.adaptive.rememberAdaptiveInfo import org.thoughtcrime.securesms.ui.components.QRScannerScreen import org.thoughtcrime.securesms.ui.components.QrImage import org.thoughtcrime.securesms.ui.components.SessionTabRow @@ -127,10 +127,8 @@ private fun Tabs(accountId: String, errors: Flow, onScan: (String) -> Un @Composable fun QrPage(string: String) { - val isTwoPane = rememberTwoPane() - - if (isTwoPane) { - TwoPaneContent(string) + if (rememberAdaptiveInfo().isLandscape) { + LandscapeContent(string) } else { PortraitContent(string) } @@ -165,7 +163,7 @@ private fun PortraitContent(string: String) { } @Composable -private fun TwoPaneContent(string: String) { +private fun LandscapeContent(string: String) { BoxWithConstraints( modifier = Modifier .fillMaxSize() From 6d437040a13094eae5b4f15f739553ccaa7d6909 Mon Sep 17 00:00:00 2001 From: jbsession Date: Mon, 6 Oct 2025 14:00:42 +0800 Subject: [PATCH 027/103] Share component for qrPanel --- .../home/StartConversation.kt | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) 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 e3de283126..045ed26b14 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 @@ -38,7 +38,6 @@ import org.thoughtcrime.securesms.home.startconversation.StartConversationDestin import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.ItemButton import org.thoughtcrime.securesms.ui.adaptive.rememberAdaptiveInfo -import org.thoughtcrime.securesms.ui.adaptive.rememberTwoPane import org.thoughtcrime.securesms.ui.components.AppBarCloseIcon import org.thoughtcrime.securesms.ui.components.BasicAppBar import org.thoughtcrime.securesms.ui.components.QrImage @@ -80,7 +79,7 @@ internal fun StartConversationScreen( color = LocalColors.current.backgroundSecondary ) { if (isLandscape) { - TwoPaneContent(accountId, navigateTo) + LandscapeContent(accountId, navigateTo) } else { PortraitContent(accountId, navigateTo) } @@ -97,31 +96,19 @@ private fun PortraitContent( modifier = Modifier.verticalScroll(rememberScrollState()) ) { ActionList(navigateTo = navigateTo) - Column( + QrPanel( + accountId = accountId, 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 - ) - } + .fillMaxWidth(), + ) } } @Composable -private fun TwoPaneContent( +private fun LandscapeContent( accountId: String, navigateTo: (StartConversationDestination) -> Unit ) { @@ -158,9 +145,13 @@ private fun QrPanel( BoxWithConstraints( modifier = modifier ) { - // Fit the QR inside available height/width of the right rail - val shortest: Dp = if (maxWidth < maxHeight) maxWidth else maxHeight - val qrSide = (shortest * 0.72f).coerceIn(160.dp, 520.dp) + val qrModifier = if (rememberAdaptiveInfo().isLandscape) { + val shortest: Dp = if (maxWidth < maxHeight) maxWidth else maxHeight + val qrSide = (shortest * 0.70f).coerceIn(160.dp, 520.dp) + Modifier.size(qrSide) + } else { + Modifier + } Column( modifier = Modifier.widthIn(max = 420.dp), @@ -176,8 +167,7 @@ private fun QrPanel( Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) QrImage( string = accountId, - modifier = Modifier - .size(qrSide) + modifier = qrModifier .qaTag(R.string.AccessibilityId_qrCode), icon = R.drawable.session ) From ead5bbc8be94021cddc5e29af8ee2bbc8f0c7cd1 Mon Sep 17 00:00:00 2001 From: jbsession Date: Tue, 7 Oct 2025 14:07:27 +0800 Subject: [PATCH 028/103] Fixed overlap for 3 button navigation --- .../java/org/thoughtcrime/securesms/home/ConversationView.kt | 3 ++- .../main/java/org/thoughtcrime/securesms/home/HomeActivity.kt | 2 ++ app/src/main/res/layout/view_conversation.xml | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) 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 5d580c3d81..2fd06e2843 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,8 @@ class ConversationView : LinearLayout { override fun onFinishInflate() { super.onFinishInflate() - layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT) +// 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 9e18662ea9..e1ecff4510 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding @@ -91,6 +92,7 @@ import org.thoughtcrime.securesms.ui.setThemedContent import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.util.AvatarUtils import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.applySafeInsetsMargins import org.thoughtcrime.securesms.util.applySafeInsetsPaddings import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.fadeIn 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" /> Date: Tue, 7 Oct 2025 17:18:55 +0800 Subject: [PATCH 029/103] camera inset in conversation --- .../conversation/v2/ConversationActivityV2.kt | 8 + .../res/layout/activity_conversation_v2.xml | 549 +++++++++--------- 2 files changed, 284 insertions(+), 273 deletions(-) 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 f8d946cc74..55c8e6d347 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 @@ -44,6 +44,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 @@ -644,6 +645,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) { @@ -661,6 +664,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/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml index 202a67a80a..c39d1b37ec 100644 --- a/app/src/main/res/layout/activity_conversation_v2.xml +++ b/app/src/main/res/layout/activity_conversation_v2.xml @@ -1,312 +1,315 @@ - + 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"> + + + + - - - + - - - - - - - - - + -