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" exclude module: "commons-logging"
} }
def canonicalVersionCode = 376 def canonicalVersionCode = 377
def canonicalVersionName = "1.18.6" def canonicalVersionName = "1.19.0"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, 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.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository; import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
import org.thoughtcrime.securesms.util.Broadcaster; import org.thoughtcrime.securesms.util.Broadcaster;
import org.thoughtcrime.securesms.util.VersionDataFetcher;
import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper; import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor; import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory;
@ -110,7 +111,6 @@ import javax.inject.Inject;
import dagger.hilt.EntryPoints; import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp; import dagger.hilt.android.HiltAndroidApp;
import kotlin.Unit; import kotlin.Unit;
import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig; import network.loki.messenger.BuildConfig;
import network.loki.messenger.libsession_util.ConfigBase; import network.loki.messenger.libsession_util.ConfigBase;
import network.loki.messenger.libsession_util.UserProfile; import network.loki.messenger.libsession_util.UserProfile;
@ -151,6 +151,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject PushRegistry pushRegistry; @Inject PushRegistry pushRegistry;
@Inject ConfigFactory configFactory; @Inject ConfigFactory configFactory;
@Inject LastSentTimestampCache lastSentTimestampCache; @Inject LastSentTimestampCache lastSentTimestampCache;
@Inject VersionDataFetcher versionDataFetcher;
CallMessageProcessor callMessageProcessor; CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration; MessagingModuleConfiguration messagingModuleConfiguration;
@ -275,6 +276,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
OpenGroupManager.INSTANCE.startPolling(); OpenGroupManager.INSTANCE.startPolling();
}); });
// fetch last version data
versionDataFetcher.startTimedVersionCheck();
} }
@Override @Override
@ -287,12 +291,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
poller.stopIfNeeded(); poller.stopIfNeeded();
} }
ClosedGroupPollerV2.getShared().stopAll(); ClosedGroupPollerV2.getShared().stopAll();
versionDataFetcher.stopTimedVersionCheck();
} }
@Override @Override
public void onTerminate() { public void onTerminate() {
stopKovenant(); // Loki stopKovenant(); // Loki
OpenGroupManager.INSTANCE.stopPolling(); OpenGroupManager.INSTANCE.stopPolling();
versionDataFetcher.stopTimedVersionCheck();
super.onTerminate(); super.onTerminate();
} }

View File

@ -18,7 +18,7 @@ fun showMuteDialog(
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) { 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)), 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)), 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)), SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE }); FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });

View File

@ -27,7 +27,11 @@ import org.thoughtcrime.securesms.groups.JoinCommunityFragment
@AndroidEntryPoint @AndroidEntryPoint
class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate { 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( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,

View File

@ -1,10 +1,13 @@
package org.thoughtcrime.securesms.conversation.start.newmessage 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.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter 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.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import network.loki.messenger.R 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.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.AppBar
import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode 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.SessionOutlinedTextField
import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.components.SessionTabRow
import org.thoughtcrime.securesms.ui.contentDescription 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.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) private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
@ -76,11 +93,34 @@ private fun EnterAccountId(
callbacks: Callbacks, callbacks: Callbacks,
onHelp: () -> Unit = {} onHelp: () -> Unit = {}
) { ) {
Column( // the scaffold is required to provide the contentPadding. That contentPadding is needed
modifier = Modifier // 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
) {
var accountModifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
// 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() .imePadding()
}
Column(
modifier = accountModifier
) { ) {
Column( Column(
modifier = Modifier.padding(vertical = LocalDimensions.current.spacing), modifier = Modifier.padding(vertical = LocalDimensions.current.spacing),
@ -132,6 +172,33 @@ private fun EnterAccountId(
} }
} }
} }
}
}
@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 @Preview
@Composable @Composable

View File

