Merge pull request #1597 from oxen-io/release/1.19.0

Release/1.19.0
This commit is contained in:
ThomasSession 2024-08-05 16:56:42 +10:00 committed by GitHub
commit c6c1aa60e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 419 additions and 165 deletions

View File

@ -28,8 +28,8 @@ configurations.all {
exclude module: "commons-logging"
}
def canonicalVersionCode = 376
def canonicalVersionName = "1.18.6"
def canonicalVersionCode = 377
def canonicalVersionName = "1.19.0"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
import org.thoughtcrime.securesms.util.Broadcaster;
import org.thoughtcrime.securesms.util.VersionDataFetcher;
import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory;
@ -110,7 +111,6 @@ import javax.inject.Inject;
import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp;
import kotlin.Unit;
import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig;
import network.loki.messenger.libsession_util.ConfigBase;
import network.loki.messenger.libsession_util.UserProfile;
@ -151,6 +151,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject PushRegistry pushRegistry;
@Inject ConfigFactory configFactory;
@Inject LastSentTimestampCache lastSentTimestampCache;
@Inject VersionDataFetcher versionDataFetcher;
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
@ -275,6 +276,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
OpenGroupManager.INSTANCE.startPolling();
});
// fetch last version data
versionDataFetcher.startTimedVersionCheck();
}
@Override
@ -287,12 +291,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
poller.stopIfNeeded();
}
ClosedGroupPollerV2.getShared().stopAll();
versionDataFetcher.stopTimedVersionCheck();
}
@Override
public void onTerminate() {
stopKovenant(); // Loki
OpenGroupManager.INSTANCE.stopPolling();
versionDataFetcher.stopTimedVersionCheck();
super.onTerminate();
}

View File

@ -18,7 +18,7 @@ fun showMuteDialog(
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.DAYS.toMillis(2)),
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.HOURS.toMillis(2)),
ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });

View File

@ -27,7 +27,11 @@ import org.thoughtcrime.securesms.groups.JoinCommunityFragment
@AndroidEntryPoint
class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate {
private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * 0.94).toInt() }
companion object{
const val PEEK_RATIO = 0.94f
}
private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * PEEK_RATIO).toInt() }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,

View File

