[SES-3251] - Add additional deprecation state and bring back legacy group creation (#928)

* Resurrect legacy group creation

* Added debug options

* Fixed tests

* Tidy up

* Remove constant
pull/1709/head
SessionHero01 2 months ago committed by GitHub
parent 021674bf15
commit ca7eecca39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -163,7 +163,7 @@
android:label="@string/conversationsBlockedContacts"
/>
<activity
android:name="org.thoughtcrime.securesms.groups.EditLegacyGroupActivity"
android:name="org.thoughtcrime.securesms.groups.legacy.EditLegacyGroupActivity"
android:label="@string/groupEdit"
android:screenOrientation="portrait" />

@ -15,6 +15,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.modifyLayoutParams
import org.thoughtcrime.securesms.conversation.start.home.StartConversationHomeFragment
@ -23,6 +24,8 @@ import org.thoughtcrime.securesms.conversation.start.newmessage.NewMessageFragme
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.groups.CreateGroupFragment
import org.thoughtcrime.securesms.groups.JoinCommunityFragment
import org.thoughtcrime.securesms.groups.legacy.CreateLegacyGroupFragment
import javax.inject.Inject
@AndroidEntryPoint
class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate {
@ -33,6 +36,9 @@ class StartConversationFragment : BottomSheetDialogFragment(), StartConversation
private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * PEEK_RATIO).toInt() }
@Inject
lateinit var deprecationManager: LegacyGroupDeprecationManager
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -66,7 +72,13 @@ class StartConversationFragment : BottomSheetDialogFragment(), StartConversation
}
override fun onCreateGroupSelected() {
replaceFragment(CreateGroupFragment())
val fragment = if (deprecationManager.deprecationState.value == LegacyGroupDeprecationManager.DeprecationState.NOT_DEPRECATING) {
CreateLegacyGroupFragment()
} else {
CreateGroupFragment()
}
replaceFragment(fragment)
}
override fun onJoinCommunitySelected() {

@ -91,7 +91,6 @@ 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.GROUP_NAME_KEY
@ -875,7 +874,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
binding.outdatedGroupBanner.setOnClickListener {
showOpenUrlDialog(NonTranslatableStringConstants.GROUP_UPDATE_URL)
showOpenUrlDialog("https://getsession.org/blog/session-groups-v2")
}
}
}

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2
import android.app.Application
import android.content.Context
import android.content.Intent
import android.view.MenuItem
import android.widget.Toast
import androidx.annotation.StringRes
@ -203,7 +202,7 @@ class ConversationViewModel(
val legacyGroupBanner: StateFlow<CharSequence?> = combine(
legacyGroupDeprecationManager.deprecationState,
legacyGroupDeprecationManager.deprecationTime,
legacyGroupDeprecationManager.deprecatedTime,
isAdmin
) { state, time, admin ->
when {
@ -212,19 +211,23 @@ class ConversationViewModel(
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)
state == LegacyGroupDeprecationManager.DeprecationState.DEPRECATING ->
Phrase.from(application, if (admin) R.string.legacyGroupBeforeDeprecationAdmin else R.string.legacyGroupBeforeDeprecationMember)
.put(DATE_KEY,
time.withZoneSameInstant(ZoneId.systemDefault())
.toLocalDate()
.format(DateUtils.getShortDateFormatter())
)
.format()
else -> null
}
}.stateIn(viewModelScope, SharingStarted.Lazily, null)
val showRecreateGroupButton: StateFlow<Boolean> = isAdmin
.map { admin ->
val showRecreateGroupButton: StateFlow<Boolean> =
combine(isAdmin, legacyGroupDeprecationManager.deprecationState) { admin, state ->
admin && recipient?.isLegacyGroupRecipient == true
&& state != LegacyGroupDeprecationManager.DeprecationState.NOT_DEPRECATING
}.stateIn(viewModelScope, SharingStarted.Lazily, false)
private val attachmentDownloadHandler = AttachmentDownloadHandler(

@ -50,8 +50,8 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.EditGroupActivity
import org.thoughtcrime.securesms.groups.EditLegacyGroupActivity
import org.thoughtcrime.securesms.groups.EditLegacyGroupActivity.Companion.groupIDKey
import org.thoughtcrime.securesms.groups.legacy.EditLegacyGroupActivity
import org.thoughtcrime.securesms.groups.legacy.EditLegacyGroupActivity.Companion.groupIDKey
import org.thoughtcrime.securesms.groups.GroupMembersActivity
import org.thoughtcrime.securesms.media.MediaOverviewActivity
import org.thoughtcrime.securesms.permissions.Permissions

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.debugmenu
import android.widget.TimePicker
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -17,6 +18,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DatePickerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
@ -26,6 +28,7 @@ import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker
import androidx.compose.material3.TimePickerState
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
@ -71,25 +74,21 @@ fun DebugMenu(
onClose: () -> Unit
) {
val snackbarHostState = remember { SnackbarHostState() }
val showingDeprecationDatePicker = rememberDatePickerState()
val datePickerState = rememberDatePickerState()
val timePickerState = rememberTimePickerState()
var showingDeprecatedDatePicker by remember { mutableStateOf(false) }
var showingDeprecatedTimePicker by remember { mutableStateOf(false) }
val deprecatedTimePickerState = rememberTimePickerState()
var showingDeprecatingStartDatePicker by remember { mutableStateOf(false) }
var showingDeprecatingStartTimePicker by remember { mutableStateOf(false) }
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()
}
val localDate = ZonedDateTime.ofInstant(
Instant.ofEpochMilli(datePickerState.selectedDateMillis!!), ZoneId.of("UTC")
).toLocalDate()
val localTime = LocalTime.of(timePickerState.hour, timePickerState.minute)
ZonedDateTime.of(localDate, localTime, ZoneId.systemDefault())
}
@ -208,70 +207,113 @@ fun DebugMenu(
sendCommand(DebugMenuViewModel.Commands.OverrideDeprecationState(override))
}
DebugRow(title = "Deprecating start date", modifier = Modifier.clickable {
datePickerState.applyFromZonedDateTime(uiState.deprecatingStartTime)
timePickerState.applyFromZonedDateTime(uiState.deprecatingStartTime)
showingDeprecatingStartDatePicker = true
}) {
Text(text = uiState.deprecatingStartTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalDate().toString())
}
DebugRow(title = "Deprecating start time", modifier = Modifier.clickable {
datePickerState.applyFromZonedDateTime(uiState.deprecatingStartTime)
timePickerState.applyFromZonedDateTime(uiState.deprecatingStartTime)
showingDeprecatingStartTimePicker = true
}) {
Text(text = uiState.deprecatingStartTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalTime().toString())
}
DebugRow(title = "Deprecated date", modifier = Modifier.clickable {
showingDeprecationDatePicker.selectedDateMillis = uiState.forceDeprecatedTime.withZoneSameLocal(
ZoneId.of("UTC")).toEpochSecond() * 1000L
datePickerState.applyFromZonedDateTime(uiState.deprecatedTime)
timePickerState.applyFromZonedDateTime(uiState.deprecatedTime)
showingDeprecatedDatePicker = true
}) {
Text(text = uiState.forceDeprecatedTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalDate().toString())
Text(text = uiState.deprecatedTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalDate().toString())
}
DebugRow(title = "Deprecated time", modifier = Modifier.clickable {
datePickerState.applyFromZonedDateTime(uiState.deprecatedTime)
timePickerState.applyFromZonedDateTime(uiState.deprecatedTime)
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())
Text(text = uiState.deprecatedTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalTime().toString())
}
}
}
}
// Deprecation date picker
if (showingDeprecationDatePicker.selectedDateMillis != null) {
if (showingDeprecatedDatePicker || showingDeprecatingStartDatePicker) {
DatePickerDialog(
onDismissRequest = {
showingDeprecationDatePicker.selectedDateMillis = null
showingDeprecatedDatePicker = false
showingDeprecatingStartDatePicker = false
},
confirmButton = {
TextButton(onClick = {
sendCommand(DebugMenuViewModel.Commands.OverrideDeprecatedTime(getPickedTime()))
showingDeprecationDatePicker.selectedDateMillis = null
if (showingDeprecatedDatePicker) {
sendCommand(DebugMenuViewModel.Commands.OverrideDeprecatedTime(getPickedTime()))
showingDeprecatedDatePicker = false
} else {
sendCommand(DebugMenuViewModel.Commands.OverrideDeprecatingStartTime(getPickedTime()))
showingDeprecatingStartDatePicker = false
}
}) {
Text("Set", color = LocalColors.current.text)
}
},
) {
DatePicker(showingDeprecationDatePicker)
DatePicker(datePickerState)
}
}
if (showingDeprecatedTimePicker) {
if (showingDeprecatedTimePicker || showingDeprecatingStartTimePicker) {
AlertDialog(
onDismissRequest = {
showingDeprecatedTimePicker = false
showingDeprecatingStartTimePicker = false
},
title = "Set Deprecated Time",
title = "Set Time",
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.cancel),
onClick = { showingDeprecatedTimePicker = false }
onClick = {
showingDeprecatedTimePicker = false
showingDeprecatingStartTimePicker = false
}
),
DialogButtonModel(
text = GetString(R.string.ok),
onClick = {
sendCommand(DebugMenuViewModel.Commands.OverrideDeprecatedTime(getPickedTime()))
showingDeprecatedTimePicker = false
if (showingDeprecatedTimePicker) {
sendCommand(DebugMenuViewModel.Commands.OverrideDeprecatedTime(getPickedTime()))
showingDeprecatedTimePicker = false
} else {
sendCommand(DebugMenuViewModel.Commands.OverrideDeprecatingStartTime(getPickedTime()))
showingDeprecatingStartTimePicker = false
}
}
)
)
) {
TimePicker(deprecatedTimePickerState)
TimePicker(timePickerState)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
private fun DatePickerState.applyFromZonedDateTime(time: ZonedDateTime) {
selectedDateMillis = time.withZoneSameInstant(ZoneId.of("UTC")).toEpochSecond() * 1000L
}
@OptIn(ExperimentalMaterial3Api::class)
private fun TimePickerState.applyFromZonedDateTime(time: ZonedDateTime) {
val normalised = time.withZoneSameInstant(ZoneId.systemDefault())
hour = normalised.hour
minute = normalised.minute
}
private val LegacyGroupDeprecationManager.DeprecationState?.displayName: String get() {
return this?.name ?: "No state override"
@ -376,8 +418,9 @@ fun PreviewDebugMenu() {
hideMessageRequests = true,
hideNoteToSelf = false,
forceDeprecationState = null,
forceDeprecatedTime = ZonedDateTime.now(),
availableDeprecationState = emptyList()
deprecatedTime = ZonedDateTime.now(),
availableDeprecationState = emptyList(),
deprecatingStartTime = ZonedDateTime.now()
),
sendCommand = {},
onClose = {}

@ -39,7 +39,8 @@ class DebugMenuViewModel @Inject constructor(
hideNoteToSelf = textSecurePreferences.hasHiddenNoteToSelf(),
forceDeprecationState = deprecationManager.deprecationStateOverride.value,
availableDeprecationState = listOf(null) + LegacyGroupDeprecationManager.DeprecationState.entries.toList(),
forceDeprecatedTime = deprecationManager.deprecationTime.value
deprecatedTime = deprecationManager.deprecatedTime.value,
deprecatingStartTime = deprecationManager.deprecatingStartTime.value,
)
)
val uiState: StateFlow<UIState>
@ -77,7 +78,12 @@ class DebugMenuViewModel @Inject constructor(
is Commands.OverrideDeprecatedTime -> {
deprecationManager.overrideDeprecatedTime(command.time)
_uiState.value = _uiState.value.copy(forceDeprecatedTime = command.time)
_uiState.value = _uiState.value.copy(deprecatedTime = command.time)
}
is Commands.OverrideDeprecatingStartTime -> {
deprecationManager.overrideDeprecatingStartTime(command.time)
_uiState.value = _uiState.value.copy(deprecatingStartTime = command.time)
}
}
}
@ -131,7 +137,8 @@ class DebugMenuViewModel @Inject constructor(
val hideNoteToSelf: Boolean,
val forceDeprecationState: LegacyGroupDeprecationManager.DeprecationState?,
val availableDeprecationState: List<LegacyGroupDeprecationManager.DeprecationState?>,
val forceDeprecatedTime: ZonedDateTime
val deprecatedTime: ZonedDateTime,
val deprecatingStartTime: ZonedDateTime,
)
sealed class Commands {
@ -142,5 +149,6 @@ class DebugMenuViewModel @Inject constructor(
data class HideNoteToSelf(val hide: Boolean) : Commands()
data class OverrideDeprecationState(val state: LegacyGroupDeprecationManager.DeprecationState?) : Commands()
data class OverrideDeprecatedTime(val time: ZonedDateTime) : Commands()
data class OverrideDeprecatingStartTime(val time: ZonedDateTime) : Commands()
}
}

