From ca7eecca39f0d9632766b2a8c688417b1e4d9c73 Mon Sep 17 00:00:00 2001
From: SessionHero01 <180888785+SessionHero01@users.noreply.github.com>
Date: Mon, 10 Feb 2025 14:34:26 +1100
Subject: [PATCH] [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
---
app/src/main/AndroidManifest.xml | 2 +-
.../start/NewConversationFragment.kt | 14 +-
.../conversation/v2/ConversationActivityV2.kt | 3 +-
.../conversation/v2/ConversationViewModel.kt | 13 +-
.../v2/menus/ConversationMenuHelper.kt | 4 +-
.../securesms/debugmenu/DebugMenu.kt | 111 ++++++++++-----
.../securesms/debugmenu/DebugMenuViewModel.kt | 14 +-
.../legacy/CreateLegacyGroupFragment.kt | 132 ++++++++++++++++++
.../legacy/CreateLegacyGroupViewModel.kt | 46 ++++++
.../EditLegacyClosedGroupLoader.kt | 14 +-
.../{ => legacy}/EditLegacyGroupActivity.kt | 7 +-
.../EditLegacyGroupMembersAdapter.kt | 2 +-
.../res/layout/activity_edit_closed_group.xml | 2 +-
.../v2/ConversationViewModelTest.kt | 3 +-
.../conversation/v2/MentionEditableTest.kt | 4 +-
.../conversation/v2/MentionViewModelTest.kt | 8 +-
.../groups/LegacyGroupDeprecationManager.kt | 62 +++++---
.../pollers/LegacyClosedGroupPollerV2.kt | 2 +-
.../NonTranslatableStringConstants.kt | 1 -
.../utilities/TextSecurePreferences.kt | 12 ++
20 files changed, 366 insertions(+), 90 deletions(-)
create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/legacy/CreateLegacyGroupFragment.kt
create mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/legacy/CreateLegacyGroupViewModel.kt
rename app/src/main/java/org/thoughtcrime/securesms/groups/{ => legacy}/EditLegacyClosedGroupLoader.kt (73%)
rename app/src/main/java/org/thoughtcrime/securesms/groups/{ => legacy}/EditLegacyGroupActivity.kt (98%)
rename app/src/main/java/org/thoughtcrime/securesms/groups/{ => legacy}/EditLegacyGroupMembersAdapter.kt (97%)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 04fde2a923..ff8031044f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -163,7 +163,7 @@
android:label="@string/conversationsBlockedContacts"
/>
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt
index f01eeaea2d..3fb861defe 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt
@@ -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() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
index 4eb1ef7299..9fc48dbe47 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
@@ -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")
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
index 03ef0a5938..994352853b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
@@ -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 = 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 = isAdmin
- .map { admin ->
+ val showRecreateGroupButton: StateFlow =
+ combine(isAdmin, legacyGroupDeprecationManager.deprecationState) { admin, state ->
admin && recipient?.isLegacyGroupRecipient == true
+ && state != LegacyGroupDeprecationManager.DeprecationState.NOT_DEPRECATING
}.stateIn(viewModelScope, SharingStarted.Lazily, false)
private val attachmentDownloadHandler = AttachmentDownloadHandler(
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
index cee15cd1cd..8bd3a8f362 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
@@ -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
diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt
index 93ebeff500..1c72819e4e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt
@@ -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 = {}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt
index 69d40d02ae..eb450386b2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt
@@ -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
@@ -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,
- 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()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/CreateLegacyGroupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/CreateLegacyGroupFragment.kt
new file mode 100644
index 0000000000..37a8890507
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/CreateLegacyGroupFragment.kt
@@ -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)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/CreateLegacyGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/CreateLegacyGroupViewModel.kt
new file mode 100644
index 0000000000..1e76872d88
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/CreateLegacyGroupViewModel.kt
@@ -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>()
+ val recipients: LiveData> = _recipients
+
+ init {
+ viewModelScope.launch {
+ threadDb.approvedConversationList.use { openCursor ->
+ val reader = threadDb.readerFor(openCursor)
+ val recipients = mutableListOf()
+ 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 {
+ return _recipients.value?.filter {
+ it.address.serialize().contains(query, ignoreCase = true) || it.name?.contains(query, ignoreCase = true) == true
+ } ?: emptyList()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyClosedGroupLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/EditLegacyClosedGroupLoader.kt
similarity index 73%
rename from app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyClosedGroupLoader.kt
rename to app/src/main/java/org/thoughtcrime/securesms/groups/legacy/EditLegacyClosedGroupLoader.kt
index 3c34395c8b..abea121fc4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyClosedGroupLoader.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/EditLegacyClosedGroupLoader.kt
@@ -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()
+ }
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/EditLegacyGroupActivity.kt
similarity index 98%
rename from app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyGroupActivity.kt
rename to app/src/main/java/org/thoughtcrime/securesms/groups/legacy/EditLegacyGroupActivity.kt
index 88762c9185..29e7f1c0a3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyGroupActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/EditLegacyGroupActivity.kt
@@ -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
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyGroupMembersAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/EditLegacyGroupMembersAdapter.kt
similarity index 97%
rename from app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyGroupMembersAdapter.kt
rename to app/src/main/java/org/thoughtcrime/securesms/groups/legacy/EditLegacyGroupMembersAdapter.kt
index 7f55e1dd46..f039ecdaa0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditLegacyGroupMembersAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/legacy/EditLegacyGroupMembersAdapter.kt
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms.groups
+package org.thoughtcrime.securesms.groups.legacy
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
diff --git a/app/src/main/res/layout/activity_edit_closed_group.xml b/app/src/main/res/layout/activity_edit_closed_group.xml
index 2445fde11c..d6881200ce 100644
--- a/app/src/main/res/layout/activity_edit_closed_group.xml
+++ b/app/src/main/res/layout/activity_edit_closed_group.xml
@@ -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">
get() = mutableDeprecatedTime
+ val deprecatedTime: StateFlow 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 = MutableStateFlow(
+ prefs.deprecatingStartTimeOverride ?: defaultDeprecatingStartTime
+ )
+
+ val deprecatingStartTime: StateFlow get() = mutableDeprecatingStartTime
@Suppress("OPT_IN_USAGE")
val deprecationState: StateFlow
- 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
}
diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/LegacyClosedGroupPollerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/LegacyClosedGroupPollerV2.kt
index ac21294cf5..9697b2a08b 100644
--- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/LegacyClosedGroupPollerV2.kt
+++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/LegacyClosedGroupPollerV2.kt
@@ -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
diff --git a/libsession/src/main/java/org/session/libsession/utilities/NonTranslatableStringConstants.kt b/libsession/src/main/java/org/session/libsession/utilities/NonTranslatableStringConstants.kt
index eab88e60a2..31f893eccb 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/NonTranslatableStringConstants.kt
+++ b/libsession/src/main/java/org/session/libsession/utilities/NonTranslatableStringConstants.kt
@@ -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"
}
diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt
index 38499bda70..2b992f84c0 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt
+++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt
@@ -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())
+ }
+ }
}