@ -1,10 +1,13 @@
package org.thoughtcrime.securesms.conversation.start.newmessage
import androidx.compose.animation.AnimatedVisibility
import android.graphics.Rect
import android.os.Build
import android.view.ViewTreeObserver
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -15,23 +18,31 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.stringResource
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import network.loki.messenger.R
import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton
import org.thoughtcrime.securesms.conversation.start.StartConversationFragment.Companion.PEEK_RATIO
import org.thoughtcrime.securesms.ui.LoadingArcOr
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.components.AppBar
import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
@ -39,7 +50,13 @@ import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
import org.thoughtcrime.securesms.ui.components.SessionTabRow
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors
import kotlin.math.max
private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
@ -76,63 +93,113 @@ private fun EnterAccountId(
callbacks: Callbacks,
onHelp: () -> Unit = {}
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.imePadding()
) {
Column(
modifier = Modifier.padding(vertical = LocalDimensions.current.spacing),
horizontalAlignment = Alignment.CenterHorizontally,
// the scaffold is required to provide the contentPadding. That contentPadding is needed
// to properly handle the ime padding.
Scaffold() { contentPadding ->
// we need this extra surface to handle nested scrolling properly,
// because this scrollable component is inside a bottomSheet dialog which is itself scrollable
Surface(
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()),
color = LocalColors.current.backgroundSecondary
) {
SessionOutlinedTextField(
text = state.newMessageIdOrOns,
modifier = Modifier
.padding(horizontal = LocalDimensions.current.spacing),
contentDescription = "Session id input box",
placeholder = stringResource(R.string.accountIdOrOnsEnter),
onChange = callbacks::onChange,
onContinue = callbacks::onContinue,
error = state.error?.string(),
isTextErrorColor = state.isTextErrorColor
)
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
var accountModifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
BorderlessButtonWithIcon(
text = stringResource(R.string.messageNewDescription),
modifier = Modifier
.contentDescription(R.string.AccessibilityId_help_desk_link)
.padding(horizontal = LocalDimensions.current.mediumSpacing)
.fillMaxWidth(),
style = LocalType.current.small,
color = LocalColors.current.textSecondary,
iconRes = R.drawable.ic_circle_question_mark,
onClick = onHelp
)
}
// There is a known issue with the ime padding on android versions below 30
/// So on these older versions we need to resort to some manual padding based on the visible height
// when the keyboard is up
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
val keyboardHeight by keyboardHeight()
accountModifier = accountModifier.padding(bottom = keyboardHeight)
} else {
accountModifier = accountModifier
.consumeWindowInsets(contentPadding)
.imePadding()
}
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
Spacer(Modifier.weight(2f))
Column(
modifier = accountModifier
) {
Column(
modifier = Modifier.padding(vertical = LocalDimensions.current.spacing),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SessionOutlinedTextField(
text = state.newMessageIdOrOns,
modifier = Modifier
.padding(horizontal = LocalDimensions.current.spacing),
contentDescription = "Session id input box",
placeholder = stringResource(R.string.accountIdOrOnsEnter),
onChange = callbacks::onChange,
onContinue = callbacks::onContinue,
error = state.error?.string(),
isTextErrorColor = state.isTextErrorColor
)
PrimaryOutlineButton(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = LocalDimensions.current.xlargeSpacing)
.padding(bottom = LocalDimensions.current.smallSpacing)
.fillMaxWidth()
.contentDescription(R.string.next),
enabled = state.isNextButtonEnabled,
onClick = callbacks::onContinue
) {
LoadingArcOr(state.loading) {
Text(stringResource(R.string.next))
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
BorderlessButtonWithIcon(
text = stringResource(R.string.messageNewDescription),
modifier = Modifier
.contentDescription(R.string.AccessibilityId_help_desk_link)
.padding(horizontal = LocalDimensions.current.mediumSpacing)
.fillMaxWidth(),
style = LocalType.current.small,
color = LocalColors.current.textSecondary,
iconRes = R.drawable.ic_circle_question_mark,
onClick = onHelp
)
}
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
Spacer(Modifier.weight(2f))
PrimaryOutlineButton(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = LocalDimensions.current.xlargeSpacing)
.padding(bottom = LocalDimensions.current.smallSpacing)
.fillMaxWidth()
.contentDescription(R.string.next),
enabled = state.isNextButtonEnabled,
onClick = callbacks::onContinue
) {
LoadingArcOr(state.loading) {
Text(stringResource(R.string.next))
}
}
}
}
}
}
@Composable
fun keyboardHeight(): MutableState<Dp> {
val view = LocalView.current
var keyboardHeight = remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
DisposableEffect(view) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height * PEEK_RATIO
val keypadHeightPx = max( screenHeight - rect.bottom, 0f)
keyboardHeight.value = with(density) { keypadHeightPx.toDp() }
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
}
return keyboardHeight
}
@Preview
@Composable
private fun PreviewNewMessage(

View File

@ -46,7 +46,7 @@ internal class NewMessageViewModel @Inject constructor(
}
override fun onContinue() {
val idOrONS = state.value.newMessageIdOrOns
val idOrONS = state.value.newMessageIdOrOns.trim()
if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) {
onUnvalidatedPublicKey(publicKey = idOrONS)

View File

@ -164,7 +164,6 @@ import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@ -177,7 +176,6 @@ import org.thoughtcrime.securesms.util.isScrolledToBottom
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.start
import org.thoughtcrime.securesms.util.toPx
import java.lang.ref.WeakReference
import java.util.Locale
@ -1589,8 +1587,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val text = getMessageBody()
val userPublicKey = textSecurePreferences.getLocalNumber()
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
start<RecoveryPasswordActivity>()
if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) {
showSessionDialog {
title(R.string.dialog_send_seed_title)
text(R.string.dialog_send_seed_explanation)
button(R.string.dialog_send_seed_send_button_title) { sendTextOnlyMessage(true) }
cancelButton()
}
return null
}
// Create the message
val message = VisibleMessage().applyExpiryMode(viewModel.threadId)

View File

@ -1,23 +0,0 @@
package org.thoughtcrime.securesms.conversation.v2.dialogs
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import network.loki.messenger.R
import org.thoughtcrime.securesms.createSessionDialog
/** Shown if the user is about to send their recovery phrase to someone. */
class SendSeedDialog(private val proceed: (() -> Unit)? = null) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog {
title(R.string.dialog_send_seed_title)
text(R.string.dialog_send_seed_explanation)
button(R.string.dialog_send_seed_send_button_title) { send() }
cancelButton()
}
private fun send() {
proceed?.invoke()
dismiss()
}
}