@ -0,0 +1,132 @@
package org.thoughtcrime.securesms.groups.legacy
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentCreateGroupBinding
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Device
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.contacts.SelectContactsAdapter
import org.thoughtcrime.securesms.conversation.start.StartConversationDelegate
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut
import javax.inject.Inject
@AndroidEntryPoint
class CreateLegacyGroupFragment : Fragment() {
@Inject
lateinit var device: Device
@Inject
lateinit var textSecurePreferences: TextSecurePreferences
private lateinit var binding: FragmentCreateGroupBinding
private val viewModel: CreateLegacyGroupViewModel by viewModels()
private val delegate: StartConversationDelegate
get() = (context as? StartConversationDelegate)
?: (parentFragment as StartConversationDelegate)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentCreateGroupBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = SelectContactsAdapter(requireContext(), Glide.with(requireContext()))
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
binding.contactSearch.callbacks = object : KeyboardPageSearchView.Callbacks {
override fun onQueryChanged(query: String) {
adapter.members = viewModel.filter(query).map { it.address.serialize() }
}
}
binding.createNewPrivateChatButton.setOnClickListener { delegate.onNewMessageSelected() }
binding.recyclerView.adapter = adapter
val divider = ContextCompat.getDrawable(requireActivity(), R.drawable.conversation_menu_divider)!!.let {
DividerItemDecoration(requireActivity(), RecyclerView.VERTICAL).apply {
setDrawable(it)
}
}
binding.recyclerView.addItemDecoration(divider)
var isLoading = false
binding.createClosedGroupButton.setOnClickListener {
if (isLoading) return@setOnClickListener
val name = binding.nameEditText.text.trim()
if (name.isEmpty()) {
return@setOnClickListener Toast.makeText(context, R.string.groupNameEnterPlease, Toast.LENGTH_LONG).show()
}
// Limit the group name length if it exceeds the limit
if (name.length > resources.getInteger(R.integer.max_group_and_community_name_length_chars)) {
return@setOnClickListener Toast.makeText(context, R.string.groupNameEnterShorter, Toast.LENGTH_LONG).show()
}
val selectedMembers = adapter.selectedMembers
if (selectedMembers.isEmpty()) {
return@setOnClickListener Toast.makeText(context, R.string.groupCreateErrorNoMembers, Toast.LENGTH_LONG).show()
}
if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later
return@setOnClickListener Toast.makeText(context, R.string.groupAddMemberMaximum, Toast.LENGTH_LONG).show()
}
val userPublicKey = textSecurePreferences.getLocalNumber()!!
isLoading = true
binding.loaderContainer.fadeIn()
MessageSender.createClosedGroup(device, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
binding.loaderContainer.fadeOut()
isLoading = false
val threadID = DatabaseComponent.get(requireContext()).threadDatabase().getOrCreateThreadIdFor(Recipient.from(requireContext(), Address.fromSerialized(groupID), false))
openConversationActivity(
requireContext(),
threadID,
Recipient.from(requireContext(), Address.fromSerialized(groupID), false)
)
delegate.onDialogClosePressed()
}.failUi {
binding.loaderContainer.fadeOut()
isLoading = false
Toast.makeText(context, it.message, Toast.LENGTH_LONG).show()
}
}
binding.mainContentGroup.isVisible = !viewModel.recipients.value.isNullOrEmpty()
binding.emptyStateGroup.isVisible = viewModel.recipients.value.isNullOrEmpty()
viewModel.recipients.observe(viewLifecycleOwner) { recipients ->
adapter.members = recipients.map { it.address.serialize() }
}
}
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
val intent = Intent(context, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
context.startActivity(intent)
}
}

