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()) + } + } }