[SES-3251] - Legacy group migration - Part I (#916)

pull/1709/head
SessionHero01 2 months ago committed by GitHub
parent cc8ecc4c51
commit 6ad806afb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -170,6 +170,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject Lazy<MessageNotifier> messageNotifierLazy;
@Inject LokiAPIDatabase apiDB;
@Inject EmojiSearchDatabase emojiSearchDb;
@Inject LegacyClosedGroupPollerV2 legacyClosedGroupPollerV2;
public volatile boolean isAppVisible;
public String KEYGUARD_LOCK_TAG = NonTranslatableStringConstants.APP_NAME + ":KeyguardLock";
@ -257,7 +258,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
tokenFetcher,
groupManagerV2,
snodeClock,
textSecurePreferences
textSecurePreferences,
legacyClosedGroupPollerV2
);
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
Log.i(TAG, "onCreate()");
@ -343,7 +345,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
poller.stopIfNeeded();
}
pollerFactory.stopAll();
LegacyClosedGroupPollerV2.getShared().stopAll();
legacyClosedGroupPollerV2.stopAll();
versionDataFetcher.stopTimedVersionCheck();
}
@ -455,7 +457,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
poller.startIfNeeded();
}
pollerFactory.startAll();
LegacyClosedGroupPollerV2.getShared().start();
legacyClosedGroupPollerV2.start();
}
public void retrieveUserProfile() {

@ -71,6 +71,7 @@ class ConfigToDatabaseSync @Inject constructor(
private val preferences: TextSecurePreferences,
private val conversationRepository: ConversationRepository,
private val mmsSmsDatabase: MmsSmsDatabase,
private val legacyClosedGroupPollerV2: LegacyClosedGroupPollerV2,
) {
init {
if (!preferences.migratedToGroupV2Config) {
@ -369,7 +370,7 @@ class ConfigToDatabaseSync @Inject constructor(
// Don't create config group here, it's from a config update
// Start polling
LegacyClosedGroupPollerV2.shared.startPolling(group.accountId)
legacyClosedGroupPollerV2.startPolling(group.accountId)
}
if (messageTimestamp != null) {

@ -20,7 +20,7 @@ import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.text.style.ImageSpan
import android.util.Pair
@ -86,19 +86,16 @@ import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.NonTranslatableStringConstants
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.DATE_TIME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY
import org.session.libsession.utilities.Stub
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask
@ -833,41 +830,36 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
private fun setUpLegacyGroupBanner() {
val shouldDisplayBanner = viewModel.recipient?.isLegacyGroupRecipient ?: return
binding.outdatedGroupBanner.isVisible = shouldDisplayBanner
if (!shouldDisplayBanner) return
val url = "https://getsession.org/blog/session-groups-v2"
with(binding) {
// Create a SpannableString with text
val text = SpannableString(
Phrase.from(this@ConversationActivityV2, R.string.groupLegacyBanner)
//TODO groupsv2, date
.put(DATE_KEY, "")
.format()
)
// we need to add the inline icon
val drawable = ContextCompat.getDrawable(this@ConversationActivityV2, R.drawable.ic_external)
val imageSize = toPx(10, resources)
val imagePaddingTop = toPx(4, resources)
drawable?.setBounds(0, 0, imageSize, imageSize)
drawable?.setTint(getColorFromAttr(R.attr.message_sent_text_color))
// Create an ImageSpan with the drawable
val imageSpan = PaddedImageSpan(drawable!!, ImageSpan.ALIGN_BASELINE, imagePaddingTop)
// Append the image to the text
val spannable = SpannableString(text)
spannable.setSpan(imageSpan, text.length - 1, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
outdatedGroupBanner.text = spannable
lifecycleScope.launch {
viewModel.legacyGroupBanner
.collectLatest { banner ->
if (banner == null) {
binding.outdatedGroupBanner.isVisible = false
binding.outdatedGroupBanner.text = null
} else {
binding.outdatedGroupBanner.isVisible = true
binding.outdatedGroupBanner.text = SpannableStringBuilder(banner)
.apply {
// we need to add the inline icon
val drawable = ContextCompat.getDrawable(this@ConversationActivityV2, R.drawable.ic_external)!!
val imageSize = toPx(10, resources)
val imagePaddingTop = toPx(4, resources)
drawable.setBounds(0, 0, imageSize, imageSize)
drawable.setTint(getColorFromAttr(R.attr.message_sent_text_color))
setSpan(
PaddedImageSpan(drawable, ImageSpan.ALIGN_BASELINE, imagePaddingTop),
length - 1,
length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
outdatedGroupBanner.setOnClickListener {
showOpenUrlDialog(url)
}
binding.outdatedGroupBanner.setOnClickListener {
showOpenUrlDialog(NonTranslatableStringConstants.GROUP_UPDATE_URL)
}
}
}
}
}

@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.goterl.lazysodium.utils.KeyPair
import com.squareup.phrase.Phrase
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
@ -16,16 +17,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -40,7 +38,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.ConfigUpdateNotification
import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.getGroup
import org.session.libsession.utilities.recipients.MessageType
@ -60,9 +58,12 @@ import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.util.DateUtils
import java.time.ZoneId
import java.util.UUID
@OptIn(ExperimentalCoroutinesApi::class)
@ -80,6 +81,7 @@ class ConversationViewModel(
private val textSecurePreferences: TextSecurePreferences,
private val configFactory: ConfigFactory,
private val groupManagerV2: GroupManagerV2,
private val legacyGroupDeprecationManager: LegacyGroupDeprecationManager,
) : ViewModel() {
val showSendAfterApprovalText: Boolean
@ -195,6 +197,28 @@ class ConversationViewModel(
// allow reactions if the open group is null (normal conversations) or the open group's capabilities include reactions
get() = (openGroup == null || OpenGroupApi.Capability.REACTIONS.name.lowercase() in serverCapabilities)
val legacyGroupBanner: StateFlow<CharSequence?> = combine(
legacyGroupDeprecationManager.deprecationState,
legacyGroupDeprecationManager.deprecationTime,
isAdmin
) { state, time, admin ->
when {
recipient?.isLegacyGroupRecipient != true -> null
state == LegacyGroupDeprecationManager.DeprecationState.DEPRECATED -> {
Phrase.from(application, if (admin) R.string.legacyGroupAfterDeprecationAdmin else R.string.legacyGroupAfterDeprecationMember)
.format()
}
else -> Phrase.from(application, if (admin) R.string.legacyGroupBeforeDeprecationAdmin else R.string.legacyGroupBeforeDeprecationMember)
.put(DATE_KEY,
time.withZoneSameInstant(ZoneId.systemDefault())
.toLocalDate()
.format(DateUtils.getShortDateFormatter())
)
.format()
}
}.stateIn(viewModelScope, SharingStarted.Lazily, null)
private val attachmentDownloadHandler = AttachmentDownloadHandler(
storage = storage,
messageDataProvider = messageDataProvider,
@ -206,17 +230,18 @@ class ConversationViewModel(
combine(
repository.recipientUpdateFlow(threadId),
_openGroup,
) { a, b -> a to b }
.collect { (recipient, community) ->
_uiState.update {
it.copy(
shouldExit = recipient == null,
showInput = shouldShowInput(recipient, community),
enableInputMediaControls = shouldEnableInputMediaControls(recipient),
messageRequestState = buildMessageRequestState(recipient),
)
}
legacyGroupDeprecationManager.deprecationState,
::Triple
).collect { (recipient, community, deprecationState) ->
_uiState.update {
it.copy(
shouldExit = recipient == null,
showInput = shouldShowInput(recipient, community, deprecationState),
enableInputMediaControls = shouldEnableInputMediaControls(recipient),
messageRequestState = buildMessageRequestState(recipient),
)
}
}
}
// Listen for changes in the open group's write access
@ -260,13 +285,18 @@ class ConversationViewModel(
* For these situations we hide the input bar:
* 1. The user has been kicked from a group(v2), OR
* 2. The legacy group is inactive, OR
* 3. The community chat is read only
* 3. The legacy group is deprecated, OR
* 4. The community chat is read only
*/
private fun shouldShowInput(recipient: Recipient?, community: OpenGroup?): Boolean {
private fun shouldShowInput(recipient: Recipient?,
community: OpenGroup?,
deprecationState: LegacyGroupDeprecationManager.DeprecationState
): Boolean {
return when {
recipient?.isGroupV2Recipient == true -> !repository.isGroupReadOnly(recipient)
recipient?.isLegacyGroupRecipient == true -> {
groupDb.getGroup(recipient.address.toGroupString()).orNull()?.isActive == true
groupDb.getGroup(recipient.address.toGroupString()).orNull()?.isActive == true &&
deprecationState != LegacyGroupDeprecationManager.DeprecationState.DEPRECATED
}
community != null -> community.canWrite
else -> true
@ -1000,6 +1030,7 @@ class ConversationViewModel(
private val textSecurePreferences: TextSecurePreferences,
private val configFactory: ConfigFactory,
private val groupManagerV2: GroupManagerV2,
private val legacyGroupDeprecationManager: LegacyGroupDeprecationManager,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@ -1017,6 +1048,7 @@ class ConversationViewModel(
textSecurePreferences = textSecurePreferences,
configFactory = configFactory,
groupManagerV2 = groupManagerV2,
legacyGroupDeprecationManager = legacyGroupDeprecationManager,
) as T
}
}

@ -2,27 +2,38 @@ package org.thoughtcrime.securesms.debugmenu
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchColors
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
@ -33,6 +44,7 @@ import network.loki.messenger.R
import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Commands.ChangeEnvironment
import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Commands.HideEnvironmentWarningDialog
import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Commands.ShowEnvironmentWarningDialog
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.Cell
import org.thoughtcrime.securesms.ui.DialogButtonModel
@ -45,6 +57,10 @@ 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.bold
import java.time.Instant
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -55,6 +71,27 @@ fun DebugMenu(
onClose: () -> Unit
) {
val snackbarHostState = remember { SnackbarHostState() }
val showingDeprecationDatePicker = rememberDatePickerState()
var showingDeprecatedTimePicker by remember { mutableStateOf(false) }
val deprecatedTimePickerState = rememberTimePickerState()
val getPickedTime = {
val localDate = showingDeprecationDatePicker.selectedDateMillis?.let {
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.of("UTC")).toLocalDate()
} ?: uiState.forceDeprecatedTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalDate()
val localTime = if (showingDeprecatedTimePicker) {
LocalTime.of(
deprecatedTimePickerState.hour,
deprecatedTimePickerState.minute
)
} else {
uiState.forceDeprecatedTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalTime()
}
ZonedDateTime.of(localDate, localTime, ZoneId.systemDefault())
}
Scaffold(
modifier = modifier.fillMaxSize(),
@ -158,8 +195,105 @@ fun DebugMenu(
}
)
}
// Group deprecation state
DebugCell("Legacy Group Deprecation Overrides") {
DropDown(
selectedText = uiState.forceDeprecationState.displayName,
values = uiState.availableDeprecationState.map { it.displayName },
) { selected ->
val override = LegacyGroupDeprecationManager.DeprecationState.entries
.firstOrNull { it.displayName == selected }
sendCommand(DebugMenuViewModel.Commands.OverrideDeprecationState(override))
}
DebugRow(title = "Deprecated date", modifier = Modifier.clickable {
showingDeprecationDatePicker.selectedDateMillis = uiState.forceDeprecatedTime.withZoneSameLocal(
ZoneId.of("UTC")).toEpochSecond() * 1000L
}) {
Text(text = uiState.forceDeprecatedTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalDate().toString())
}
DebugRow(title = "Deprecated time", modifier = Modifier.clickable {
showingDeprecatedTimePicker = true
val time = uiState.forceDeprecatedTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalTime()
deprecatedTimePickerState.hour = time.hour
deprecatedTimePickerState.minute = time.minute
}) {
Text(text = uiState.forceDeprecatedTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalTime().toString())
}
}
}
}
// Deprecation date picker
if (showingDeprecationDatePicker.selectedDateMillis != null) {
DatePickerDialog(
onDismissRequest = {
showingDeprecationDatePicker.selectedDateMillis = null
},
confirmButton = {
TextButton(onClick = {
sendCommand(DebugMenuViewModel.Commands.OverrideDeprecatedTime(getPickedTime()))
showingDeprecationDatePicker.selectedDateMillis = null
}) {
Text("Set", color = LocalColors.current.text)
}
},
) {
DatePicker(showingDeprecationDatePicker)
}
}
if (showingDeprecatedTimePicker) {
AlertDialog(
onDismissRequest = {
showingDeprecatedTimePicker = false
},
title = "Set Deprecated Time",
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.cancel),
onClick = { showingDeprecatedTimePicker = false }
),
DialogButtonModel(
text = GetString(R.string.ok),
onClick = {
sendCommand(DebugMenuViewModel.Commands.OverrideDeprecatedTime(getPickedTime()))
showingDeprecatedTimePicker = false
}
)
)
) {
TimePicker(deprecatedTimePickerState)
}
}
}
}
private val LegacyGroupDeprecationManager.DeprecationState?.displayName: String get() {
return this?.name ?: "No state override"
}
@Composable
private fun DebugRow(
title: String,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
Row(
modifier = modifier.heightIn(min = LocalDimensions.current.minItemButtonHeight),
verticalAlignment = Alignment.CenterVertically
){
Text(
text = title,
style = LocalType.current.base,
modifier = Modifier.weight(1f)
)
content()
}
}
@ -170,18 +304,12 @@ fun DebugSwitchRow(
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
){
Row(
DebugRow(
title = text,
modifier = modifier
.fillMaxWidth()
.clickable { onCheckedChange(!checked) },
verticalAlignment = Alignment.CenterVertically
){
Text(
text = text,
style = LocalType.current.base,
modifier = Modifier.weight(1f)
)
SessionSwitch(
checked = checked,
onCheckedChange = onCheckedChange
@ -213,7 +341,7 @@ fun SessionSwitch(
fun ColumnScope.DebugCell(
title: String,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
content: @Composable ColumnScope.() -> Unit
) {
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
@ -221,15 +349,14 @@ fun ColumnScope.DebugCell(
modifier = modifier
) {
Column(
modifier = Modifier.padding(LocalDimensions.current.spacing)
modifier = Modifier.padding(LocalDimensions.current.spacing),
verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.xsSpacing)
) {
Text(
text = title,
style = LocalType.current.large.bold()
)
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
content()
}
}
@ -247,7 +374,10 @@ fun PreviewDebugMenu() {
showEnvironmentWarningDialog = false,
showEnvironmentLoadingDialog = false,
hideMessageRequests = true,
hideNoteToSelf = false
hideNoteToSelf = false,
forceDeprecationState = null,
forceDeprecatedTime = ZonedDateTime.now(),
availableDeprecationState = emptyList()
),
sendCommand = {},
onClose = {}

@ -15,13 +15,16 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import java.time.ZonedDateTime
import javax.inject.Inject
@HiltViewModel
class DebugMenuViewModel @Inject constructor(
private val application: Application,
private val textSecurePreferences: TextSecurePreferences,
private val configFactory: ConfigFactory
private val configFactory: ConfigFactory,
private val deprecationManager: LegacyGroupDeprecationManager,
) : ViewModel() {
private val TAG = "DebugMenu"
@ -33,7 +36,10 @@ class DebugMenuViewModel @Inject constructor(
showEnvironmentWarningDialog = false,
showEnvironmentLoadingDialog = false,
hideMessageRequests = textSecurePreferences.hasHiddenMessageRequests(),
hideNoteToSelf = textSecurePreferences.hasHiddenNoteToSelf()
hideNoteToSelf = textSecurePreferences.hasHiddenNoteToSelf(),
forceDeprecationState = deprecationManager.deprecationStateOverride.value,
availableDeprecationState = listOf(null) + LegacyGroupDeprecationManager.DeprecationState.entries.toList(),
forceDeprecatedTime = deprecationManager.deprecationTime.value
)
)
val uiState: StateFlow<UIState>
@ -63,6 +69,16 @@ class DebugMenuViewModel @Inject constructor(
}
_uiState.value = _uiState.value.copy(hideNoteToSelf = command.hide)
}
is Commands.OverrideDeprecationState -> {
deprecationManager.overrideDeprecationState(command.state)
_uiState.value = _uiState.value.copy(forceDeprecationState = command.state)
}
is Commands.OverrideDeprecatedTime -> {
deprecationManager.overrideDeprecatedTime(command.time)
_uiState.value = _uiState.value.copy(forceDeprecatedTime = command.time)
}
}
}
@ -112,7 +128,10 @@ class DebugMenuViewModel @Inject constructor(
val showEnvironmentWarningDialog: Boolean,
val showEnvironmentLoadingDialog: Boolean,
val hideMessageRequests: Boolean,
val hideNoteToSelf: Boolean
val hideNoteToSelf: Boolean,
val forceDeprecationState: LegacyGroupDeprecationManager.DeprecationState?,
val availableDeprecationState: List<LegacyGroupDeprecationManager.DeprecationState?>,
val forceDeprecatedTime: ZonedDateTime
)
sealed class Commands {
@ -121,5 +140,7 @@ class DebugMenuViewModel @Inject constructor(
object HideEnvironmentWarningDialog : Commands()
data class HideMessageRequest(val hide: Boolean) : Commands()
data class HideNoteToSelf(val hide: Boolean) : Commands()
data class OverrideDeprecationState(val state: LegacyGroupDeprecationManager.DeprecationState?) : Commands()
data class OverrideDeprecatedTime(val time: ZonedDateTime) : Commands()
}
}

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.dependencies
import android.content.Context
import android.widget.Toast
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -15,7 +14,6 @@ import org.session.libsession.utilities.AppTextSecurePreferences
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.Toaster
import org.thoughtcrime.securesms.groups.GroupManagerV2Impl
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier
import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier

@ -15,8 +15,11 @@ import kotlinx.coroutines.GlobalScope
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.groups.GroupScope
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.session.libsession.messaging.sending_receiving.pollers.LegacyClosedGroupPollerV2
import org.session.libsession.snode.SnodeClock
import org.session.libsession.utilities.ConfigFactoryProtocol
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.thoughtcrime.securesms.database.ConfigDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
@ -64,4 +67,19 @@ object SessionUtilModule {
@Provides
@Singleton
fun provideGroupScope() = GroupScope()
@Provides
@Singleton
fun provideLegacyGroupPoller(
storage: StorageProtocol,
deprecationManager: LegacyGroupDeprecationManager
): LegacyClosedGroupPollerV2 {
return LegacyClosedGroupPollerV2(storage, deprecationManager)
}
@Provides
@Singleton
fun provideLegacyGroupDeprecationManager(prefs: TextSecurePreferences): LegacyGroupDeprecationManager {
return LegacyGroupDeprecationManager(prefs)
}
}

@ -25,7 +25,7 @@ object ClosedGroupManager {
// Notify the PN server
PushRegistryV1.unsubscribeGroup(closedGroupPublicKey = groupPublicKey, publicKey = userPublicKey)
// Stop polling
LegacyClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
MessagingModuleConfiguration.shared.legacyClosedGroupPollerV2.stopPolling(groupPublicKey)
storage.cancelPendingMessageSendJobs(threadId)
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
if (delete) {

@ -128,7 +128,10 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
// Closed groups
if (requestTargets.contains(Targets.CLOSED_GROUPS)) {
val closedGroupPoller = LegacyClosedGroupPollerV2() // Intentionally don't use shared
val closedGroupPoller = LegacyClosedGroupPollerV2(
MessagingModuleConfiguration.shared.storage,
MessagingModuleConfiguration.shared.legacyClosedGroupPollerV2.deprecationManager
) // Intentionally don't use shared
val storage = MessagingModuleConfiguration.shared.storage
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
allGroupPublicKeys.iterator().forEach { closedGroupPoller.poll(it) }

@ -16,16 +16,10 @@
*/
package org.thoughtcrime.securesms.util
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.text.format.DateFormat
import androidx.compose.ui.text.capitalize
import org.session.libsignal.utilities.Log
import java.text.DateFormat.SHORT
import java.text.DateFormat.getTimeInstance
import java.text.ParseException
import java.text.SimpleDateFormat
import java.time.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
import java.util.Locale
@ -114,6 +108,10 @@ object DateUtils : android.text.format.DateUtils() {
return SimpleDateFormat(dateFormatPattern, locale)
}
fun getShortDateFormatter(): DateTimeFormatter {
return DateTimeFormatter.ofPattern("d MMM yyyy")
}
// Method to get the String for a relative day in a locale-aware fashion, including using the
// auto-localised words for "today" and "yesterday" as appropriate.
fun getRelativeDate(

@ -7,6 +7,7 @@ import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.groups.GroupScope
import org.session.libsession.messaging.notifications.TokenFetcher
import org.session.libsession.messaging.sending_receiving.pollers.LegacyClosedGroupPollerV2
import org.session.libsession.snode.OwnedSwarmAuth
import org.session.libsession.snode.SnodeClock
import org.session.libsession.utilities.ConfigFactoryProtocol
@ -26,7 +27,8 @@ class MessagingModuleConfiguration(
val tokenFetcher: TokenFetcher,
val groupManagerV2: GroupManagerV2,
val clock: SnodeClock,
val preferences: TextSecurePreferences
val preferences: TextSecurePreferences,
val legacyClosedGroupPollerV2: LegacyClosedGroupPollerV2,
) {
companion object {

@ -0,0 +1,74 @@
package org.session.libsession.messaging.groups
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import org.session.libsession.utilities.TextSecurePreferences
import java.time.Duration
import java.time.ZoneId
import java.time.ZonedDateTime
class LegacyGroupDeprecationManager(private val prefs: TextSecurePreferences) {
private val mutableDeprecationStateOverride = MutableStateFlow(
DeprecationState.entries.firstOrNull { it.name == prefs.deprecationStateOverride }
)
val deprecationStateOverride: StateFlow<DeprecationState?> get() = mutableDeprecationStateOverride
// The time all legacy groups will cease working. This value can be overridden by a debug
// facility.
private val defaultDeprecatedTime = ZonedDateTime.of(2025, 7, 1, 0, 0, 0, 0, ZoneId.of("UTC"))
private val mutableDeprecatedTime = MutableStateFlow<ZonedDateTime>(
prefs.deprecatedTimeOverride ?: defaultDeprecatedTime
)
val deprecationTime: StateFlow<ZonedDateTime> get() = mutableDeprecatedTime
@Suppress("OPT_IN_USAGE")
val deprecationState: StateFlow<DeprecationState>
get() = combine(mutableDeprecationStateOverride, mutableDeprecatedTime, ::Pair)
.flatMapLatest { (overriding, deadline) ->
if (overriding != null) {
flowOf(overriding)
} else {
flow {
val now = ZonedDateTime.now()
if (now.isBefore(deadline)) {
emit(DeprecationState.DEPRECATING)
delay(Duration.between(now, deadline).toMillis())
}
emit(DeprecationState.DEPRECATED)
}
}
}
.stateIn(
scope = GlobalScope,
started = SharingStarted.Lazily,
initialValue = mutableDeprecationStateOverride.value ?: DeprecationState.DEPRECATING
)
fun overrideDeprecationState(deprecationState: DeprecationState?) {
mutableDeprecationStateOverride.value = deprecationState
prefs.deprecationStateOverride = deprecationState?.name
}
fun overrideDeprecatedTime(deprecatedTime: ZonedDateTime?) {
mutableDeprecatedTime.value = deprecatedTime ?: defaultDeprecatedTime
prefs.deprecatedTimeOverride = deprecatedTime
}
enum class DeprecationState {
DEPRECATING,
DEPRECATED
}
}

@ -99,7 +99,7 @@ fun MessageSender.create(
// Notify the PN server
PushRegistryV1.register(device = device, publicKey = userPublicKey)
// Start polling
LegacyClosedGroupPollerV2.shared.startPolling(groupPublicKey)
MessagingModuleConfiguration.shared.legacyClosedGroupPollerV2.startPolling(groupPublicKey)
groupID
}
}

@ -871,7 +871,7 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp)
}
// Start polling
LegacyClosedGroupPollerV2.shared.startPolling(groupPublicKey)
MessagingModuleConfiguration.shared.legacyClosedGroupPollerV2.startPolling(groupPublicKey)
}
private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
@ -1177,7 +1177,7 @@ fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, grou
// Notify the PN server
PushRegistryV1.unsubscribeGroup(groupPublicKey, publicKey = userPublicKey)
// Stop polling
LegacyClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
MessagingModuleConfiguration.shared.legacyClosedGroupPollerV2.stopPolling(groupPublicKey)
if (delete) {
storage.getThreadId(Address.fromSerialized(groupID))?.let { threadId ->

@ -4,8 +4,8 @@ import kotlinx.coroutines.GlobalScope
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import nl.komponents.kovenant.task
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveParameters
@ -25,7 +25,10 @@ import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import kotlin.math.min
class LegacyClosedGroupPollerV2 {
class LegacyClosedGroupPollerV2(
private val storage: StorageProtocol,
val deprecationManager: LegacyGroupDeprecationManager,
) {
private val executorService = Executors.newScheduledThreadPool(1)
private var isPolling = mutableMapOf<String, Boolean>()
private var futures = mutableMapOf<String, ScheduledFuture<*>>()
@ -34,19 +37,17 @@ class LegacyClosedGroupPollerV2 {
return isPolling[groupPublicKey] ?: false
}
private fun canPoll(): Boolean = deprecationManager.deprecationState.value == LegacyGroupDeprecationManager.DeprecationState.DEPRECATING
companion object {
private val minPollInterval = 4 * 1000
private val maxPollInterval = 4 * 60 * 1000
@JvmStatic
val shared = LegacyClosedGroupPollerV2()
}
class InsufficientSnodesException() : Exception("No snodes left to poll.")
class PollingCanceledException() : Exception("Polling canceled.")
fun start() {
val storage = MessagingModuleConfiguration.shared.storage
val allGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
allGroupPublicKeys.iterator().forEach { startPolling(it) }
}
@ -77,10 +78,17 @@ class LegacyClosedGroupPollerV2 {
}
private fun pollRecursively(groupPublicKey: String) {
if (!isPolling(groupPublicKey)) { return }
if (!isPolling(groupPublicKey)) {
return
}
if (!canPoll()) {
Log.d("Loki", "Unable to start polling due to being deprecated")
return
}
// Get the received date of the last message in the thread. If we don't have any messages yet, pick some
// reasonable fake time interval to use instead.
val storage = MessagingModuleConfiguration.shared.storage
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val threadID = storage.getThreadId(groupID)
if (threadID == null) {
@ -106,6 +114,12 @@ class LegacyClosedGroupPollerV2 {
fun poll(groupPublicKey: String): Promise<Unit, Exception> {
if (!isPolling(groupPublicKey)) { return Promise.of(Unit) }
if (!canPoll()) {
Log.d("Loki", "Unable to start polling due to being deprecated")
return Promise.of(Unit)
}
val promise = SnodeAPI.getSwarm(groupPublicKey).bind { swarm ->
val snode = swarm.secureRandomOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
if (!isPolling(groupPublicKey)) { throw PollingCanceledException() }

@ -6,5 +6,6 @@ object NonTranslatableStringConstants {
const val SESSION_DOWNLOAD_URL = "https://getsession.org/download"
const val GIF = "GIF"
const val OXEN_FOUNDATION = "Oxen Foundation"
const val GROUP_UPDATE_URL = "https://getsession.org/blog/session-groups-v2"
}

@ -36,6 +36,7 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.SHOWN_CA
import org.session.libsession.utilities.TextSecurePreferences.Companion._events
import org.session.libsignal.utilities.Log
import java.io.IOException
import java.time.ZonedDateTime
import java.util.Arrays
import java.util.Date
import javax.inject.Inject
@ -196,6 +197,9 @@ interface TextSecurePreferences {
fun getEnvironment(): Environment
fun setEnvironment(value: Environment)
var deprecationStateOverride: String?
var deprecatedTimeOverride: ZonedDateTime?
var migratedToGroupV2Config: Boolean
companion object {
@ -311,6 +315,9 @@ interface TextSecurePreferences {
const val ALLOW_MESSAGE_REQUESTS = "libsession.ALLOW_MESSAGE_REQUESTS"
const val DEPRECATED_STATE_OVERRIDE = "deprecation_state_override"
const val DEPRECATED_TIME_OVERRIDE = "deprecated_time_override"
// Key name for if we've warned the user that saving attachments will allow other apps to access them.
// Note: We only ever display this once - and when the user has accepted the warning we never show it again
// for the lifetime of the Session installation.
@ -1692,4 +1699,24 @@ class AppTextSecurePreferences @Inject constructor(
override fun setHidePassword(value: Boolean) {
setBooleanPreference(HIDE_PASSWORD, value)
}
override var deprecationStateOverride: String?
get() = getStringPreference(TextSecurePreferences.DEPRECATED_STATE_OVERRIDE, null)
set(value) {
if (value == null) {
removePreference(TextSecurePreferences.DEPRECATED_STATE_OVERRIDE)
} else {
setStringPreference(TextSecurePreferences.DEPRECATED_STATE_OVERRIDE, value)
}
}
override var deprecatedTimeOverride: ZonedDateTime?
get() = getStringPreference(TextSecurePreferences.DEPRECATED_TIME_OVERRIDE, null)?.let(ZonedDateTime::parse)
set(value) {
if (value == null) {
removePreference(TextSecurePreferences.DEPRECATED_TIME_OVERRIDE)
} else {
setStringPreference(TextSecurePreferences.DEPRECATED_TIME_OVERRIDE, value.toString())
}
}
}

Loading…
Cancel
Save