@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.groups.legacy
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.database.ThreadDatabase
import javax.inject.Inject
@HiltViewModel
class CreateLegacyGroupViewModel @Inject constructor(
private val threadDb: ThreadDatabase,
private val textSecurePreferences: TextSecurePreferences
) : ViewModel() {
private val _recipients = MutableLiveData<List<Recipient>>()
val recipients: LiveData<List<Recipient>> = _recipients
init {
viewModelScope.launch {
threadDb.approvedConversationList.use { openCursor ->
val reader = threadDb.readerFor(openCursor)
val recipients = mutableListOf<Recipient>()
while (true) {
recipients += reader.next?.recipient ?: break
}
withContext(Dispatchers.Main) {
_recipients.value = recipients
.filter { !it.isGroupRecipient && it.hasApprovedMe() && it.address.serialize() != textSecurePreferences.getLocalNumber() }
}
}
}
}
fun filter(query: String): List<Recipient> {
return _recipients.value?.filter {
it.address.serialize().contains(query, ignoreCase = true) || it.name?.contains(query, ignoreCase = true) == true
} ?: emptyList()
}
}

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.groups
package org.thoughtcrime.securesms.groups.legacy
import android.content.Context
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@ -11,12 +11,12 @@ class EditLegacyClosedGroupLoader(context: Context, val groupID: String) : Async
val members = groupDatabase.getGroupMembers(groupID, true)
val zombieMembers = groupDatabase.getGroupZombieMembers(groupID)
return EditLegacyGroupActivity.GroupMembers(
members.map {
it.address.toString()
},
zombieMembers.map {
it.address.toString()
}
members.map {
it.address.toString()
},
zombieMembers.map {
it.address.toString()
}
)
}
}