View File

@ -80,16 +80,14 @@ import org.thoughtcrime.securesms.util.start
import java.io.IOException
import javax.inject.Inject
private const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
private const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
@AndroidEntryPoint
class HomeActivity : PassphraseRequiredActionBarActivity(),
ConversationClickListener,
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
companion object {
const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
}
private lateinit var binding: ActivityHomeBinding
private lateinit var glide: RequestManager
@ -137,7 +135,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
}
private val isNewAccount: Boolean get() = intent.getBooleanExtra(FROM_ONBOARDING, false)
private val isFromOnboarding: Boolean get() = intent.getBooleanExtra(FROM_ONBOARDING, false)
private val isNewAccount: Boolean get() = intent.getBooleanExtra(NEW_ACCOUNT, false)
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
@ -251,7 +250,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
else -> buildList {
result.contactAndGroupList.takeUnless { it.isEmpty() }?.let {
add(GlobalSearchAdapter.Model.Header(R.string.contacts))
add(GlobalSearchAdapter.Model.Header(R.string.conversations))
addAll(it)
}
result.messageResults.takeUnless { it.isEmpty() }?.let {
@ -264,8 +263,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
}
EventBus.getDefault().register(this@HomeActivity)
if (intent.hasExtra(FROM_ONBOARDING)
&& intent.getBooleanExtra(FROM_ONBOARDING, false)) {
if (isFromOnboarding) {
if (Build.VERSION.SDK_INT >= 33 &&
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) {
Permissions.with(this)
@ -637,10 +635,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
}
fun Context.startHomeActivity(isNewAccount: Boolean) {
fun Context.startHomeActivity(isFromOnboarding: Boolean, isNewAccount: Boolean) {
Intent(this, HomeActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(HomeActivity.NEW_ACCOUNT, true)
putExtra(HomeActivity.FROM_ONBOARDING, true)
putExtra(NEW_ACCOUNT, isNewAccount)
putExtra(FROM_ONBOARDING, isFromOnboarding)
}.also(::startActivity)
}

View File

@ -32,7 +32,7 @@ class LoadingActivity: BaseActionBarActivity() {
when {
loadFailed -> startPickDisplayNameActivity(loadFailed = true)
else -> startHomeActivity(isNewAccount = false)
else -> startHomeActivity(isNewAccount = false, isFromOnboarding = true)
}
finish()

View File

@ -8,6 +8,7 @@ import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.VersionDataFetcher
import javax.inject.Inject
import javax.inject.Singleton
@ -16,6 +17,7 @@ class CreateAccountManager @Inject constructor(
private val application: Application,
private val prefs: TextSecurePreferences,
private val configFactory: ConfigFactory,
private val versionDataFetcher: VersionDataFetcher
) {
private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
@ -41,5 +43,7 @@ class CreateAccountManager @Inject constructor(
prefs.setLocalRegistrationId(registrationID)
prefs.setLocalNumber(userHexEncodedPublicKey)
prefs.setRestorationTime(0)
versionDataFetcher.startTimedVersionCheck()
}
}

View File

@ -12,6 +12,7 @@ import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.VersionDataFetcher
import javax.inject.Inject
import javax.inject.Singleton
@ -19,7 +20,8 @@ import javax.inject.Singleton
class LoadAccountManager @Inject constructor(
@dagger.hilt.android.qualifiers.ApplicationContext private val context: Context,
private val configFactory: ConfigFactory,
private val prefs: TextSecurePreferences
private val prefs: TextSecurePreferences,
private val versionDataFetcher: VersionDataFetcher
) {
private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage
@ -52,6 +54,8 @@ class LoadAccountManager @Inject constructor(
setHasViewedSeed(true)
}
versionDataFetcher.startTimedVersionCheck()
ApplicationContext.getInstance(context).retrieveUserProfile()
}
}

View File

@ -49,7 +49,7 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
viewModel.events.collect {
when (it) {
Event.Loading -> start<LoadingActivity>()
Event.OnboardingComplete -> startHomeActivity(isNewAccount = true)
Event.OnboardingComplete -> startHomeActivity(isNewAccount = true, isFromOnboarding = true)
}
}
}

View File

@ -45,7 +45,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
viewModel.events.collect {
when (it) {
is Event.CreateAccount -> startMessageNotificationsActivity(it.profileName)
Event.LoadAccountComplete -> startHomeActivity(isNewAccount = false)
Event.LoadAccountComplete -> startHomeActivity(isNewAccount = false, isFromOnboarding = true)
}
}
}

View File

@ -6,7 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ui.theme.selectedTheme
import org.thoughtcrime.securesms.ui.theme.invalidateComposeThemeColors
import org.thoughtcrime.securesms.util.ThemeState
import org.thoughtcrime.securesms.util.themeState
import javax.inject.Inject
@ -21,6 +21,8 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec
prefs.setAccentColorStyle(newAccentColorStyle)
// update UI state
_uiState.value = prefs.themeState()
invalidateComposeThemeColors()
}
fun setNewStyle(newThemeStyle: String) {
@ -28,16 +30,13 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec
// update UI state
_uiState.value = prefs.themeState()
// force compose to refresh its style reference
selectedTheme = null
invalidateComposeThemeColors()
}
fun setNewFollowSystemSettings(followSystemSettings: Boolean) {
prefs.setFollowSystemSettings(followSystemSettings)
_uiState.value = prefs.themeState()
// force compose to refresh its style reference
selectedTheme = null
invalidateComposeThemeColors()
}
}
}