@ -46,7 +46,7 @@ internal class NewMessageViewModel @Inject constructor(
} }
override fun onContinue() { override fun onContinue() {
val idOrONS = state.value.newMessageIdOrOns val idOrONS = state.value.newMessageIdOrOns.trim()
if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) { if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) {
onUnvalidatedPublicKey(publicKey = idOrONS) 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.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities 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.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.start
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Locale import java.util.Locale
@ -1589,8 +1587,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val text = getMessageBody() val text = getMessageBody()
val userPublicKey = textSecurePreferences.getLocalNumber() val userPublicKey = textSecurePreferences.getLocalNumber()
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey) val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) { if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) {
start<RecoveryPasswordActivity>() 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 // Create the message
val message = VisibleMessage().applyExpiryMode(viewModel.threadId) 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 java.io.IOException
import javax.inject.Inject import javax.inject.Inject
private const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
private const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
@AndroidEntryPoint @AndroidEntryPoint
class HomeActivity : PassphraseRequiredActionBarActivity(), class HomeActivity : PassphraseRequiredActionBarActivity(),
ConversationClickListener, ConversationClickListener,
GlobalSearchInputLayout.GlobalSearchInputLayoutListener { 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 binding: ActivityHomeBinding
private lateinit var glide: RequestManager 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 // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
@ -251,7 +250,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
else -> buildList { else -> buildList {
result.contactAndGroupList.takeUnless { it.isEmpty() }?.let { result.contactAndGroupList.takeUnless { it.isEmpty() }?.let {
add(GlobalSearchAdapter.Model.Header(R.string.contacts)) add(GlobalSearchAdapter.Model.Header(R.string.conversations))
addAll(it) addAll(it)
} }
result.messageResults.takeUnless { it.isEmpty() }?.let { result.messageResults.takeUnless { it.isEmpty() }?.let {
@ -264,8 +263,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
} }
EventBus.getDefault().register(this@HomeActivity) EventBus.getDefault().register(this@HomeActivity)
if (intent.hasExtra(FROM_ONBOARDING) if (isFromOnboarding) {
&& intent.getBooleanExtra(FROM_ONBOARDING, false)) {
if (Build.VERSION.SDK_INT >= 33 && if (Build.VERSION.SDK_INT >= 33 &&
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) { (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) {
Permissions.with(this) 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 { Intent(this, HomeActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(HomeActivity.NEW_ACCOUNT, true) putExtra(NEW_ACCOUNT, isNewAccount)
putExtra(HomeActivity.FROM_ONBOARDING, true) putExtra(FROM_ONBOARDING, isFromOnboarding)
}.also(::startActivity) }.also(::startActivity)
} }

View File

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

View File

@ -8,6 +8,7 @@ import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.hexEncodedPublicKey import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.VersionDataFetcher
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -16,6 +17,7 @@ class CreateAccountManager @Inject constructor(
private val application: Application, private val application: Application,
private val prefs: TextSecurePreferences, private val prefs: TextSecurePreferences,
private val configFactory: ConfigFactory, private val configFactory: ConfigFactory,
private val versionDataFetcher: VersionDataFetcher
) { ) {
private val database: LokiAPIDatabaseProtocol private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage get() = SnodeModule.shared.storage
@ -41,5 +43,7 @@ class CreateAccountManager @Inject constructor(
prefs.setLocalRegistrationId(registrationID) prefs.setLocalRegistrationId(registrationID)
prefs.setLocalNumber(userHexEncodedPublicKey) prefs.setLocalNumber(userHexEncodedPublicKey)
prefs.setRestorationTime(0) 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.ApplicationContext
import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.VersionDataFetcher
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -19,7 +20,8 @@ import javax.inject.Singleton
class LoadAccountManager @Inject constructor( class LoadAccountManager @Inject constructor(
@dagger.hilt.android.qualifiers.ApplicationContext private val context: Context, @dagger.hilt.android.qualifiers.ApplicationContext private val context: Context,
private val configFactory: ConfigFactory, private val configFactory: ConfigFactory,
private val prefs: TextSecurePreferences private val prefs: TextSecurePreferences,
private val versionDataFetcher: VersionDataFetcher
) { ) {
private val database: LokiAPIDatabaseProtocol private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage get() = SnodeModule.shared.storage
@ -52,6 +54,8 @@ class LoadAccountManager @Inject constructor(
setHasViewedSeed(true) setHasViewedSeed(true)
} }
versionDataFetcher.startTimedVersionCheck()
ApplicationContext.getInstance(context).retrieveUserProfile() ApplicationContext.getInstance(context).retrieveUserProfile()
} }
} }

View File

@ -49,7 +49,7 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
viewModel.events.collect { viewModel.events.collect {
when (it) { when (it) {
Event.Loading -> start<LoadingActivity>() 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 { viewModel.events.collect {
when (it) { when (it) {
is Event.CreateAccount -> startMessageNotificationsActivity(it.profileName) 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.session.libsession.utilities.TextSecurePreferences 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 org.thoughtcrime.securesms.util.themeState import org.thoughtcrime.securesms.util.themeState
import javax.inject.Inject import javax.inject.Inject
@ -21,6 +21,8 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec
prefs.setAccentColorStyle(newAccentColorStyle) prefs.setAccentColorStyle(newAccentColorStyle)
// update UI state // update UI state
_uiState.value = prefs.themeState() _uiState.value = prefs.themeState()
invalidateComposeThemeColors()
} }
fun setNewStyle(newThemeStyle: String) { fun setNewStyle(newThemeStyle: String) {
@ -28,16 +30,13 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec
// update UI state // update UI state
_uiState.value = prefs.themeState() _uiState.value = prefs.themeState()
// force compose to refresh its style reference invalidateComposeThemeColors()
selectedTheme = null
} }
fun setNewFollowSystemSettings(followSystemSettings: Boolean) { fun setNewFollowSystemSettings(followSystemSettings: Boolean) {
prefs.setFollowSystemSettings(followSystemSettings) prefs.setFollowSystemSettings(followSystemSettings)
_uiState.value = prefs.themeState() _uiState.value = prefs.themeState()
// force compose to refresh its style reference invalidateComposeThemeColors()
selectedTheme = null
} }
} }

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 package org.thoughtcrime.securesms.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences.Companion.BLUE_ACCENT 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 * 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 * But future themes will be picked and set directly from the "Appearance" screen
*/ */
@Composable fun TextSecurePreferences.getColorsProvider(): ThemeColorsProvider {
fun TextSecurePreferences.getComposeTheme(): ThemeColors {
val selectedTheme = getThemeStyle() val selectedTheme = getThemeStyle()
// get the chosen primary color from the preferences // get the chosen primary color from the preferences
val selectedPrimary = primaryColor() val selectedPrimary = primaryColor()
// create a theme set with the appropriate primary val isOcean = "ocean" in selectedTheme
val colorSet = when(selectedTheme){
TextSecurePreferences.OCEAN_DARK, val createLight = if (isOcean) ::OceanLight else ::ClassicLight
TextSecurePreferences.OCEAN_LIGHT -> ThemeColorSet( val createDark = if (isOcean) ::OceanDark else ::ClassicDark
light = OceanLight(selectedPrimary),
dark = OceanDark(selectedPrimary) return when {
) getFollowSystemSettings() -> FollowSystemThemeColorsProvider(
light = createLight(selectedPrimary),
else -> ThemeColorSet( dark = createDark(selectedPrimary)
light = ClassicLight(selectedPrimary),
dark = ClassicDark(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()) { fun TextSecurePreferences.primaryColor(): Color = when(getSelectedAccentColor()) {
@ -60,6 +45,3 @@ fun TextSecurePreferences.primaryColor(): Color = when(getSelectedAccentColor())
YELLOW_ACCENT -> primaryYellow YELLOW_ACCENT -> primaryYellow
else -> primaryGreen else -> primaryGreen
} }

View File

@ -20,7 +20,12 @@ import org.session.libsession.utilities.AppTextSecurePreferences
val LocalColors = compositionLocalOf <ThemeColors> { ClassicDark() } val LocalColors = compositionLocalOf <ThemeColors> { ClassicDark() }
val LocalType = compositionLocalOf { sessionTypography } 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. * Apply a Material2 compose theme based on user selections in SharedPreferences.
@ -29,15 +34,15 @@ var selectedTheme: ThemeColors? = null
fun SessionMaterialTheme( fun SessionMaterialTheme(
content: @Composable () -> Unit 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 context = LocalContext.current
val preferences = AppTextSecurePreferences(context) val preferences = AppTextSecurePreferences(context)
selectedTheme = preferences.getComposeTheme()
}
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, LocalType provides sessionTypography,
LocalContentColor provides colors.text, LocalContentColor provides colors.text,
LocalTextSelectionColors provides colors.textSelectionColors, 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 config_base.cpp
contacts.cpp contacts.cpp
conversation.cpp conversation.cpp
blinded_key.cpp
util.cpp) util.cpp)
add_library( # Sets the name of the library. 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 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.Promise
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.utilities.await
import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import kotlin.time.Duration.Companion.milliseconds
object FileServerApi { object FileServerApi {
@ -23,6 +27,7 @@ object FileServerApi {
sealed class Error(message: String) : Exception(message) { sealed class Error(message: String) : Exception(message) {
object ParsingFailed : Error("Invalid response.") object ParsingFailed : Error("Invalid response.")
object InvalidURL : Error("Invalid URL.") object InvalidURL : Error("Invalid URL.")
object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
} }
data class Request( data class Request(
@ -105,4 +110,50 @@ object FileServerApi {
val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file") val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file")
return send(request) 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.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import org.session.libsession.R 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.AUTOPLAY_AUDIO_MESSAGES
import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED
import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_DARK 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.FOLLOW_SYSTEM_SETTINGS
import org.session.libsession.utilities.TextSecurePreferences.Companion.HIDE_PASSWORD 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_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.LEGACY_PREF_KEY_SELECTED_UI_MODE
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_DARK import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_DARK
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_LIGHT import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_LIGHT
@ -186,6 +188,8 @@ interface TextSecurePreferences {
fun clearAll() fun clearAll()
fun getHidePassword(): Boolean fun getHidePassword(): Boolean
fun setHidePassword(value: Boolean) fun setHidePassword(value: Boolean)
fun getLastVersionCheck(): Long
fun setLastVersionCheck()
companion object { companion object {
val TAG = TextSecurePreferences::class.simpleName val TAG = TextSecurePreferences::class.simpleName
@ -272,6 +276,7 @@ interface TextSecurePreferences {
const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio" const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio"
const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated" const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated"
const val SELECTED_ACCENT_COLOR = "selected_accent_color" 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_RECEIVED_LEGACY_CONFIG = "has_received_legacy_config"
const val HAS_FORCED_NEW_CONFIG = "has_forced_new_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()) 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 { override fun setShownCallNotification(): Boolean {
val previousValue = getBooleanPreference(SHOWN_CALL_NOTIFICATION, false) val previousValue = getBooleanPreference(SHOWN_CALL_NOTIFICATION, false)
if (previousValue) return false if (previousValue) return false