[SES-3251] - Add recreate group UI and show/hide thread/message options accordingly (#919)

* Add recreate button

* CreateGroup as part of dialog

* Show/hide items

* Address feedback

* Icon spacing

* Address feedback

* Avoiding leaking the popup window

---------

Co-authored-by: ThomasSession <thomas.r@getsession.org>
pull/1709/head
SessionHero01 3 months ago committed by GitHub
parent fc914f2667
commit 7187b79663
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -509,7 +509,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
setUpSearchResultObserver()
scrollToFirstUnreadMessageIfNeeded()
setUpOutdatedClientBanner()
setUpLegacyGroupBanner()
setUpLegacyGroupUI()
if (author != null && messageTimestamp >= 0 && targetPosition >= 0) {
binding.conversationRecyclerView.scrollToPosition(targetPosition)
@ -543,6 +543,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
setupMentionView()
setupUiEventsObserver()
}
private fun setupUiEventsObserver() {
lifecycleScope.launch {
viewModel.uiEvents.collect { event ->
when (event) {
is ConversationUiEvent.NavigateToConversation -> {
finish()
startActivity(Intent(this@ConversationActivityV2, ConversationActivityV2::class.java)
.putExtra(THREAD_ID, event.threadId)
)
}
}
}
}
}
private fun setupMentionView() {
@ -829,7 +845,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
private fun setUpLegacyGroupBanner() {
private fun setUpLegacyGroupUI() {
lifecycleScope.launch {
viewModel.legacyGroupBanner
.collectLatest { banner ->
@ -843,12 +859,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// we need to add the inline icon
val drawable = ContextCompat.getDrawable(this@ConversationActivityV2, R.drawable.ic_external)!!
val imageSize = toPx(10, resources)
val imagePaddingTop = toPx(4, resources)
val imagePadding = toPx(4, resources)
drawable.setBounds(0, 0, imageSize, imageSize)
drawable.setTint(getColorFromAttr(R.attr.message_sent_text_color))
setSpan(
PaddedImageSpan(drawable, ImageSpan.ALIGN_BASELINE, imagePaddingTop),
PaddedImageSpan(drawable, ImageSpan.ALIGN_BASELINE,
paddingStart = imagePadding,
paddingTop = imagePadding
),
length - 1,
length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
@ -861,6 +880,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
}
}
lifecycleScope.launch {
viewModel.showRecreateGroupButton
.collectLatest { show ->
binding.recreateGroupButtonContainer.isVisible = show
}
}
binding.recreateGroupButton.setOnClickListener {
viewModel.onCommand(ConversationViewModel.Commands.RecreateGroup)
}
}
private fun setUpLinkPreviewObserver() {
@ -963,6 +993,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
thread = recipient,
context = this,
configFactory = configFactory,
deprecationManager = viewModel.legacyGroupDeprecationManager
)
}
maybeUpdateToolbar(recipient)

@ -33,19 +33,23 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.session.libsession.LocalisedTimeUtil.toShortTwoPartString
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper.userCanBanSelectedUsers
import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.util.AnimationCompleteListener
import org.thoughtcrime.securesms.util.DateUtils
@ -95,6 +99,11 @@ class ConversationReactionOverlay : FrameLayout {
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
@Inject lateinit var repository: ConversationRepository
@Inject lateinit var lokiThreadDatabase: LokiThreadDatabase
@Inject lateinit var threadDatabase: ThreadDatabase
@Inject lateinit var textSecurePreferences: TextSecurePreferences
@Inject lateinit var deprecationManager: LegacyGroupDeprecationManager
private val scope = CoroutineScope(Dispatchers.Default)
private var job: Job? = null
@ -163,7 +172,8 @@ class ConversationReactionOverlay : FrameLayout {
private fun showAfterLayout(messageRecord: MessageRecord,
lastSeenDownPoint: PointF,
isMessageOnLeft: Boolean) {
val contextMenu = ConversationContextMenu(dropdownAnchor, getMenuActionItems(messageRecord))
val recipient = threadDatabase.getRecipientForThreadId(messageRecord.threadId)
val contextMenu = ConversationContextMenu(dropdownAnchor, recipient?.let { getMenuActionItems(messageRecord, it) }.orEmpty())
this.contextMenu = contextMenu
var endX = if (isMessageOnLeft) scrubberHorizontalMargin.toFloat() else selectedConversationModel.bubbleX - conversationItem.width + selectedConversationModel.bubbleWidth
var endY = selectedConversationModel.bubbleY - statusBarHeight
@ -260,6 +270,12 @@ class ConversationReactionOverlay : FrameLayout {
} else {
(width - scrubberWidth - scrubberHorizontalMargin).toFloat()
}
val isDeprecatedLegacyGroup =
recipient?.isLegacyGroupRecipient == true &&
deprecationManager.deprecationState.value == LegacyGroupDeprecationManager.DeprecationState.DEPRECATED
foregroundView.isVisible = !isDeprecatedLegacyGroup
backgroundView.isVisible = !isDeprecatedLegacyGroup
foregroundView.x = scrubberX
foregroundView.y = reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.height / 2f
backgroundView.x = scrubberX
@ -359,6 +375,12 @@ class ConversationReactionOverlay : FrameLayout {
updateBoundsOnLayoutChanged()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
hide()
}
private fun updateBoundsOnLayoutChanged() {
backgroundView.getGlobalVisibleRect(emojiStripViewBounds)
emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect)
@ -521,16 +543,14 @@ class ConversationReactionOverlay : FrameLayout {
.firstOrNull()
?.let(ReactionRecord::emoji)
private fun getMenuActionItems(message: MessageRecord): List<ActionItem> {
private fun getMenuActionItems(message: MessageRecord, recipient: Recipient): List<ActionItem> {
val items: MutableList<ActionItem> = ArrayList()
// Prepare
val containsControlMessage = message.isUpdate
val hasText = !message.body.isEmpty()
val openGroup = get(context).lokiThreadDatabase().getOpenGroupChat(message.threadId)
val recipient = get(context).threadDatabase().getRecipientForThreadId(message.threadId)
?: return emptyList()
val userPublicKey = getLocalNumber(context)!!
val openGroup = lokiThreadDatabase.getOpenGroupChat(message.threadId)
val userPublicKey = textSecurePreferences.getLocalNumber()!!
// control messages and "marked as deleted" messages can only delete
val isDeleteOnly = message.isDeleted || message.isControlMessage
@ -544,9 +564,15 @@ class ConversationReactionOverlay : FrameLayout {
R.string.AccessibilityId_select
)
}
val isDeprecatedLegacyGroup = recipient.isLegacyGroupRecipient &&
deprecationManager.deprecationState.value == LegacyGroupDeprecationManager.DeprecationState.DEPRECATED
// Reply
val canWrite = openGroup == null || openGroup.canWrite
if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation && !isDeleteOnly) {
if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation && !isDeleteOnly
&& !isDeprecatedLegacyGroup) {
items += ActionItem(R.attr.menu_reply_icon, R.string.reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply)
}
// Copy message text
@ -558,14 +584,23 @@ class ConversationReactionOverlay : FrameLayout {
items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
}
// Delete message
items += ActionItem(R.attr.menu_trash_icon, R.string.delete, { handleActionItemClicked(Action.DELETE) },
R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
if (!isDeprecatedLegacyGroup) {
items += ActionItem(
R.attr.menu_trash_icon,
R.string.delete,
{ handleActionItemClicked(Action.DELETE) },
R.string.AccessibilityId_deleteMessage,
message.subtitle,
ThemeUtil.getThemedColor(context, R.attr.danger)
)
}
// Ban user
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey) && !isDeleteOnly) {
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey) && !isDeleteOnly && !isDeprecatedLegacyGroup) {
items += ActionItem(R.attr.menu_block_icon, R.string.banUser, { handleActionItemClicked(Action.BAN_USER) })
}
// Ban and delete all
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey) && !isDeleteOnly) {
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey) && !isDeleteOnly && !isDeprecatedLegacyGroup) {
items += ActionItem(R.attr.menu_trash_icon, R.string.banDeleteAll, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
}
// Message detail
@ -576,11 +611,11 @@ class ConversationReactionOverlay : FrameLayout {
{ handleActionItemClicked(Action.VIEW_INFO) })
}
// Resend
if (message.isFailed) {
if (message.isFailed && !isDeprecatedLegacyGroup) {
items += ActionItem(R.attr.menu_reply_icon, R.string.resend, { handleActionItemClicked(Action.RESEND) })
}
// Resync
if (message.isSyncFailed) {
if (message.isSyncFailed && !isDeprecatedLegacyGroup) {
items += ActionItem(R.attr.menu_reply_icon, R.string.resync, { handleActionItemClicked(Action.RESYNC) })
}
// Save media..

@ -2,7 +2,10 @@ package org.thoughtcrime.securesms.conversation.v2
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -16,11 +19,16 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.squareup.phrase.Phrase
import network.loki.messenger.R
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.*
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.DeleteForEveryoneDialogData
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.ClearEmoji
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.ConfirmRecreateGroup
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideClearEmoji
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteEveryoneDialog
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideRecreateGroupConfirm
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedForEveryone
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedLocally
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.ShowOpenUrlDialog
import org.thoughtcrime.securesms.groups.compose.CreateGroupScreen
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.GetString
@ -33,6 +41,7 @@ import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConversationV2Dialogs(
dialogsState: ConversationViewModel.DialogsState,
@ -162,6 +171,54 @@ fun ConversationV2Dialogs(
)
)
}
if (dialogsState.recreateGroupConfirm) {
AlertDialog(
onDismissRequest = {
// hide dialog
sendCommand(HideRecreateGroupConfirm)
},
title = stringResource(R.string.recreateGroup),
text = stringResource(R.string.legacyGroupChatHistory),
buttons = listOf(
DialogButtonModel(
text = GetString(stringResource(id = R.string.theContinue)),
color = LocalColors.current.danger,
onClick = {
sendCommand(ConfirmRecreateGroup)
}
),
DialogButtonModel(
GetString(stringResource(R.string.cancel))
)
)
)
}
if (dialogsState.recreateGroupData != null) {
val state = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
onDismissRequest = {
sendCommand(ConversationViewModel.Commands.HideRecreateGroup)
},
sheetState = state,
dragHandle = null
) {
CreateGroupScreen(
fromLegacyGroupId = dialogsState.recreateGroupData.legacyGroupId,
onNavigateToConversationScreen = { threadId ->
sendCommand(ConversationViewModel.Commands.NavigateToConversation(threadId))
},
onBack = {
sendCommand(ConversationViewModel.Commands.HideRecreateGroup)
},
onClose = {
sendCommand(ConversationViewModel.Commands.HideRecreateGroup)
},
)
}
}
}
}