View File

@ -1,10 +0,0 @@
package org.thoughtcrime.securesms.ui.theme
/**
* This class holds two instances of [ThemeColors], [light] representing the [ThemeColors] to use when the system is in a
* light theme, and [dark] representing the [ThemeColors] to use when the system is in a dark theme.
*/
data class ThemeColorSet(
val light: ThemeColors,
val dark: ThemeColors
)

View File

@ -0,0 +1,20 @@
package org.thoughtcrime.securesms.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
fun interface ThemeColorsProvider {
@Composable
fun get(): ThemeColors
}
@Suppress("FunctionName")
fun FollowSystemThemeColorsProvider(light: ThemeColors, dark: ThemeColors) = ThemeColorsProvider {
when {
isSystemInDarkTheme() -> dark
else -> light
}
}
@Suppress("FunctionName")
fun ThemeColorsProvider(colors: ThemeColors) = ThemeColorsProvider { colors }

View File

@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences.Companion.BLUE_ACCENT
@ -17,38 +15,25 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.YELLOW_A
* Some behaviour is hardcoded to cater for legacy usage of people with themes already set
* But future themes will be picked and set directly from the "Appearance" screen
*/
@Composable
fun TextSecurePreferences.getComposeTheme(): ThemeColors {
fun TextSecurePreferences.getColorsProvider(): ThemeColorsProvider {
val selectedTheme = getThemeStyle()
// get the chosen primary color from the preferences
val selectedPrimary = primaryColor()
// create a theme set with the appropriate primary
val colorSet = when(selectedTheme){
TextSecurePreferences.OCEAN_DARK,
TextSecurePreferences.OCEAN_LIGHT -> ThemeColorSet(
light = OceanLight(selectedPrimary),
dark = OceanDark(selectedPrimary)
)
else -> ThemeColorSet(
light = ClassicLight(selectedPrimary),
dark = ClassicDark(selectedPrimary)
val isOcean = "ocean" in selectedTheme
val createLight = if (isOcean) ::OceanLight else ::ClassicLight
val createDark = if (isOcean) ::OceanDark else ::ClassicDark
return when {
getFollowSystemSettings() -> FollowSystemThemeColorsProvider(
light = createLight(selectedPrimary),
dark = createDark(selectedPrimary)
)
"light" in selectedTheme -> ThemeColorsProvider(createLight(selectedPrimary))
else -> ThemeColorsProvider(createDark(selectedPrimary))
}
// deliver the right set from the light/dark mode chosen
val theme = when{
getFollowSystemSettings() -> if(isSystemInDarkTheme()) colorSet.dark else colorSet.light
selectedTheme == TextSecurePreferences.CLASSIC_LIGHT ||
selectedTheme == TextSecurePreferences.OCEAN_LIGHT -> colorSet.light
else -> colorSet.dark
}
return theme
}
fun TextSecurePreferences.primaryColor(): Color = when(getSelectedAccentColor()) {
@ -60,6 +45,3 @@ fun TextSecurePreferences.primaryColor(): Color = when(getSelectedAccentColor())
YELLOW_ACCENT -> primaryYellow
else -> primaryGreen
}

View File

@ -20,7 +20,12 @@ import org.session.libsession.utilities.AppTextSecurePreferences
val LocalColors = compositionLocalOf <ThemeColors> { ClassicDark() }
val LocalType = compositionLocalOf { sessionTypography }
var selectedTheme: ThemeColors? = null
var cachedColorsProvider: ThemeColorsProvider? = null
fun invalidateComposeThemeColors() {
// invalidate compose theme colors
cachedColorsProvider = null
}
/**
* Apply a Material2 compose theme based on user selections in SharedPreferences.
@ -29,15 +34,15 @@ var selectedTheme: ThemeColors? = null
fun SessionMaterialTheme(
content: @Composable () -> Unit
) {
// set the theme data if it hasn't been done yet
if(selectedTheme == null) {
// Some values can be set from the preferences, and if not should fallback to a default value
val context = LocalContext.current
val preferences = AppTextSecurePreferences(context)
selectedTheme = preferences.getComposeTheme()
}
val context = LocalContext.current
val preferences = AppTextSecurePreferences(context)
SessionMaterialTheme(colors = selectedTheme ?: ClassicDark()) { content() }
val cachedColors = cachedColorsProvider ?: preferences.getColorsProvider().also { cachedColorsProvider = it }
SessionMaterialTheme(
colors = cachedColors.get(),
content = content
)
}
/**
@ -58,9 +63,8 @@ fun SessionMaterialTheme(
LocalType provides sessionTypography,
LocalContentColor provides colors.text,
LocalTextSelectionColors provides colors.textSelectionColors,
) {
content()
}
content = content
)
}
}

View File

@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.util
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.hours
private val TAG: String = VersionDataFetcher::class.java.simpleName
private val REFRESH_TIME_MS = 4.hours.inWholeMilliseconds
@Singleton
class VersionDataFetcher @Inject constructor(
private val prefs: TextSecurePreferences
) {
private val handler = Handler(Looper.getMainLooper())
private val fetchVersionData = Runnable {
scope.launch {
try {
// Perform the version check
val clientVersion = FileServerApi.getClientVersion()
Log.i(TAG, "Fetched version data: $clientVersion")
prefs.setLastVersionCheck()
startTimedVersionCheck()
} catch (e: Exception) {
// We can silently ignore the error
Log.e(TAG, "Error fetching version data", e)
// Schedule the next check for 4 hours from now, but do not setLastVersionCheck
// so the app will retry when the app is next foregrounded.
startTimedVersionCheck(REFRESH_TIME_MS)
}
}
}
private val scope = CoroutineScope(Dispatchers.Default)
/**
* Schedules fetching version data.
*
* @param delayMillis The delay before fetching version data. Default value is 4 hours from the
* last check or 0 if there was no previous check or if it was longer than 4 hours ago.
*/
@JvmOverloads
fun startTimedVersionCheck(
delayMillis: Long = REFRESH_TIME_MS + prefs.getLastVersionCheck() - System.currentTimeMillis()
) {
stopTimedVersionCheck()
handler.postDelayed(fetchVersionData, delayMillis)
}
fun stopTimedVersionCheck() {
handler.removeCallbacks(fetchVersionData)
}
}

@ -1 +1 @@
Subproject commit 20c06674d85369c2d12261582dd36a9f21504233
Subproject commit 0193c36e0dad461385d6407a00f33b7314e6d740

View File

@ -30,6 +30,7 @@ set(SOURCES
config_base.cpp
contacts.cpp
conversation.cpp
blinded_key.cpp
util.cpp)
add_library( # Sets the name of the library.

View File

@ -0,0 +1,34 @@
#include <jni.h>
#include <session/blinding.hpp>
#include "util.h"
#include "jni_utils.h"
//
// Created by Thomas Ruffie on 29/7/2024.
//
extern "C"
JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionKeyPair(JNIEnv *env,
jobject thiz,
jbyteArray ed25519_secret_key) {
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
const auto [pk, sk] = session::blind_version_key_pair(util::ustring_from_bytes(env, ed25519_secret_key));
jclass kp_class = env->FindClass("network/loki/messenger/libsession_util/util/KeyPair");
jmethodID kp_constructor = env->GetMethodID(kp_class, "<init>", "([B[B)V");
return env->NewObject(kp_class, kp_constructor, util::bytes_from_ustring(env, {pk.data(), pk.size()}), util::bytes_from_ustring(env, {sk.data(), sk.size()}));
});
}
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionSign(JNIEnv *env,
jobject thiz,
jbyteArray ed25519_secret_key,
jlong timestamp) {
return jni_utils::run_catching_cxx_exception_or_throws<jbyteArray>(env, [=] {
auto bytes = session::blind_version_sign(util::ustring_from_bytes(env, ed25519_secret_key), session::Platform::android, timestamp);
return util::bytes_from_ustring(env, bytes);
});
}

View File

@ -0,0 +1,15 @@
package network.loki.messenger.libsession_util.util
object BlindKeyAPI {
private val loadLibrary by lazy {
System.loadLibrary("session_util")
}
init {
// Ensure the library is loaded at initialization
loadLibrary
}
external fun blindVersionKeyPair(ed25519SecretKey: ByteArray): KeyPair
external fun blindVersionSign(ed25519SecretKey: ByteArray, timestamp: Long): ByteArray
}

View File

@ -1,18 +1,22 @@
package org.session.libsession.messaging.file_server
import android.util.Base64
import network.loki.messenger.libsession_util.util.BlindKeyAPI
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.utilities.await
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import kotlin.time.Duration.Companion.milliseconds
object FileServerApi {
@ -23,6 +27,7 @@ object FileServerApi {
sealed class Error(message: String) : Exception(message) {
object ParsingFailed : Error("Invalid response.")
object InvalidURL : Error("Invalid URL.")
object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
}
data class Request(
@ -105,4 +110,50 @@ object FileServerApi {
val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file")
return send(request)
}
/**
* Returns the current version of session
* This is effectively proxying (and caching) the response from the github release
* page.
*
* Note that the value is cached and can be up to 30 minutes out of date normally, and up to 24
* hours out of date if we cannot reach the Github API for some reason.
*
* https://github.com/oxen-io/session-file-server/blob/dev/doc/api.yaml#L119
*/
suspend fun getClientVersion(): VersionData {
// Generate the auth signature
val secretKey = MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes
?: throw (Error.NoEd25519KeyPair)
val blindedKeys = BlindKeyAPI.blindVersionKeyPair(secretKey)
val timestamp = System.currentTimeMillis().milliseconds.inWholeSeconds // The current timestamp in seconds
val signature = BlindKeyAPI.blindVersionSign(secretKey, timestamp)
// The hex encoded version-blinded public key with a 07 prefix
val blindedPkHex = "07" + blindedKeys.pubKey.toHexString()
val request = Request(
verb = HTTP.Verb.GET,
endpoint = "session_version",
queryParameters = mapOf("platform" to "android"),
headers = mapOf(
"X-FS-Pubkey" to blindedPkHex,
"X-FS-Timestamp" to timestamp.toString(),
"X-FS-Signature" to Base64.encodeToString(signature, Base64.NO_WRAP)
)
)
// transform the promise into a coroutine
val result = send(request).await()
// map out the result
return JsonUtil.fromJson(result, Map::class.java).let {
VersionData(
statusCode = it["status_code"] as? Int ?: 0,
version = it["result"] as? String ?: "",
updated = it["updated"] as? Double ?: 0.0
)
}
}
}

View File

@ -0,0 +1,7 @@
package org.session.libsession.messaging.file_server
data class VersionData(
val statusCode: Int, // The value 200. Included for backwards compatibility, and may be removed someday.
val version: String, // The Session version.
val updated: Double // The unix timestamp when this version was retrieved from Github; this can be up to 24 hours ago in case of consistent fetch errors, though normally will be within the last 30 minutes.
)

View File

@ -0,0 +1,13 @@
package org.session.libsession.snode.utilities
import nl.komponents.kovenant.Promise
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
suspend fun <T, E: Throwable> Promise<T, E>.await(): T {
return suspendCoroutine { cont ->
success(cont::resume)
fail(cont::resumeWithException)
}
}

View File

@ -13,6 +13,7 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.session.libsession.R
import org.session.libsession.utilities.TextSecurePreferences.Companion
import org.session.libsession.utilities.TextSecurePreferences.Companion.AUTOPLAY_AUDIO_MESSAGES
import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED
import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_DARK
@ -20,6 +21,7 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_
import org.session.libsession.utilities.TextSecurePreferences.Companion.FOLLOW_SYSTEM_SETTINGS
import org.session.libsession.utilities.TextSecurePreferences.Companion.HIDE_PASSWORD
import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VACUUM_TIME
import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VERSION_CHECK
import org.session.libsession.utilities.TextSecurePreferences.Companion.LEGACY_PREF_KEY_SELECTED_UI_MODE
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_DARK
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_LIGHT
@ -186,6 +188,8 @@ interface TextSecurePreferences {
fun clearAll()
fun getHidePassword(): Boolean
fun setHidePassword(value: Boolean)
fun getLastVersionCheck(): Long
fun setLastVersionCheck()
companion object {
val TAG = TextSecurePreferences::class.simpleName
@ -272,6 +276,7 @@ interface TextSecurePreferences {
const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio"
const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated"
const val SELECTED_ACCENT_COLOR = "selected_accent_color"
const val LAST_VERSION_CHECK = "pref_last_version_check"
const val HAS_RECEIVED_LEGACY_CONFIG = "has_received_legacy_config"
const val HAS_FORCED_NEW_CONFIG = "has_forced_new_config"
@ -1541,6 +1546,14 @@ class AppTextSecurePreferences @Inject constructor(
setLongPreference(LAST_VACUUM_TIME, System.currentTimeMillis())
}
override fun getLastVersionCheck(): Long {
return getLongPreference(LAST_VERSION_CHECK, 0)
}
override fun setLastVersionCheck() {
setLongPreference(LAST_VERSION_CHECK, System.currentTimeMillis())
}
override fun setShownCallNotification(): Boolean {
val previousValue = getBooleanPreference(SHOWN_CALL_NOTIFICATION, false)
if (previousValue) return false