@ -1,10 +1,8 @@
package org.thoughtcrime.securesms.groups
package org.thoughtcrime.securesms.groups.legacy
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.SpannableString
import android.text.style.StyleSpan
import android.view.Menu
import android.view.MenuItem
import android.view.View
@ -20,7 +18,6 @@ import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.io.IOException
@ -31,7 +28,6 @@ import org.session.libsession.messaging.sending_receiving.groupSizeLimit
import org.session.libsession.messaging.sending_receiving.leave
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.StringSubstitutionConstants.COUNT_KEY
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.recipients.Recipient
@ -42,6 +38,7 @@ import org.thoughtcrime.securesms.contacts.SelectContactsActivity
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.ClosedGroupEditingOptionsBottomSheet
import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.groups
package org.thoughtcrime.securesms.groups.legacy
import android.content.Context
import androidx.recyclerview.widget.RecyclerView

@ -4,7 +4,7 @@
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="org.thoughtcrime.securesms.groups.EditLegacyGroupActivity">
tools:context="org.thoughtcrime.securesms.groups.legacy.EditLegacyGroupActivity">
<LinearLayout
android:id="@+id/mainContentContainer"

@ -47,7 +47,8 @@ class ConversationViewModelTest: BaseViewModelTest() {
application = application,
reactionDb = mock(),
configFactory = mock(),
groupManagerV2 = mock()
groupManagerV2 = mock(),
legacyGroupDeprecationManager = mock(),
)
}