@ -2,6 +2,7 @@ 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
@ -14,10 +15,11 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
@ -31,6 +33,7 @@ import network.loki.messenger.R
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.open_groups.OpenGroupApi
@ -58,7 +61,6 @@ import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.repository.ConversationRepository
@ -66,7 +68,6 @@ import org.thoughtcrime.securesms.util.DateUtils
import java.time.ZoneId
import java.util.UUID
@OptIn(ExperimentalCoroutinesApi::class)
class ConversationViewModel(
val threadId: Long,
val edKeyPair: KeyPair?,
@ -81,7 +82,7 @@ class ConversationViewModel(
private val textSecurePreferences: TextSecurePreferences,
private val configFactory: ConfigFactory,
private val groupManagerV2: GroupManagerV2,
private val legacyGroupDeprecationManager: LegacyGroupDeprecationManager,
val legacyGroupDeprecationManager: LegacyGroupDeprecationManager,
) : ViewModel() {
val showSendAfterApprovalText: Boolean
@ -90,6 +91,9 @@ class ConversationViewModel(
private val _uiState = MutableStateFlow(ConversationUiState())
val uiState: StateFlow<ConversationUiState> get() = _uiState
private val _uiEvents = MutableSharedFlow<ConversationUiEvent>(extraBufferCapacity = 1)
val uiEvents: SharedFlow<ConversationUiEvent> get() = _uiEvents
private val _dialogsState = MutableStateFlow(DialogsState())
val dialogsState: StateFlow<DialogsState> = _dialogsState
@ -218,6 +222,10 @@ class ConversationViewModel(
}
}.stateIn(viewModelScope, SharingStarted.Lazily, null)
val showRecreateGroupButton: StateFlow<Boolean> = isAdmin
.map { admin ->
admin && recipient?.isLegacyGroupRecipient == true
}.stateIn(viewModelScope, SharingStarted.Lazily, false)
private val attachmentDownloadHandler = AttachmentDownloadHandler(
storage = storage,
@ -949,6 +957,37 @@ class ConversationViewModel(
is Commands.ClearEmoji -> {
clearEmoji(command.emoji, command.messageId)
}
Commands.RecreateGroup -> {
_dialogsState.update {
it.copy(recreateGroupConfirm = true)
}
}
Commands.HideRecreateGroupConfirm -> {
_dialogsState.update {
it.copy(recreateGroupConfirm = false)
}
}
Commands.ConfirmRecreateGroup -> {
_dialogsState.update {
it.copy(
recreateGroupConfirm = false,
recreateGroupData = recipient?.address?.serialize()?.let { addr -> RecreateGroupDialogData(legacyGroupId = addr) }
)
}
}
Commands.HideRecreateGroup -> {
_dialogsState.update {
it.copy(recreateGroupData = null)
}
}
is Commands.NavigateToConversation -> {
_uiEvents.tryEmit(ConversationUiEvent.NavigateToConversation(command.threadId))
}
}
}
@ -1056,7 +1095,13 @@ class ConversationViewModel(
data class DialogsState(
val openLinkDialogUrl: String? = null,
val clearAllEmoji: ClearAllEmoji? = null,
val deleteEveryone: DeleteForEveryoneDialogData? = null
val deleteEveryone: DeleteForEveryoneDialogData? = null,
val recreateGroupConfirm: Boolean = false,
val recreateGroupData: RecreateGroupDialogData? = null,
)
data class RecreateGroupDialogData(
val legacyGroupId: String,
)
data class DeleteForEveryoneDialogData(
@ -1073,16 +1118,22 @@ class ConversationViewModel(
val messageId: MessageId
)
sealed class Commands {
data class ShowOpenUrlDialog(val url: String?) : Commands()
sealed interface Commands {
data class ShowOpenUrlDialog(val url: String?) : Commands
data class ClearEmoji(val emoji:String, val messageId: MessageId) : Commands()
data class ClearEmoji(val emoji:String, val messageId: MessageId) : Commands
data object HideDeleteEveryoneDialog : Commands()
data object HideClearEmoji : Commands()
data object HideDeleteEveryoneDialog : Commands
data object HideClearEmoji : Commands
data class MarkAsDeletedLocally(val messages: Set<MessageRecord>): Commands()
data class MarkAsDeletedForEveryone(val data: DeleteForEveryoneDialogData): Commands()
data class MarkAsDeletedLocally(val messages: Set<MessageRecord>): Commands
data class MarkAsDeletedForEveryone(val data: DeleteForEveryoneDialogData): Commands
data object RecreateGroup : Commands
data object ConfirmRecreateGroup : Commands
data object HideRecreateGroupConfirm : Commands
data object HideRecreateGroup : Commands
data class NavigateToConversation(val threadId: Long) : Commands
}
}
@ -1097,6 +1148,10 @@ data class ConversationUiState(
val showLoader: Boolean = false,
)
sealed interface ConversationUiEvent {
data class NavigateToConversation(val threadId: Long) : ConversationUiEvent
}
sealed interface MessageRequestUiState {
data object Invisible : MessageRequestUiState

@ -28,6 +28,7 @@ import java.io.IOException
import network.loki.messenger.R
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.leave
import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID
@ -70,14 +71,21 @@ object ConversationMenuHelper {
thread: Recipient,
context: Context,
configFactory: ConfigFactory,
deprecationManager: LegacyGroupDeprecationManager,
) {
val isDeprecatedLegacyGroup = thread.isLegacyGroupRecipient &&
deprecationManager.deprecationState.value == LegacyGroupDeprecationManager.DeprecationState.DEPRECATED
// Prepare
menu.clear()
val isCommunity = thread.isCommunityRecipient
// Base menu (options that should always be present)
inflater.inflate(R.menu.menu_conversation, menu)
menu.findItem(R.id.menu_add_shortcut).isVisible = !isDeprecatedLegacyGroup
// Expiring messages
if (!isCommunity && (thread.hasApprovedMe() || thread.isLegacyGroupRecipient || thread.isLocalNumber)) {
if (!isCommunity && (thread.hasApprovedMe() || thread.isLegacyGroupRecipient || thread.isLocalNumber)
&& !isDeprecatedLegacyGroup) {
inflater.inflate(R.menu.menu_conversation_expiration, menu)
}
// One-on-one chat menu allows copying the account id
@ -95,6 +103,8 @@ object ConversationMenuHelper {
// (Legacy) Closed group menu (options that should only be present in closed groups)
if (thread.isLegacyGroupRecipient) {
inflater.inflate(R.menu.menu_conversation_legacy_group, menu)
menu.findItem(R.id.menu_edit_group).isVisible = !isDeprecatedLegacyGroup
}
// Groups v2 menu
@ -118,13 +128,15 @@ object ConversationMenuHelper {
inflater.inflate(R.menu.menu_conversation_open_group, menu)
}
// Muting
if (!isDeprecatedLegacyGroup) {
if (thread.isMuted) {
inflater.inflate(R.menu.menu_conversation_muted, menu)
} else {
inflater.inflate(R.menu.menu_conversation_unmuted, menu)
}
}
if (thread.isGroupOrCommunityRecipient && !thread.isMuted) {
if (thread.isGroupOrCommunityRecipient && !thread.isMuted && !isDeprecatedLegacyGroup) {
inflater.inflate(R.menu.menu_conversation_notification_settings, menu)
}

@ -33,7 +33,8 @@ class CreateGroupFragment : Fragment() {
)
},
onBack = delegate::onDialogBackPressed,
onClose = delegate::onDialogClosePressed
onClose = delegate::onDialogClosePressed,
fromLegacyGroupId = null,
)
}
}

@ -3,6 +3,9 @@ package org.thoughtcrime.securesms.groups
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@ -15,17 +18,21 @@ import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.textSizeInBytes
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import javax.inject.Inject
@HiltViewModel
class CreateGroupViewModel @Inject constructor(
@HiltViewModel(assistedFactory = CreateGroupViewModel.Factory::class)
class CreateGroupViewModel @AssistedInject constructor(
configFactory: ConfigFactory,
@ApplicationContext private val appContext: Context,
private val storage: StorageProtocol,
private val groupManagerV2: GroupManagerV2,
groupDatabase: GroupDatabase,
@Assisted createFromLegacyGroupId: String?,
): ViewModel() {
// Child view model to handle contact selection logic
val selectContactsViewModel = SelectContactsViewModel(
@ -51,6 +58,30 @@ class CreateGroupViewModel @Inject constructor(
private val mutableEvents = MutableSharedFlow<CreateGroupEvent>()
val events: SharedFlow<CreateGroupEvent> get() = mutableEvents
init {
// When a legacy group ID is given, fetch the group details and pre-fill the name and members
createFromLegacyGroupId?.let { id ->
mutableIsLoading.value = true
viewModelScope.launch(Dispatchers.Default) {
try {
groupDatabase.getGroup(id).orNull()?.let { group ->
mutableGroupName.value = group.title
val myPublicKey = storage.getUserPublicKey()
selectContactsViewModel.selectAccountIDs(
group.members
.asSequence()
.filter { it.serialize() != myPublicKey }
.mapTo(mutableSetOf()) { AccountId(it.serialize()) }
)
}
} finally {
mutableIsLoading.value = false
}
}
}
}
fun onCreateClicked() {
viewModelScope.launch {
val groupName = groupName.value.trim()
@ -104,6 +135,11 @@ class CreateGroupViewModel @Inject constructor(
mutableGroupNameError.value = ""
}
@AssistedFactory
interface Factory {
fun create(createFromLegacyGroupId: String?): CreateGroupViewModel
}
}
sealed interface CreateGroupEvent {

@ -35,7 +35,7 @@ class SelectContactsViewModel @AssistedInject constructor(
private val configFactory: ConfigFactory,
@ApplicationContext private val appContext: Context,
@Assisted private val excludingAccountIDs: Set<AccountId>,
@Assisted private val scope: CoroutineScope
@Assisted private val scope: CoroutineScope,
) : ViewModel() {
// Input: The search query
private val mutableSearchQuery = MutableStateFlow("")
@ -114,6 +114,10 @@ class SelectContactsViewModel @AssistedInject constructor(
mutableSelectedContactAccountIDs.value = newSet
}
fun selectAccountIDs(accountIDs: Set<AccountId>) {
mutableSelectedContactAccountIDs.value += accountIDs
}
@AssistedFactory
interface Factory {
fun create(

@ -50,11 +50,15 @@ import org.thoughtcrime.securesms.ui.theme.PreviewTheme
@Composable
fun CreateGroupScreen(
fromLegacyGroupId: String?,
onNavigateToConversationScreen: (threadID: Long) -> Unit,
onBack: () -> Unit,
onClose: () -> Unit,
) {
val viewModel: CreateGroupViewModel = hiltViewModel()
val viewModel = hiltViewModel<CreateGroupViewModel, CreateGroupViewModel.Factory> { factory ->
factory.create(fromLegacyGroupId)
}
val context = LocalContext.current
LaunchedEffect(viewModel) {

@ -11,6 +11,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding
import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.getGroup
import org.session.libsession.utilities.wasKickedFromGroupV2
@ -32,6 +33,7 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
var group: GroupRecord? = null
@Inject lateinit var configFactory: ConfigFactory
@Inject lateinit var deprecationManager: LegacyGroupDeprecationManager
var onViewDetailsTapped: (() -> Unit?)? = null
var onCopyConversationId: (() -> Unit?)? = null
@ -80,15 +82,28 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
} else {
binding.detailsTextView.visibility = View.GONE
}
binding.copyConversationId.visibility = if (!recipient.isGroupOrCommunityRecipient && !recipient.isLocalNumber) View.VISIBLE else View.GONE
val isDeprecatedLegacyGroup = recipient.isLegacyGroupRecipient &&
deprecationManager.deprecationState.value == LegacyGroupDeprecationManager.DeprecationState.DEPRECATED
binding.copyConversationId.isVisible = !recipient.isGroupOrCommunityRecipient
&& !recipient.isLocalNumber
&& !isDeprecatedLegacyGroup
binding.copyConversationId.setOnClickListener(this)
binding.copyCommunityUrl.visibility = if (recipient.isCommunityRecipient) View.VISIBLE else View.GONE
binding.copyCommunityUrl.isVisible = recipient.isCommunityRecipient
binding.copyCommunityUrl.setOnClickListener(this)
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
&& !isDeprecatedLegacyGroup
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
&& !isDeprecatedLegacyGroup
binding.unMuteNotificationsTextView.setOnClickListener(this)
binding.muteNotificationsTextView.setOnClickListener(this)
binding.notificationsTextView.isVisible = recipient.isGroupOrCommunityRecipient && !recipient.isMuted
&& !isDeprecatedLegacyGroup
binding.notificationsTextView.setOnClickListener(this)
// delete
@ -132,11 +147,12 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, drawableStartRes, 0, 0, 0)
}
binding.markAllAsReadTextView.isVisible = thread.unreadCount > 0 ||
configFactory.withUserConfigs { it.convoInfoVolatile.getConversationUnread(thread) }
binding.markAllAsReadTextView.isVisible = (thread.unreadCount > 0 ||
configFactory.withUserConfigs { it.convoInfoVolatile.getConversationUnread(thread) })
&& !isDeprecatedLegacyGroup
binding.markAllAsReadTextView.setOnClickListener(this)
binding.pinTextView.isVisible = !thread.isPinned
binding.unpinTextView.isVisible = thread.isPinned
binding.pinTextView.isVisible = !thread.isPinned && !isDeprecatedLegacyGroup
binding.unpinTextView.isVisible = thread.isPinned && !isDeprecatedLegacyGroup
binding.pinTextView.setOnClickListener(this)
binding.unpinTextView.setOnClickListener(this)
}

@ -8,7 +8,8 @@ import android.text.style.ImageSpan
class PaddedImageSpan(
drawable: Drawable,
verticalAlignment: Int,
private val paddingTop: Int
private val paddingTop: Int,
private val paddingStart: Int
) : ImageSpan(drawable, verticalAlignment) {
override fun draw(
@ -25,10 +26,11 @@ class PaddedImageSpan(
val drawable = drawable
canvas.save()
// Adjust the image's vertical position by adding the top padding
// Adjust the image's top and start with padding
val transX = x + paddingStart
val transY = top + paddingTop
canvas.translate(x, transY.toFloat())
canvas.translate(transX, transY.toFloat())
drawable.draw(canvas)
canvas.restore()
}

@ -60,7 +60,7 @@
android:layout_height="wrap_content"
tools:layout_height="60dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageRequestBar"
app:layout_constraintTop_toBottomOf="@+id/recreateGroupButtonContainer"
app:layout_constraintBottom_toBottomOf="parent"
/>
@ -306,7 +306,7 @@
android:id="@+id/messageRequestBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/inputBar"
app:layout_constraintBottom_toTopOf="@+id/recreateGroupButtonContainer"
app:layout_constraintTop_toBottomOf="@+id/textSendAfterApproval"
android:layout_marginBottom="@dimen/large_spacing"
android:orientation="vertical"
@ -366,6 +366,26 @@
</LinearLayout>
<FrameLayout
android:id="@+id/recreateGroupButtonContainer"
app:layout_constraintBottom_toTopOf="@+id/inputBar"
app:layout_constraintTop_toBottomOf="@+id/messageRequestBar"
android:padding="@dimen/medium_spacing"
android:visibility="gone"
tools:visibility="visible"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/recreateGroupButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.Session.Button.Common.ProminentOutline"
android:contentDescription="@string/AccessibilityId_messageRequestsAccept"
android:text="@string/recreateGroup" />
</FrameLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_open_url"
tools:visibility="gone"

Loading…
Cancel
Save