Merge remote-tracking branch 'origin/release/1.21.0' into merge-1.21.0

pull/1710/head
SessionHero01 3 months ago
commit 3d4e690754
No known key found for this signature in database

@ -22,12 +22,14 @@ Build instructions can be found in [BUILDING.md](BUILDING.md).
## Translations ## 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 ## Verifying signatures
**Step 1:** **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 wget https://github.com/jagerman.gpg
gpg --import jagerman.gpg gpg --import jagerman.gpg
@ -35,11 +37,11 @@ gpg --import jagerman.gpg
**Step 2:** **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 export SESSION_VERSION=1.20.8
wget https://github.com/session-foundation/session-android/releases/download/$SESSION_VERSION/signatures.asc wget https://github.com/session-foundation/session-android/releases/download/$SESSION_VERSION/signature.asc
``` ```
**Step 3:** **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. 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:** **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 sha256sum session-$SESSION_VERSION-universal.apk
grep universal.apk signatures.asc grep universal.apk signature.asc
``` ```
## License ## License
@ -67,7 +69,9 @@ Copyright 2011 Whisper Systems
Copyright 2013-2017 Open 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 Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html

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

@ -15,6 +15,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.modifyLayoutParams import org.session.libsession.utilities.modifyLayoutParams
import org.thoughtcrime.securesms.conversation.start.home.StartConversationHomeFragment 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.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.groups.CreateGroupFragment import org.thoughtcrime.securesms.groups.CreateGroupFragment
import org.thoughtcrime.securesms.groups.JoinCommunityFragment import org.thoughtcrime.securesms.groups.JoinCommunityFragment
import org.thoughtcrime.securesms.groups.legacy.CreateLegacyGroupFragment
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate { 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() } private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * PEEK_RATIO).toInt() }
@Inject
lateinit var deprecationManager: LegacyGroupDeprecationManager
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
@ -66,7 +72,13 @@ class StartConversationFragment : BottomSheetDialogFragment(), StartConversation
} }
override fun onCreateGroupSelected() { override fun onCreateGroupSelected() {
replaceFragment(CreateGroupFragment()) val fragment = if (deprecationManager.deprecationState.value == LegacyGroupDeprecationManager.DeprecationState.NOT_DEPRECATING) {
CreateLegacyGroupFragment()
} else {
CreateGroupFragment()
}
replaceFragment(fragment)
} }
override fun onJoinCommunitySelected() { 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.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MediaTypes 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.APP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY
@ -881,7 +880,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate,
} }
binding.outdatedGroupBanner.setOnClickListener { 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.app.Application
import android.content.Context import android.content.Context
import android.content.Intent
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
@ -203,7 +202,7 @@ class ConversationViewModel(
val legacyGroupBanner: StateFlow<CharSequence?> = combine( val legacyGroupBanner: StateFlow<CharSequence?> = combine(
legacyGroupDeprecationManager.deprecationState, legacyGroupDeprecationManager.deprecationState,
legacyGroupDeprecationManager.deprecationTime, legacyGroupDeprecationManager.deprecatedTime,
isAdmin isAdmin
) { state, time, admin -> ) { state, time, admin ->
when { when {
@ -212,19 +211,23 @@ class ConversationViewModel(
Phrase.from(application, if (admin) R.string.legacyGroupAfterDeprecationAdmin else R.string.legacyGroupAfterDeprecationMember) Phrase.from(application, if (admin) R.string.legacyGroupAfterDeprecationAdmin else R.string.legacyGroupAfterDeprecationMember)
.format() .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, .put(DATE_KEY,
time.withZoneSameInstant(ZoneId.systemDefault()) time.withZoneSameInstant(ZoneId.systemDefault())
.toLocalDate() .toLocalDate()
.format(DateUtils.getShortDateFormatter()) .format(DateUtils.getShortDateFormatter())
) )
.format() .format()
else -> null
} }
}.stateIn(viewModelScope, SharingStarted.Lazily, null) }.stateIn(viewModelScope, SharingStarted.Lazily, null)
val showRecreateGroupButton: StateFlow<Boolean> = isAdmin val showRecreateGroupButton: StateFlow<Boolean> =
.map { admin -> combine(isAdmin, legacyGroupDeprecationManager.deprecationState) { admin, state ->
admin && recipient?.isLegacyGroupRecipient == true admin && recipient?.isLegacyGroupRecipient == true
&& state != LegacyGroupDeprecationManager.DeprecationState.NOT_DEPRECATING
}.stateIn(viewModelScope, SharingStarted.Lazily, false) }.stateIn(viewModelScope, SharingStarted.Lazily, false)
private val attachmentDownloadHandler = AttachmentDownloadHandler( 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.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.EditGroupActivity import org.thoughtcrime.securesms.groups.EditGroupActivity
import org.thoughtcrime.securesms.groups.EditLegacyGroupActivity import org.thoughtcrime.securesms.groups.legacy.EditLegacyGroupActivity
import org.thoughtcrime.securesms.groups.EditLegacyGroupActivity.Companion.groupIDKey import org.thoughtcrime.securesms.groups.legacy.EditLegacyGroupActivity.Companion.groupIDKey
import org.thoughtcrime.securesms.groups.GroupMembersActivity import org.thoughtcrime.securesms.groups.GroupMembersActivity
import org.thoughtcrime.securesms.media.MediaOverviewActivity import org.thoughtcrime.securesms.media.MediaOverviewActivity
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions

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

@ -39,7 +39,8 @@ class DebugMenuViewModel @Inject constructor(
hideNoteToSelf = textSecurePreferences.hasHiddenNoteToSelf(), hideNoteToSelf = textSecurePreferences.hasHiddenNoteToSelf(),
forceDeprecationState = deprecationManager.deprecationStateOverride.value, forceDeprecationState = deprecationManager.deprecationStateOverride.value,
availableDeprecationState = listOf(null) + LegacyGroupDeprecationManager.DeprecationState.entries.toList(), availableDeprecationState = listOf(null) + LegacyGroupDeprecationManager.DeprecationState.entries.toList(),
forceDeprecatedTime = deprecationManager.deprecationTime.value deprecatedTime = deprecationManager.deprecatedTime.value,
deprecatingStartTime = deprecationManager.deprecatingStartTime.value,
) )
) )
val uiState: StateFlow<UIState> val uiState: StateFlow<UIState>
@ -77,7 +78,12 @@ class DebugMenuViewModel @Inject constructor(
is Commands.OverrideDeprecatedTime -> { is Commands.OverrideDeprecatedTime -> {
deprecationManager.overrideDeprecatedTime(command.time) 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 hideNoteToSelf: Boolean,
val forceDeprecationState: LegacyGroupDeprecationManager.DeprecationState?, val forceDeprecationState: LegacyGroupDeprecationManager.DeprecationState?,
val availableDeprecationState: List<LegacyGroupDeprecationManager.DeprecationState?>, val availableDeprecationState: List<LegacyGroupDeprecationManager.DeprecationState?>,
val forceDeprecatedTime: ZonedDateTime val deprecatedTime: ZonedDateTime,
val deprecatingStartTime: ZonedDateTime,
) )
sealed class Commands { sealed class Commands {
@ -142,5 +149,6 @@ class DebugMenuViewModel @Inject constructor(
data class HideNoteToSelf(val hide: Boolean) : Commands() data class HideNoteToSelf(val hide: Boolean) : Commands()
data class OverrideDeprecationState(val state: LegacyGroupDeprecationManager.DeprecationState?) : Commands() data class OverrideDeprecationState(val state: LegacyGroupDeprecationManager.DeprecationState?) : Commands()
data class OverrideDeprecatedTime(val time: ZonedDateTime) : 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 android.content.Context
import org.thoughtcrime.securesms.dependencies.DatabaseComponent 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 members = groupDatabase.getGroupMembers(groupID, true)
val zombieMembers = groupDatabase.getGroupZombieMembers(groupID) val zombieMembers = groupDatabase.getGroupZombieMembers(groupID)
return EditLegacyGroupActivity.GroupMembers( return EditLegacyGroupActivity.GroupMembers(
members.map { members.map {
it.address.toString() it.address.toString()
}, },
zombieMembers.map { zombieMembers.map {
it.address.toString() 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.Context
import android.content.Intent import android.content.Intent
@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.contacts.SelectContactsActivity
import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.ClosedGroupEditingOptionsBottomSheet
import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup import org.thoughtcrime.securesms.groups.ClosedGroupManager.updateLegacyGroup
import org.thoughtcrime.securesms.util.fadeIn import org.thoughtcrime.securesms.util.fadeIn
import org.thoughtcrime.securesms.util.fadeOut 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 android.content.Context
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView

@ -4,7 +4,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" 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 <LinearLayout
android:id="@+id/mainContentContainer" android:id="@+id/mainContentContainer"

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

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

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.text.Selection import android.text.Selection
import androidx.test.platform.app.InstrumentationRegistry
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -69,7 +70,6 @@ class MentionViewModelTest {
fun setUp() { fun setUp() {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
mentionViewModel = MentionViewModel( mentionViewModel = MentionViewModel(
threadID,
contentResolver = mock { }, contentResolver = mock { },
threadDatabase = mock { threadDatabase = mock {
on { getRecipientForThreadId(threadID) } doAnswer { on { getRecipientForThreadId(threadID) } doAnswer {
@ -108,7 +108,9 @@ class MentionViewModelTest {
on { getOpenGroup(threadID) } doReturn openGroup on { getOpenGroup(threadID) } doReturn openGroup
}, },
dispatcher = StandardTestDispatcher(), 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() memberContacts[index].displayName(Contact.ContactContext.OPEN_GROUP).orEmpty()
MentionViewModel.Candidate( 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, name,
0 0
) )

@ -30,32 +30,48 @@ class LegacyGroupDeprecationManager(private val prefs: TextSecurePreferences) {
prefs.deprecatedTimeOverride ?: defaultDeprecatedTime 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") @Suppress("OPT_IN_USAGE")
val deprecationState: StateFlow<DeprecationState> val deprecationState: StateFlow<DeprecationState>
get() = combine(mutableDeprecationStateOverride, mutableDeprecatedTime, ::Pair) get() = combine(mutableDeprecationStateOverride,
.flatMapLatest { (overriding, deadline) -> mutableDeprecatedTime,
if (overriding != null) { mutableDeprecatingStartTime,
flowOf(overriding) ::Triple
} else { ).flatMapLatest { (overriding, deprecatedTime, deprecatingStartTime) ->
flow { if (overriding != null) {
val now = ZonedDateTime.now() flowOf(overriding)
} else {
if (now.isBefore(deadline)) { flow {
emit(DeprecationState.DEPRECATING) val now = ZonedDateTime.now()
delay(Duration.between(now, deadline).toMillis())
} if (now.isBefore(deprecatingStartTime)) {
emit(DeprecationState.NOT_DEPRECATING)
emit(DeprecationState.DEPRECATED) delay(Duration.between(now, deprecatingStartTime).toMillis())
} }
if (now.isBefore(deprecatedTime)) {
emit(DeprecationState.DEPRECATING)
delay(Duration.between(now, deprecatedTime).toMillis())
}
emit(DeprecationState.DEPRECATED)
} }
} }
.stateIn( }.stateIn(
scope = GlobalScope, scope = GlobalScope,
started = SharingStarted.Lazily, started = SharingStarted.Lazily,
initialValue = mutableDeprecationStateOverride.value ?: DeprecationState.DEPRECATING initialValue = mutableDeprecationStateOverride.value ?: DeprecationState.NOT_DEPRECATING
) )
fun overrideDeprecationState(deprecationState: DeprecationState?) { fun overrideDeprecationState(deprecationState: DeprecationState?) {
mutableDeprecationStateOverride.value = deprecationState mutableDeprecationStateOverride.value = deprecationState
@ -67,7 +83,13 @@ class LegacyGroupDeprecationManager(private val prefs: TextSecurePreferences) {
prefs.deprecatedTimeOverride = deprecatedTime prefs.deprecatedTimeOverride = deprecatedTime
} }
fun overrideDeprecatingStartTime(deprecatingStartTime: ZonedDateTime?) {
mutableDeprecatingStartTime.value = deprecatingStartTime ?: defaultDeprecatingStartTime
prefs.deprecatingStartTimeOverride = deprecatingStartTime
}
enum class DeprecationState { enum class DeprecationState {
NOT_DEPRECATING,
DEPRECATING, DEPRECATING,
DEPRECATED DEPRECATED
} }

@ -37,7 +37,7 @@ class LegacyClosedGroupPollerV2(
return isPolling[groupPublicKey] ?: false 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 { companion object {
private val minPollInterval = 4 * 1000 private val minPollInterval = 4 * 1000

@ -6,6 +6,5 @@ object NonTranslatableStringConstants {
const val SESSION_DOWNLOAD_URL = "https://getsession.org/download" const val SESSION_DOWNLOAD_URL = "https://getsession.org/download"
const val GIF = "GIF" const val GIF = "GIF"
const val OXEN_FOUNDATION = "Oxen Foundation" 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 deprecationStateOverride: String?
var deprecatedTimeOverride: ZonedDateTime? var deprecatedTimeOverride: ZonedDateTime?
var deprecatingStartTimeOverride: ZonedDateTime?
var migratedToGroupV2Config: Boolean var migratedToGroupV2Config: Boolean
@ -315,6 +316,7 @@ interface TextSecurePreferences {
const val DEPRECATED_STATE_OVERRIDE = "deprecation_state_override" const val DEPRECATED_STATE_OVERRIDE = "deprecation_state_override"
const val DEPRECATED_TIME_OVERRIDE = "deprecated_time_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. // 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 // 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()) 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