@ -80,7 +80,7 @@ class MentionEditableTest {
@Test
fun `should move pass the whole span while moving cursor around mentioned block `() {
mentionEditable.append("Mention @user here")
mentionEditable.addMention(MentionViewModel.Member("user", "User", false), 8..14)
mentionEditable.addMention(MentionViewModel.Member("user", "User", false, false), 8..14)
// Put cursor right before @user, it should then select nothing
Selection.setSelection(mentionEditable, 8)
@ -98,7 +98,7 @@ class MentionEditableTest {
@Test
fun `should delete the whole mention block while deleting only part of it`() {
mentionEditable.append("Mention @user here")
mentionEditable.addMention(MentionViewModel.Member("user", "User", false), 8..14)
mentionEditable.addMention(MentionViewModel.Member("user", "User", false, false), 8..14)
mentionEditable.delete(8, 9)
assertThat(mentionEditable.toString()).isEqualTo("Mention here")

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.v2
import android.text.Selection
import androidx.test.platform.app.InstrumentationRegistry
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -69,7 +70,6 @@ class MentionViewModelTest {
fun setUp() {
@Suppress("UNCHECKED_CAST")
mentionViewModel = MentionViewModel(
threadID,
contentResolver = mock { },
threadDatabase = mock {
on { getRecipientForThreadId(threadID) } doAnswer {
@ -108,7 +108,9 @@ class MentionViewModelTest {
on { getOpenGroup(threadID) } doReturn openGroup
},
dispatcher = StandardTestDispatcher(),
configFactory = mock()
configFactory = mock(),
threadID = threadID,
application = InstrumentationRegistry.getInstrumentation().context as android.app.Application
)
}
@ -137,7 +139,7 @@ class MentionViewModelTest {
memberContacts[index].displayName(Contact.ContactContext.OPEN_GROUP).orEmpty()
MentionViewModel.Candidate(
MentionViewModel.Member(m.pubKey, name, m.roles.any { it.isModerator }),
MentionViewModel.Member(m.pubKey, name, m.roles.any { it.isModerator }, isMe = false),
name,
0
)

@ -30,32 +30,48 @@ class LegacyGroupDeprecationManager(private val prefs: TextSecurePreferences) {
prefs.deprecatedTimeOverride ?: defaultDeprecatedTime
)
val deprecationTime: StateFlow<ZonedDateTime> get() = mutableDeprecatedTime
val deprecatedTime: StateFlow<ZonedDateTime> get() = mutableDeprecatedTime
// The time a warning will be shown to users that legacy groups are being deprecated.
private val defaultDeprecatingStartTime = ZonedDateTime.of(2025, 6, 23, 0, 0, 0, 0, ZoneId.of("UTC"))
private val mutableDeprecatingStartTime: MutableStateFlow<ZonedDateTime> = MutableStateFlow(
prefs.deprecatingStartTimeOverride ?: defaultDeprecatingStartTime
)
val deprecatingStartTime: StateFlow<ZonedDateTime> get() = mutableDeprecatingStartTime
@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)
get() = combine(mutableDeprecationStateOverride,
mutableDeprecatedTime,
mutableDeprecatingStartTime,
::Triple
).flatMapLatest { (overriding, deprecatedTime, deprecatingStartTime) ->
if (overriding != null) {
flowOf(overriding)
} else {
flow {
val now = ZonedDateTime.now()
if (now.isBefore(deprecatingStartTime)) {
emit(DeprecationState.NOT_DEPRECATING)
delay(Duration.between(now, deprecatingStartTime).toMillis())
}
if (now.isBefore(deprecatedTime)) {
emit(DeprecationState.DEPRECATING)
delay(Duration.between(now, deprecatedTime).toMillis())
}
emit(DeprecationState.DEPRECATED)
}
}
.stateIn(
scope = GlobalScope,
started = SharingStarted.Lazily,
initialValue = mutableDeprecationStateOverride.value ?: DeprecationState.DEPRECATING
)
}.stateIn(
scope = GlobalScope,
started = SharingStarted.Lazily,
initialValue = mutableDeprecationStateOverride.value ?: DeprecationState.NOT_DEPRECATING
)
fun overrideDeprecationState(deprecationState: DeprecationState?) {
mutableDeprecationStateOverride.value = deprecationState
@ -67,7 +83,13 @@ class LegacyGroupDeprecationManager(private val prefs: TextSecurePreferences) {
prefs.deprecatedTimeOverride = deprecatedTime
}
fun overrideDeprecatingStartTime(deprecatingStartTime: ZonedDateTime?) {
mutableDeprecatingStartTime.value = deprecatingStartTime ?: defaultDeprecatingStartTime
prefs.deprecatingStartTimeOverride = deprecatingStartTime
}
enum class DeprecationState {
NOT_DEPRECATING,
DEPRECATING,
DEPRECATED
}

@ -37,7 +37,7 @@ class LegacyClosedGroupPollerV2(
return isPolling[groupPublicKey] ?: false
}
private fun canPoll(): Boolean = deprecationManager.deprecationState.value == LegacyGroupDeprecationManager.DeprecationState.DEPRECATING
private fun canPoll(): Boolean = deprecationManager.deprecationState.value != LegacyGroupDeprecationManager.DeprecationState.DEPRECATED
companion object {
private val minPollInterval = 4 * 1000

@ -6,6 +6,5 @@ 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"
}

@ -199,6 +199,7 @@ interface TextSecurePreferences {
var deprecationStateOverride: String?
var deprecatedTimeOverride: ZonedDateTime?
var deprecatingStartTimeOverride: ZonedDateTime?
var migratedToGroupV2Config: Boolean
@ -317,6 +318,7 @@ interface TextSecurePreferences {
const val DEPRECATED_STATE_OVERRIDE = "deprecation_state_override"
const val DEPRECATED_TIME_OVERRIDE = "deprecated_time_override"
const val DEPRECATING_START_TIME_OVERRIDE = "deprecating_start_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
@ -1719,4 +1721,14 @@ class AppTextSecurePreferences @Inject constructor(
setStringPreference(TextSecurePreferences.DEPRECATED_TIME_OVERRIDE, value.toString())
}
}
override var deprecatingStartTimeOverride: ZonedDateTime?
get() = getStringPreference(TextSecurePreferences.DEPRECATING_START_TIME_OVERRIDE, null)?.let(ZonedDateTime::parse)
set(value) {
if (value == null) {
removePreference(TextSecurePreferences.DEPRECATING_START_TIME_OVERRIDE)
} else {
setStringPreference(TextSecurePreferences.DEPRECATING_START_TIME_OVERRIDE, value.toString())
}
}
}

Loading…
Cancel
Save