Merge pull request #930 from session-foundation/merge-1.21.0

Merge 1.21.0 to dev
pull/1710/head
SessionHero01 2 months ago committed by GitHub
commit 517ec7c5a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -22,12 +22,14 @@ Build instructions can be found in [BUILDING.md](BUILDING.md).
## Translations
Want to help us translate Session into your language? You can do so [here](https://crowdin.com/project/session-crossplatform-strings)!
Want to help us translate Session into your language? You can do so at https://getsession.org/translate
## Verifying signatures
**Step 1:**
Add Jason's GPG key. Jason Rhinelander, a member of the [Session Technology Foundation](https://session.foundation/) and is the current signer for all Session Android releases. His GPG key can be found on his GitHub and other sources.
```
wget https://github.com/jagerman.gpg
gpg --import jagerman.gpg
@ -35,11 +37,11 @@ gpg --import jagerman.gpg
**Step 2:**
Get the signed hash for this release. `SESSION_VERSION` needs to be updated for the release you want to verify.
Get the signed hashes for this release. `SESSION_VERSION` needs to be updated for the release you want to verify.
```
export SESSION_VERSION=1.10.4
wget https://github.com/session-foundation/session-android/releases/download/$SESSION_VERSION/signatures.asc
export SESSION_VERSION=1.20.8
wget https://github.com/session-foundation/session-android/releases/download/$SESSION_VERSION/signature.asc
```
**Step 3:**
@ -47,18 +49,18 @@ wget https://github.com/session-foundation/session-android/releases/download/$SE
Verify the signature of the hashes of the files.
```
gpg --verify signatures.asc 2>&1 |grep "Good signature from"
gpg --verify signature.asc 2>&1 |grep "Good signature from"
```
The command above should print "`Good signature from "Kee Jefferys...`". If it does, the hashes are valid but we still have to make the sure the signed hashes matches the downloaded files.
The command above should print "`Good signature from "Jason Rhinelander...`". If it does, the hashes are valid but we still have to make the sure the signed hashes match the downloaded files.
**Step 4:**
Make sure the two commands below returns the same hash. If they do, files are valid.
Make sure the two commands below return the same hash for the file you are checking. If they do, file is valid.
```
sha256sum session-$SESSION_VERSION-universal.apk
grep universal.apk signatures.asc
grep universal.apk signature.asc
```
## License
@ -67,7 +69,9 @@ Copyright 2011 Whisper Systems
Copyright 2013-2017 Open Whisper Systems
Copyright 2019-2021 The Oxen Project
Copyright 2019-2024 The Oxen Project
Copyright 2024-2025 Session Technology Foundation
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html

@ -161,7 +161,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.DATE_KEY
@ -881,7 +880,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
}
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,4 +1,4 @@
package org.thoughtcrime.securesms.groups
package org.thoughtcrime.securesms.groups.legacy
import android.content.Context
import android.content.Intent
@ -37,6 +37,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

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="1dp"/>
<solid android:color="?conversation_menu_border_color"/>
</shape>

@ -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"
}

@ -198,6 +198,7 @@ interface TextSecurePreferences {
var deprecationStateOverride: String?
var deprecatedTimeOverride: ZonedDateTime?
var deprecatingStartTimeOverride: ZonedDateTime?
var migratedToGroupV2Config: Boolean
@ -315,6 +316,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
@ -1708,4 +1710,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