diff --git a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt index f5f08e696d..729679f372 100644 --- a/app/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/app/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -112,7 +112,6 @@ interface StorageProtocol { fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) - fun isLegacyClosedGroup(publicKey: String): Boolean fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? fun updateFormationTimestamp(groupID: String, formationTimestamp: Long) diff --git a/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt b/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt index 672133e18a..02dc65fd0a 100644 --- a/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt +++ b/app/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt @@ -22,7 +22,6 @@ interface LokiAPIDatabaseProtocol { fun setOpenGroupPublicKey(server: String, newValue: String) fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): List fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? - fun isClosedGroup(groupPublicKey: String): Boolean fun getForkInfo(): ForkInfo fun setForkInfo(forkInfo: ForkInfo) fun migrateLegacyOpenGroup(legacyServerId: String, newServerId: String) diff --git a/app/src/main/java/org/session/libsignal/utilities/AccountId.kt b/app/src/main/java/org/session/libsignal/utilities/AccountId.kt index bce895ba6c..eae18afb7d 100644 --- a/app/src/main/java/org/session/libsignal/utilities/AccountId.kt +++ b/app/src/main/java/org/session/libsignal/utilities/AccountId.kt @@ -50,5 +50,7 @@ data class AccountId( null } } + + fun hasValidLength(candidate: String) = candidate.length == 66 } } diff --git a/app/src/main/java/org/session/libsignal/utilities/Validation.kt b/app/src/main/java/org/session/libsignal/utilities/Validation.kt deleted file mode 100644 index eaa1fa1bab..0000000000 --- a/app/src/main/java/org/session/libsignal/utilities/Validation.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.session.libsignal.utilities - -object PublicKeyValidation { - private val HEX_CHARACTERS = "0123456789ABCDEFabcdef".toSet() - private val INVALID_PREFIXES = setOf(IdPrefix.GROUP, IdPrefix.BLINDED, IdPrefix.BLINDEDV2) - - fun isValid(candidate: String, isPrefixRequired: Boolean = true): Boolean { - if (!hasValidLength(candidate)) return false - - val prefix = IdPrefix.fromValue(candidate) - - // Handle invalid Account ID conditions - // Case 1: Standard prefix "05" but not valid hex - if (prefix == IdPrefix.STANDARD && !isValidHexEncoding(candidate)) return false - - // Case 2: Blinded or Group IDs should never be accepted as valid Account IDs - if (prefix in INVALID_PREFIXES) return false - - // Standard validity rules - return isValidHexEncoding(candidate) && - (!isPrefixRequired || prefix != null) - } - - fun hasValidPrefix(candidate: String) = IdPrefix.fromValue(candidate) !in INVALID_PREFIXES - fun hasValidLength(candidate: String) = candidate.length == 66 - private fun isValidHexEncoding(candidate: String) = HEX_CHARACTERS.containsAll(candidate.toSet()) -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/InputBarDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/InputBarDialogs.kt index ee95964ebb..3b3f3e2ead 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/InputBarDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/InputBarDialogs.kt @@ -3,8 +3,8 @@ package org.thoughtcrime.securesms import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable -import org.thoughtcrime.securesms.ui.AlertDialog -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.LongMessageProCTA import org.thoughtcrime.securesms.ui.components.annotatedStringResource diff --git a/app/src/main/java/org/thoughtcrime/securesms/InputbarViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/InputbarViewModel.kt index 73079c9725..01a65e4b26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/InputbarViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/InputbarViewModel.kt @@ -12,7 +12,7 @@ import org.session.libsession.utilities.StringSubstitutionConstants.LIMIT_KEY import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.pro.ProStatus import org.thoughtcrime.securesms.pro.ProStatusManager -import org.thoughtcrime.securesms.ui.SimpleDialogData +import org.thoughtcrime.securesms.ui.dialog.SimpleDialogData import org.thoughtcrime.securesms.util.NumberUtil // the amount of character left at which point we should show an indicator diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 3e970a6203..9abd758d8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -143,7 +143,7 @@ import org.thoughtcrime.securesms.components.TypingStatusSender import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener -import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.ShowOpenUrlDialog +import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HandleLink import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_COPY import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY @@ -562,7 +562,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } // endregion - fun showOpenUrlDialog(url: String) = viewModel.onCommand(ShowOpenUrlDialog(url)) + fun handleLink(url: String) = viewModel.onCommand(HandleLink(url)) // region Lifecycle override fun onCreate(savedInstanceState: Bundle?) { @@ -837,7 +837,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val intent = ConversationSettingsActivity.createIntent( context = this@ConversationActivityV2, address = event.address, - startDestination = ConversationV3Destination.RouteDisappearingMessages + startDestination = ConversationV3Destination.RouteDisappearingMessages(event.address) ) startActivity(intent) } @@ -1264,7 +1264,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } binding.conversationHeader.outdatedGroupBanner.setOnClickListener { - showOpenUrlDialog("https://getsession.org/groups") + handleLink("https://getsession.org/groups") } } } @@ -3194,4 +3194,4 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt index e2e89f8ab3..73685c288e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationV2Dialogs.kt @@ -18,28 +18,26 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.squareup.phrase.Phrase import network.loki.messenger.R -import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY -import org.session.libsession.utilities.recipients.displayName import org.thoughtcrime.securesms.InputBarDialogs import org.thoughtcrime.securesms.InputbarViewModel import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.* import org.thoughtcrime.securesms.home.startconversation.group.CreateGroupScreen -import org.thoughtcrime.securesms.ui.AlertDialog -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.links.LinkType +import org.thoughtcrime.securesms.ui.dialog.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.RadioOption import org.thoughtcrime.securesms.ui.UserProfileModal import org.thoughtcrime.securesms.ui.components.DialogTitledRadioButton import org.thoughtcrime.securesms.ui.components.annotatedStringResource +import org.thoughtcrime.securesms.ui.dialog.LinkAlertDialog import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme -import kotlin.text.format @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -93,13 +91,16 @@ fun ConversationV2Dialogs( sendCommand = sendInputBarCommand ) - // open link confirmation - if(!dialogsState.openLinkDialogUrl.isNullOrEmpty()){ - OpenURLAlertDialog( - url = dialogsState.openLinkDialogUrl, + // Link dialogs + if(dialogsState.urlDialog != null){ + LinkAlertDialog( + data = dialogsState.urlDialog, onDismissRequest = { // hide dialog - sendCommand(ShowOpenUrlDialog(null)) + sendCommand(HideOpenUrlDialog) + }, + openOrJoinCommunity = { + sendCommand(OpenOrJoinCommunity(it)) } ) } @@ -273,30 +274,6 @@ fun ConversationV2Dialogs( ) } - // Join community - if(dialogsState.joinCommunity != null){ - AlertDialog( - onDismissRequest = { - // hide dialog - sendCommand(HideJoinCommunityDialog) - }, - title = stringResource(R.string.communityJoin), - text = Phrase.from(LocalContext.current, R.string.communityJoinDescription) - .put(COMMUNITY_NAME_KEY, dialogsState.joinCommunity.communityName).format().toString(), - buttons = listOf( - DialogButtonData( - text = GetString(stringResource(id = R.string.join)), - onClick = { - sendCommand(JoinCommunity(dialogsState.joinCommunity.communityUrl)) - } - ), - DialogButtonData( - GetString(stringResource(R.string.cancel)) - ) - ) - ) - } - // Attachment downloads if(dialogsState.attachmentDownload != null){ AlertDialog( @@ -333,7 +310,7 @@ fun PreviewURLDialog(){ PreviewTheme { ConversationV2Dialogs( dialogsState = ConversationViewModel.DialogsState( - openLinkDialogUrl = "https://google.com" + urlDialog = LinkType.GenericLink("https://google.com") ), inputBarDialogsState = InputbarViewModel.InputBarDialogsState(), sendCommand = {}, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index c809dadf94..615efbba1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -108,13 +108,15 @@ import org.thoughtcrime.securesms.database.model.GroupThreadStatus 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.links.LinkChecker +import org.thoughtcrime.securesms.links.LinkType import org.thoughtcrime.securesms.database.model.NotifyType import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.groups.ExpiredGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.repository.ConversationRepository -import org.thoughtcrime.securesms.ui.SimpleDialogData +import org.thoughtcrime.securesms.ui.dialog.SimpleDialogData import org.thoughtcrime.securesms.ui.components.ConversationAppBarData import org.thoughtcrime.securesms.ui.components.ConversationAppBarPagerData import org.thoughtcrime.securesms.ui.getSubbedString @@ -163,6 +165,7 @@ class ConversationViewModel @AssistedInject constructor( private val upmFactory: UserProfileUtils.UserProfileUtilsFactory, attachmentDownloadHandlerFactory: AttachmentDownloadHandler.Factory, private val openGroupManager: OpenGroupManager, + private val linkChecker: LinkChecker, private val attachmentDownloadJobFactory: AttachmentDownloadJob.Factory, private val communityApiExecutor: CommunityApiExecutor, private val deleteAllReactionsApiFactory: DeleteAllReactionsApi.Factory, @@ -1209,18 +1212,36 @@ class ConversationViewModel @AssistedInject constructor( } fun confirmCommunityJoin(communityName: String, communityUrl: String){ - _dialogsState.update { - it.copy( - joinCommunity = JoinCommunityDialogData( - communityName = communityName, - communityUrl = communityUrl - ) + viewModelScope.launch { + val detectedLink = linkChecker.check(communityUrl) as? LinkType.CommunityLink + val resolvedName = communityName.takeIf { it.isNotBlank() } ?: detectedLink?.name ?: communityUrl + val link = detectedLink?.copy( + name = resolvedName, + ) ?: LinkType.CommunityLink( + url = communityUrl, + name = resolvedName, + joined = false, + displayType = LinkType.CommunityLink.DisplayType.CONVERSATION ) + + _dialogsState.update { + it.copy( + urlDialog = link, + ) + } } } - private fun joinCommunity(url: String){ - val openGroup = OpenGroupUrlParser.parseUrl(url) + private fun openOrJoinCommunity(url: String){ + val openGroup = try { + OpenGroupUrlParser.parseUrl(url) + } catch (_: OpenGroupUrlParser.Error) { + Toast.makeText(application, R.string.communityEnterUrlErrorInvalidDescription, Toast.LENGTH_SHORT) + .show() + return + } + + _dialogsState.update { it.copy(urlDialog = null) } viewModelScope.launch { try { @@ -1229,6 +1250,11 @@ class ConversationViewModel @AssistedInject constructor( room = openGroup.room, publicKey = openGroup.serverPublicKey, ) + + // after joining or if already joined, open the conversation + _uiEvents.tryEmit(ConversationUiEvent.NavigateToConversation( + address = Address.Community(openGroup.server, openGroup.room), + )) } catch (e: Exception) { Log.e("", "Error joining community", e) withContext(Dispatchers.Main) { @@ -1239,6 +1265,16 @@ class ConversationViewModel @AssistedInject constructor( } } + private fun handleLink(url: String) { + viewModelScope.launch { + _dialogsState.update { + it.copy( + urlDialog = linkChecker.check(url), + ) + } + } + } + /** * Implicitly approve the recipient. * @@ -1258,9 +1294,13 @@ class ConversationViewModel @AssistedInject constructor( fun onCommand(command: Commands) { when (command) { - is Commands.ShowOpenUrlDialog -> { + is Commands.HandleLink -> { + handleLink(command.url) + } + + Commands.HideOpenUrlDialog -> { _dialogsState.update { - it.copy(openLinkDialogUrl = command.url) + it.copy(urlDialog = null) } } @@ -1332,14 +1372,14 @@ class ConversationViewModel @AssistedInject constructor( userProfileModalUtils?.onCommand(command.upmCommand) } - is Commands.JoinCommunity -> { - joinCommunity(command.url) + is Commands.OpenOrJoinCommunity -> { + openOrJoinCommunity(command.url) } is Commands.HideJoinCommunityDialog -> { _dialogsState.update { it.copy( - joinCommunity = null + urlDialog = null ) } } @@ -1565,21 +1605,15 @@ class ConversationViewModel @AssistedInject constructor( data class DialogsState( val showSimpleDialog: SimpleDialogData? = null, - val openLinkDialogUrl: String? = null, val clearAllEmoji: ClearAllEmoji? = null, val deleteEveryone: DeleteForEveryoneDialogData? = null, val recreateGroupConfirm: Boolean = false, val recreateGroupData: RecreateGroupDialogData? = null, val userProfileModal: UserProfileModalData? = null, - val joinCommunity: JoinCommunityDialogData? = null, + val urlDialog: LinkType? = null, val attachmentDownload: ConfirmAttachmentDownloadDialogData? = null ) - data class JoinCommunityDialogData( - val communityName: String, - val communityUrl: String - ) - data class ConfirmAttachmentDownloadDialogData( val attachment: DatabaseAttachment, val conversationName: String @@ -1604,7 +1638,8 @@ class ConversationViewModel @AssistedInject constructor( ) sealed interface Commands { - data class ShowOpenUrlDialog(val url: String?) : Commands + data class HandleLink(val url: String) : Commands + data object HideOpenUrlDialog : Commands data class ClearEmoji(val emoji:String, val messageId: MessageId) : Commands @@ -1625,7 +1660,7 @@ class ConversationViewModel @AssistedInject constructor( val upmCommand: UserProfileModalCommands ): Commands - data class JoinCommunity(val url: String): Commands + data class OpenOrJoinCommunity(val url: String): Commands data object HideJoinCommunityDialog: Commands data class DownloadAttachments(val attachment: DatabaseAttachment): Commands diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt index e693c84f7f..9fc4336d28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -87,7 +87,7 @@ class LinkPreviewView : LinearLayout { private fun openURL() { val url = this.url ?: return Log.w("LinkPreviewView", "Cannot open a null URL") val activity = context as? ConversationActivityV2 - activity?.showOpenUrlDialog(url) + activity?.handleLink(url) } // endregion -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index c36a7c9708..c621c1fb7e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -540,7 +540,7 @@ class VisibleMessageContentView : ConstraintLayout { val updatedUrl = urlSpan.url.toHttpUrlOrNull()?.toString() ?: urlSpan.url val replacementSpan = ModalURLSpan(updatedUrl) { url -> val activity = context as? ConversationActivityV2 - activity?.showOpenUrlDialog(url) + activity?.handleLink(url) } val start = body.getSpanStart(urlSpan) val end = body.getSpanEnd(urlSpan) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationActivityV3.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationActivityV3.kt index 997bd6099e..235396be22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationActivityV3.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationActivityV3.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -39,6 +40,7 @@ class ConversationActivityV3 : FullComposeScreenLockActivity() { } override fun onCreate(savedInstanceState: Bundle?) { + // calling before onCreate to make sure the composeContent has the right value straight away pendingScrollMessageId = extractScrollMessageId(intent) super.onCreate(savedInstanceState) } @@ -51,24 +53,29 @@ class ConversationActivityV3 : FullComposeScreenLockActivity() { @Composable override fun ComposeContent() { + val initialAddress: Address.Conversable? = IntentCompat.getParcelableExtra(intent, ADDRESS, Address.Conversable::class.java) + if (initialAddress == null) { + LaunchedEffect(Unit) { + finish() + } + return + } + val startDestination = IntentCompat.getParcelableExtra( intent, EXTRA_START_DESTINATION, ConversationV3Destination::class.java - ) ?: ConversationV3Destination.RouteConversation + ) ?: ConversationV3Destination.RouteConversation(initialAddress) ConversationV3NavHost( - //todo convov3 v2 convo would go back home if address is null - update this - address = requireNotNull(IntentCompat.getParcelableExtra(intent, ADDRESS, Address.Conversable::class.java)) { - "ConversationV3Activity requires an Address to be passed in the intent." - }, + initialAddress = initialAddress, startDestination = startDestination, pendingScrollMessageId = pendingScrollMessageId, onPendingScrollConsumed = { pendingScrollMessageId = null }, - switchConvoVersion = { - startActivity(ConversationActivityV2.createIntent(this, address = IntentCompat.getParcelableExtra(intent, ADDRESS, Address.Conversable::class.java)!!)) + switchConvoVersion = { address -> + startActivity(ConversationActivityV2.createIntent(this, address = address)) finish() }, onBack = this::finish diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationCommand.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationCommand.kt index f8f9eb2721..7012fe0e60 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationCommand.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationCommand.kt @@ -30,16 +30,15 @@ sealed interface ConversationCommand { val highlight: Boolean = true, ) : ScreenCommand, MessageCommand - data class OpenUrl(val url: String) : MessageCommand - data object HideOpenUrlDialog : DialogCommand + data class HandleLink(val url: String) : MessageCommand + data object HideUrlDialog : DialogCommand data class ClearEmoji(val emoji: String, val messageId: MessageId) : DialogCommand data object HideDeleteEveryoneDialog : DialogCommand data object HideClearEmoji : DialogCommand data class MarkAsDeletedLocally(val messages: Set) : DialogCommand data class MarkAsDeletedForEveryone(val data: DeleteForEveryoneDialogData) : DialogCommand - data class JoinCommunity(val url: String) : DialogCommand - data object HideJoinCommunityDialog : DialogCommand + data class OpenOrJoinCommunity(val url: String) : DialogCommand data class DownloadAttachments(val attachment: DatabaseAttachment) : DialogCommand data object HideAttachmentDownloadDialog : DialogCommand diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationModels.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationModels.kt index 94a878d70d..ab90556b33 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationModels.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationModels.kt @@ -4,7 +4,8 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt import org.session.libsession.utilities.recipients.MessageType import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord -import org.thoughtcrime.securesms.ui.SimpleDialogData +import org.thoughtcrime.securesms.links.LinkType +import org.thoughtcrime.securesms.ui.dialog.SimpleDialogData import org.thoughtcrime.securesms.util.UserProfileModalData data class ConversationScrollState( @@ -17,21 +18,15 @@ data class ConversationScrollState( data class ConversationDialogsState( val showSimpleDialog: SimpleDialogData? = null, - val openLinkDialogUrl: String? = null, + val urlDialog: LinkType? = null, val clearAllEmoji: ClearAllEmoji? = null, val deleteEveryone: DeleteForEveryoneDialogData? = null, val recreateGroupConfirm: Boolean = false, val recreateGroupData: RecreateGroupDialogData? = null, val userProfileModal: UserProfileModalData? = null, - val joinCommunity: JoinCommunityDialogData? = null, val attachmentDownload: ConfirmAttachmentDownloadDialogData? = null ) -data class JoinCommunityDialogData( - val communityName: String, - val communityUrl: String -) - data class ConfirmAttachmentDownloadDialogData( val attachment: DatabaseAttachment, val conversationName: String diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationV3NavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationV3NavHost.kt index f95d190c9a..f7e810d5cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationV3NavHost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationV3NavHost.kt @@ -65,16 +65,31 @@ import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.ui.handleIntent import org.thoughtcrime.securesms.ui.horizontalSlideComposable +private fun String.toConversableAddress(): Address.Conversable = + Address.fromSerialized(this) as? Address.Conversable + ?: error("Expected a conversable address but got: $this") + // Destinations sealed interface ConversationV3Destination: Parcelable { @Serializable @Parcelize - data object RouteConversation: ConversationV3Destination + data class RouteConversation private constructor( + private val serializedAddress: String + ): ConversationV3Destination { + constructor(address: Address.Conversable): this(address.address) + val address: Address.Conversable get() = serializedAddress.toConversableAddress() + } @Serializable @Parcelize - data object RouteConversationSettings: ConversationV3Destination + data class RouteConversationSettings private constructor( + private val serializedAddress: String + ): ConversationV3Destination { + constructor(address: Address.Conversable): this(address.address) + + val address: Address.Conversable get() = serializedAddress.toConversableAddress() + } @Serializable @Parcelize @@ -134,15 +149,33 @@ sealed interface ConversationV3Destination: Parcelable { @Serializable @Parcelize - data object RouteDisappearingMessages: ConversationV3Destination + data class RouteDisappearingMessages private constructor( + private val serializedAddress: String + ): ConversationV3Destination { + constructor(address: Address.Conversable): this(address.address) + + val address: Address.Conversable get() = serializedAddress.toConversableAddress() + } @Serializable @Parcelize - data object RouteAllMedia: ConversationV3Destination + data class RouteAllMedia private constructor( + private val serializedAddress: String + ): ConversationV3Destination { + constructor(address: Address.Conversable): this(address.address) + + val address: Address.Conversable get() = serializedAddress.toConversableAddress() + } @Serializable @Parcelize - data object RouteNotifications: ConversationV3Destination + data class RouteNotifications private constructor( + private val serializedAddress: String + ): ConversationV3Destination { + constructor(address: Address.Conversable): this(address.address) + + val address: Address.Conversable get() = serializedAddress.toConversableAddress() + } @Serializable @Parcelize @@ -167,11 +200,11 @@ sealed interface ConversationV3Destination: Parcelable { @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun ConversationV3NavHost( - address: Address.Conversable, - startDestination: ConversationV3Destination = RouteConversation, + initialAddress: Address.Conversable, + startDestination: ConversationV3Destination = RouteConversation(initialAddress), pendingScrollMessageId: MessageId? = null, onPendingScrollConsumed: () -> Unit = {}, - switchConvoVersion: () -> Unit, + switchConvoVersion: (Address.Conversable) -> Unit, onBack: () -> Unit ){ SharedTransitionLayout { @@ -206,10 +239,12 @@ fun ConversationV3NavHost( NavHost(navController = navController, startDestination = startDestination) { // Main conversation screen - horizontalSlideComposable { + horizontalSlideComposable { backStackEntry -> + val data: RouteConversation = backStackEntry.toRoute() + val viewModel = hiltViewModel { factory -> - factory.create(address, navigator) + factory.create(data.address, navigator) } LaunchedEffect(pendingScrollMessageId) { @@ -227,16 +262,19 @@ fun ConversationV3NavHost( ConversationScreen( viewModel = viewModel, - switchConvoVersion = switchConvoVersion, + address = data.address, + switchConvoVersion = { switchConvoVersion(data.address) }, onBack = onBack, ) } // Conversation Settings - horizontalSlideComposable { + horizontalSlideComposable { backStackEntry -> + val data: RouteConversationSettings = backStackEntry.toRoute() + val viewModel = hiltViewModel { factory -> - factory.create(address, navigator) + factory.create(data.address, navigator) } val lifecycleOwner = LocalLifecycleOwner.current @@ -349,9 +387,7 @@ fun ConversationV3NavHost( // grab a hold of settings' VM val parentEntry = remember(backStackEntry) { - navController.getBackStackEntry( - RouteConversationSettings - ) + navController.previousBackStackEntry ?: error("RouteConversationSettings not in backstack") } val settingsViewModel: ConversationSettingsViewModel = hiltViewModel(parentEntry) @@ -384,7 +420,10 @@ fun ConversationV3NavHost( ) } - val newMessageViewModel = hiltViewModel() + val newMessageViewModel = hiltViewModel{ factory -> + factory.create(allowCommunityUrl = false) + } + val uiState by newMessageViewModel.state.collectAsState(State()) // grab a hold of manage group's VM @@ -455,11 +494,13 @@ fun ConversationV3NavHost( } // Disappearing Messages - horizontalSlideComposable { + horizontalSlideComposable { backStackEntry -> + val data: RouteDisappearingMessages = backStackEntry.toRoute() + val viewModel: DisappearingMessagesViewModel = hiltViewModel { factory -> factory.create( - address = address, + address = data.address, isNewConfigEnabled = ExpirationConfiguration.isNewConfigEnabled, showDebugOptions = BuildConfig.BUILD_TYPE != "release", navigator = navigator @@ -475,10 +516,12 @@ fun ConversationV3NavHost( } // All Media - horizontalSlideComposable { + horizontalSlideComposable { backStackEntry -> + val data: RouteAllMedia = backStackEntry.toRoute() + val viewModel = hiltViewModel { factory -> - factory.create(address) + factory.create(data.address) } MediaOverviewScreen( @@ -490,10 +533,12 @@ fun ConversationV3NavHost( } // Notifications - horizontalSlideComposable { + horizontalSlideComposable { backStackEntry -> + val data: RouteNotifications = backStackEntry.toRoute() + val viewModel = hiltViewModel { factory -> - factory.create(address) + factory.create(data.address) } NotificationSettingsScreen( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationV3ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationV3ViewModel.kt index 8cc7d2eca7..d790fa6c1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationV3ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/ConversationV3ViewModel.kt @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.conversation.v3 import android.content.Context +import android.widget.Toast +import androidx.navigation.NavOptionsBuilder import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.paging.PagingConfig @@ -44,17 +46,17 @@ import network.loki.messenger.libsession_util.util.ExpiryMode import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.LegacyGroupDeprecationManager import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.utilities.Address import org.session.libsession.utilities.ExpirationUtil +import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.StringSubstitutionConstants.TIME_KEY -import org.session.libsession.utilities.recipients.MessageType import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.RecipientData import org.session.libsession.utilities.recipients.displayName import org.session.libsession.utilities.recipients.effectiveNotifyType import org.session.libsession.utilities.recipients.repeatedWithEffectiveNotifyTypeChange import org.session.libsession.utilities.toGroupString +import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.InputbarViewModel import org.thoughtcrime.securesms.database.AttachmentDatabase import org.thoughtcrime.securesms.database.GroupDatabase @@ -64,8 +66,9 @@ import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.RecipientSettingsDatabase import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageId -import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.NotifyType +import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.links.LinkChecker import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.ui.components.ConversationAppBarData @@ -93,6 +96,8 @@ class ConversationV3ViewModel @AssistedInject constructor( private val attachmentDatabase: AttachmentDatabase, private val reactionDb: ReactionDatabase, private val dataMapper: ConversationDataMapper, + private val openGroupManager: OpenGroupManager, + private val linkChecker: LinkChecker, private val proStatusManager: ProStatusManager, ) : InputbarViewModel( context = context, @@ -288,7 +293,7 @@ class ConversationV3ViewModel @AssistedInject constructor( pagerData += ConversationAppBarPagerData( title = getNotificationStatusTitle(effectiveNotifyType), action = { - navigateTo(ConversationV3Destination.RouteNotifications) + navigateTo(ConversationV3Destination.RouteNotifications(address)) } ) } @@ -311,7 +316,9 @@ class ConversationV3ViewModel @AssistedInject constructor( title = title, action = { // This pager title no longer actionable for legacy groups - if (conversation.isCommunityRecipient) navigateTo(ConversationV3Destination.RouteConversationSettings) + if (conversation.isCommunityRecipient) { + navigateTo(ConversationV3Destination.RouteConversationSettings(address)) + } else if (conversation.address is Address.Group) navigateTo(ConversationV3Destination.RouteGroupMembers(conversation.address)) }, ) @@ -360,7 +367,54 @@ class ConversationV3ViewModel @AssistedInject constructor( } } - navigateTo(ConversationV3Destination.RouteDisappearingMessages) + navigateTo(ConversationV3Destination.RouteDisappearingMessages(address)) + } + } + + private fun handleLink(url: String) { + viewModelScope.launch { + _dialogsState.update { + it.copy( + urlDialog = linkChecker.check(url), + ) + } + } + } + + private fun openOrJoinCommunity(url: String) { + val openGroup = try { + OpenGroupUrlParser.parseUrl(url) + } catch (_: OpenGroupUrlParser.Error) { + Toast.makeText(context, R.string.communityEnterUrlErrorInvalidDescription, Toast.LENGTH_SHORT) + .show() + return + } + + _dialogsState.update { it.copy(urlDialog = null) } + + viewModelScope.launch { + try { + openGroupManager.add( + server = openGroup.server, + room = openGroup.room, + publicKey = openGroup.serverPublicKey, + ) + + // after joining or if already joined, open the conversation + val communityAddress = Address.Community(openGroup.server, openGroup.room) + navigateTo( + destination = ConversationV3Destination.RouteConversation(communityAddress), + ) { + //todo convov3 confirm that we want a new stack + popUpTo(ConversationV3Destination.RouteConversation(address)) { + inclusive = true + } + } + } catch (e: Exception) { + Log.e("ConversationV3ViewModel", "Error joining community", e) + Toast.makeText(context, R.string.communityErrorDescription, Toast.LENGTH_SHORT) + .show() + } } } @@ -396,15 +450,13 @@ class ConversationV3ViewModel @AssistedInject constructor( } // Dialog related - is ConversationCommand.OpenUrl -> { - _dialogsState.update { - it.copy(openLinkDialogUrl = command.url) - } + is ConversationCommand.HandleLink -> { + handleLink(command.url) } - ConversationCommand.HideOpenUrlDialog -> { + ConversationCommand.HideUrlDialog -> { _dialogsState.update { - it.copy(openLinkDialogUrl = null) + it.copy(urlDialog = null) } } @@ -467,17 +519,8 @@ class ConversationV3ViewModel @AssistedInject constructor( //userProfileModalUtils?.onCommand(command.upmCommand) } - is ConversationCommand.JoinCommunity -> { - //todo convov3 implement - //joinCommunity(command.url) - } - - ConversationCommand.HideJoinCommunityDialog -> { - _dialogsState.update { - it.copy( - joinCommunity = null - ) - } + is ConversationCommand.OpenOrJoinCommunity -> { + openOrJoinCommunity(command.url) } is ConversationCommand.DownloadAttachments -> { @@ -519,9 +562,15 @@ class ConversationV3ViewModel @AssistedInject constructor( } } - private fun navigateTo(destination: ConversationV3Destination){ + private fun navigateTo( + destination: ConversationV3Destination, + navOptions: NavOptionsBuilder.() -> Unit = {} + ){ viewModelScope.launch { - navigator.navigate(destination) + navigator.navigate( + destination = destination, + navOptions = navOptions, + ) } } @AssistedFactory diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/conversation/ConversationScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/conversation/ConversationScreen.kt index e1fa652a6c..1217229bb2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/conversation/ConversationScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/conversation/ConversationScreen.kt @@ -39,6 +39,8 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.collections.immutable.persistentListOf +import org.session.libsession.utilities.Address +import org.session.libsignal.utilities.AccountId import org.thoughtcrime.securesms.conversation.v3.ConversationCommand import org.thoughtcrime.securesms.conversation.v3.ConversationDataMapper.ConversationItem import org.thoughtcrime.securesms.conversation.v3.ConversationScrollState @@ -70,6 +72,7 @@ import kotlin.math.absoluteValue @Composable fun ConversationScreen( viewModel: ConversationV3ViewModel, + address: Address.Conversable, switchConvoVersion: () -> Unit, onBack: () -> Unit, ) { @@ -78,6 +81,7 @@ fun ConversationScreen( val conversationItems = viewModel.conversationItems.collectAsLazyPagingItems() Conversation( + address = address, conversationState = conversationState, appBarData = appBarData, conversationItems = conversationItems, @@ -107,6 +111,7 @@ fun ConversationScreen( @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @Composable fun Conversation( + address: Address.Conversable, conversationState: ConversationV3ViewModel.UIState, appBarData: ConversationAppBarData, conversationItems: LazyPagingItems, @@ -144,7 +149,9 @@ fun Conversation( onAvatarPressed = { sendCommand( ConversationCommand.GoTo( - ConversationV3Destination.RouteConversationSettings + ConversationV3Destination.RouteConversationSettings( + address + ) ) ) } @@ -249,6 +256,7 @@ fun PreviewConversation( ) { PreviewTheme(colors) { Conversation( + address = Address.Standard(AccountId("053d30141d0d35d9c4b30a8f8880f8464e221ee71a8aff9f0dcefb1e60145cea5144")), conversationState = ConversationV3ViewModel.UIState(), appBarData = ConversationAppBarData( title ="Friendo", diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/conversation/ConversationV3Dialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/conversation/ConversationV3Dialogs.kt index 67a77a5fa8..839e386924 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/conversation/ConversationV3Dialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/conversation/ConversationV3Dialogs.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.squareup.phrase.Phrase import network.loki.messenger.R -import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY import org.thoughtcrime.securesms.InputBarDialogs @@ -31,26 +30,25 @@ import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HandleUser import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HideAttachmentDownloadDialog import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HideClearEmoji import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HideDeleteEveryoneDialog -import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HideJoinCommunityDialog -import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HideOpenUrlDialog +import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HideUrlDialog import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HideRecreateGroup import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HideRecreateGroupConfirm import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HideSimpleDialog import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.HideUserProfileModal -import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.JoinCommunity +import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.OpenOrJoinCommunity import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.MarkAsDeletedForEveryone import org.thoughtcrime.securesms.conversation.v3.ConversationCommand.MarkAsDeletedLocally import org.thoughtcrime.securesms.conversation.v3.ConversationDialogsState -import org.thoughtcrime.securesms.conversation.v3.ConversationV3ViewModel import org.thoughtcrime.securesms.home.startconversation.group.CreateGroupScreen -import org.thoughtcrime.securesms.ui.AlertDialog -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.links.LinkType +import org.thoughtcrime.securesms.ui.dialog.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.RadioOption import org.thoughtcrime.securesms.ui.UserProfileModal import org.thoughtcrime.securesms.ui.components.DialogTitledRadioButton import org.thoughtcrime.securesms.ui.components.annotatedStringResource +import org.thoughtcrime.securesms.ui.dialog.LinkAlertDialog import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -109,13 +107,16 @@ fun ConversationV3Dialogs( sendCommand = sendInputBarCommand ) - // open link confirmation - if(!dialogsState.openLinkDialogUrl.isNullOrEmpty()){ - OpenURLAlertDialog( - url = dialogsState.openLinkDialogUrl, + // Link dialogs + if(dialogsState.urlDialog != null){ + LinkAlertDialog( + data = dialogsState.urlDialog, onDismissRequest = { // hide dialog - sendCommand(HideOpenUrlDialog) + sendCommand(HideUrlDialog) + }, + openOrJoinCommunity = { + sendCommand(OpenOrJoinCommunity(it)) } ) } @@ -289,29 +290,6 @@ fun ConversationV3Dialogs( ) } - // Join community - if(dialogsState.joinCommunity != null){ - AlertDialog( - onDismissRequest = { - // hide dialog - sendCommand(HideJoinCommunityDialog) - }, - title = stringResource(R.string.communityJoin), - text = Phrase.from(LocalContext.current, R.string.communityJoinDescription) - .put(COMMUNITY_NAME_KEY, dialogsState.joinCommunity.communityName).format().toString(), - buttons = listOf( - DialogButtonData( - text = GetString(stringResource(id = R.string.join)), - onClick = { - sendCommand(JoinCommunity(dialogsState.joinCommunity.communityUrl)) - } - ), - DialogButtonData( - GetString(stringResource(R.string.cancel)) - ) - ) - ) - } // Attachment downloads if(dialogsState.attachmentDownload != null){ @@ -349,7 +327,7 @@ fun PreviewURLDialog(){ PreviewTheme { ConversationV3Dialogs( dialogsState = ConversationDialogsState( - openLinkDialogUrl = "https://google.com" + urlDialog = LinkType.GenericLink("https://google.com") ), inputBarDialogsState = InputbarViewModel.InputBarDialogsState(), sendCommand = {}, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/BaseMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/BaseMessage.kt index 3a80851df3..451730a1e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/BaseMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/BaseMessage.kt @@ -330,7 +330,7 @@ fun MessageContentRenderer( isOutgoing = isOutgoing, isExpanded = expandedText, modifier = Modifier.padding(defaultMessageBubblePadding()), - onUrlClick = { sendCommand(ConversationCommand.OpenUrl(it)) }, + onUrlClick = { sendCommand(ConversationCommand.HandleLink(it)) }, onExpand = onExpandText ) @@ -365,7 +365,8 @@ fun MessageContentRenderer( CommunityInviteMessage( name = content.contentData.name, url = content.contentData.url, - outgoing = isOutgoing + outgoing = isOutgoing, + onInviteClick = { sendCommand(ConversationCommand.HandleLink(it)) } ) is MessageContentData.Media -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/CommunityInviteMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/CommunityInviteMessage.kt index b8e168a09c..1478989ac9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/CommunityInviteMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/CommunityInviteMessage.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v3.compose.message import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -25,6 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import network.loki.messenger.R +import org.session.libsession.utilities.OpenGroupUrlParser import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -38,10 +40,13 @@ fun CommunityInviteMessage( name: String, url: String, outgoing: Boolean, + onInviteClick: (String) -> Unit, modifier: Modifier = Modifier ) { Row( - modifier = modifier.padding(defaultMessageBubblePadding()) + modifier = modifier + .clickable(onClick = { onInviteClick(url) }) + .padding(defaultMessageBubblePadding()) .padding(vertical = LocalDimensions.current.tinySpacing), verticalAlignment = Alignment.CenterVertically ) { @@ -87,7 +92,7 @@ fun CommunityInviteMessage( ) Text( - text = url, + text = OpenGroupUrlParser.trimQueryParameter(url), style = LocalType.current.small, color = getTextColor(outgoing), maxLines = 2, @@ -116,7 +121,8 @@ fun CommunityInvitePreview( CommunityInviteMessage( name = "Test Community", url = "https://www.test-community-url.com/testing-the-url-look-and-feel", - outgoing = true + outgoing = true, + onInviteClick = {} ) } } @@ -130,7 +136,8 @@ fun CommunityInvitePreview( CommunityInviteMessage( name = "Test Community", url = "https://www.test-community-url.com/testing-the-url-look-and-feel", - outgoing = false + outgoing = false, + onInviteClick = {} ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/MessageLink.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/MessageLink.kt index 0c1ff28bb6..2ea08ac518 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/MessageLink.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/compose/message/MessageLink.kt @@ -52,7 +52,7 @@ fun MessageLink( modifier = modifier.fillMaxWidth() .background(color = blackAlpha06) .clickable { - sendCommand(ConversationCommand.OpenUrl(data.url)) + sendCommand(ConversationCommand.HandleLink(data.url)) }, ) { Box( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsActivity.kt index 472137bbc5..55e49123dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsActivity.kt @@ -19,7 +19,7 @@ class ConversationSettingsActivity: FullComposeScreenLockActivity() { fun createIntent( context: Context, address: Address.Conversable, - startDestination: ConversationV3Destination = ConversationV3Destination.RouteConversationSettings + startDestination: ConversationV3Destination = ConversationV3Destination.RouteConversationSettings(address) ): Intent { return Intent(context, ConversationSettingsActivity::class.java).apply { putExtra(THREAD_ADDRESS, address) @@ -30,16 +30,20 @@ class ConversationSettingsActivity: FullComposeScreenLockActivity() { @Composable override fun ComposeContent() { + val address = requireNotNull( + IntentCompat.getParcelableExtra(intent, THREAD_ADDRESS, Address.Conversable::class.java) + ) { + "ConversationSettingsActivity requires an Address to be passed in the intent." + } + val startDestination = IntentCompat.getParcelableExtra( intent, EXTRA_START_DESTINATION, ConversationV3Destination::class.java - ) ?: ConversationV3Destination.RouteConversationSettings + ) ?: ConversationV3Destination.RouteConversationSettings(address) ConversationSettingsNavHost( - address = requireNotNull(IntentCompat.getParcelableExtra(intent, THREAD_ADDRESS, Address.Conversable::class.java)) { - "ConversationSettingsActivity requires an Address to be passed in the intent." - }, + initialAddress = address, startDestination = startDestination, returnResult = { code, value -> setResult(RESULT_OK, Intent().putExtra(code, value)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsDialogs.kt index adb6226a54..fb26dae8e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsDialogs.kt @@ -38,9 +38,9 @@ import org.thoughtcrime.securesms.conversation.v3.settings.ConversationSettingsV import org.thoughtcrime.securesms.conversation.v3.settings.ConversationSettingsViewModel.Commands.UpdateGroupName import org.thoughtcrime.securesms.conversation.v3.settings.ConversationSettingsViewModel.Commands.UpdateNickname import org.thoughtcrime.securesms.pro.ProStatus -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.CTAImage -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GenericProCTA import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.PinProCTA diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsNavHost.kt index 45ec0fbf9a..9136063791 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsNavHost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsNavHost.kt @@ -53,8 +53,8 @@ import org.thoughtcrime.securesms.ui.horizontalSlideComposable @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun ConversationSettingsNavHost( - address: Address.Conversable, - startDestination: ConversationV3Destination = RouteConversationSettings, + initialAddress: Address.Conversable, + startDestination: ConversationV3Destination = RouteConversationSettings(initialAddress), returnResult: (String, Boolean) -> Unit, onBack: () -> Unit ){ @@ -94,10 +94,12 @@ fun ConversationSettingsNavHost( NavHost(navController = navController, startDestination = startDestination) { // Conversation Settings - horizontalSlideComposable { + horizontalSlideComposable { backStackEntry -> + val data: RouteConversationSettings = backStackEntry.toRoute() + val viewModel = hiltViewModel { factory -> - factory.create(address, navigator) + factory.create(data.address, navigator) } val lifecycleOwner = LocalLifecycleOwner.current @@ -208,9 +210,7 @@ fun ConversationSettingsNavHost( // grab a hold of settings' VM val parentEntry = remember(backStackEntry) { - navController.getBackStackEntry( - RouteConversationSettings - ) + navController.previousBackStackEntry ?: error("RouteConversationSettings not in backstack") } val settingsViewModel: ConversationSettingsViewModel = hiltViewModel(parentEntry) @@ -243,7 +243,10 @@ fun ConversationSettingsNavHost( ) } - val newMessageViewModel = hiltViewModel() + val newMessageViewModel = hiltViewModel{ factory -> + factory.create(allowCommunityUrl = false) + } + val uiState by newMessageViewModel.state.collectAsState(State()) // grab a hold of manage group's VM @@ -314,11 +317,13 @@ fun ConversationSettingsNavHost( } // Disappearing Messages - horizontalSlideComposable { + horizontalSlideComposable { backStackEntry -> + val data: RouteDisappearingMessages = backStackEntry.toRoute() + val viewModel: DisappearingMessagesViewModel = hiltViewModel { factory -> factory.create( - address = address, + address = data.address, isNewConfigEnabled = ExpirationConfiguration.isNewConfigEnabled, showDebugOptions = BuildConfig.BUILD_TYPE != "release", navigator = navigator @@ -334,10 +339,12 @@ fun ConversationSettingsNavHost( } // All Media - horizontalSlideComposable { + horizontalSlideComposable { backStackEntry -> + val data: RouteAllMedia = backStackEntry.toRoute() + val viewModel = hiltViewModel { factory -> - factory.create(address) + factory.create(data.address) } MediaOverviewScreen( @@ -349,10 +356,12 @@ fun ConversationSettingsNavHost( } // Notifications - horizontalSlideComposable { + horizontalSlideComposable { backStackEntry -> + val data: RouteNotifications = backStackEntry.toRoute() + val viewModel = hiltViewModel { factory -> - factory.create(address) + factory.create(data.address) } NotificationSettingsScreen( @@ -364,4 +373,4 @@ fun ConversationSettingsNavHost( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsScreen.kt index da28ca0ab3..be5255bc7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsScreen.kt @@ -48,7 +48,7 @@ import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.ExpandableText import org.thoughtcrime.securesms.ui.ItemButton -import org.thoughtcrime.securesms.ui.LoadingDialog +import org.thoughtcrime.securesms.ui.dialog.LoadingDialog import org.thoughtcrime.securesms.ui.components.AnnotatedTextWithIcon import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.components.annotatedStringResource diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsViewModel.kt index 4b1affed80..3cf9fdfb53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v3/settings/ConversationSettingsViewModel.kt @@ -64,7 +64,7 @@ import org.thoughtcrime.securesms.home.HomeActivity import org.thoughtcrime.securesms.pro.ProStatus import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.repository.ConversationRepository -import org.thoughtcrime.securesms.ui.SimpleDialogData +import org.thoughtcrime.securesms.ui.dialog.SimpleDialogData import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.ui.getSubbedString import org.thoughtcrime.securesms.util.AvatarUIData @@ -134,7 +134,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( qaTag = R.string.qa_conversation_settings_disappearing, subtitleQaTag = R.string.qa_conversation_settings_disappearing_sub, onClick = { - navigateTo(ConversationV3Destination.RouteDisappearingMessages) + navigateTo(ConversationV3Destination.RouteDisappearingMessages(address)) } ) } @@ -165,7 +165,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( qaTag = R.string.qa_conversation_settings_notifications, subtitleQaTag = R.string.qa_conversation_settings_notifications_sub, onClick = { - navigateTo(ConversationV3Destination.RouteNotifications) + navigateTo(ConversationV3Destination.RouteNotifications(address)) } ) } @@ -176,7 +176,7 @@ class ConversationSettingsViewModel @AssistedInject constructor( icon = R.drawable.ic_file, qaTag = R.string.qa_conversation_settings_attachments, onClick = { - navigateTo(ConversationV3Destination.RouteAllMedia) + navigateTo(ConversationV3Destination.RouteAllMedia(address)) } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt index e756cf542c..0852abcd48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt @@ -2,22 +2,18 @@ package org.thoughtcrime.securesms.database import android.content.ContentValues import android.content.Context -import androidx.collection.arrayMapOf import androidx.sqlite.db.SupportSQLiteDatabase -import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.ecc.DjbECPrivateKey import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.ForkInfo import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.PublicKeyValidation import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.removingIdPrefixIfNeeded import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.util.asSequence -import java.util.Date import javax.inject.Provider class LokiAPIDatabase(context: Context, helper: Provider) : Database(context, helper), LokiAPIDatabaseProtocol { @@ -438,11 +434,6 @@ class LokiAPIDatabase(context: Context, helper: Provider) : }.toSet() } - override fun isClosedGroup(groupPublicKey: String): Boolean { - if (!PublicKeyValidation.isValid(groupPublicKey)) { return false } - return getAllClosedGroupPublicKeys().contains(groupPublicKey) - } - fun removeClosedGroupPublicKey(groupPublicKey: String) { val database = writableDatabase database.delete(closedGroupPublicKeysTable, "${Companion.groupPublicKey} = ?", wrap(groupPublicKey)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index ac573afe31..c4ab09f2f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -673,10 +673,6 @@ open class Storage @Inject constructor( groupDatabase.updateMembers(groupID, members) } - override fun isLegacyClosedGroup(publicKey: String): Boolean { - return lokiAPIDatabase.isClosedGroup(publicKey) - } - override fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList { return lokiAPIDatabase.getClosedGroupEncryptionKeyPairs(groupPublicKey).toMutableList() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt index 2b76664e9a..8b04c5363f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt @@ -71,12 +71,12 @@ import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Companion.SEEN_2 import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Companion.SEEN_3 import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Companion.SEEN_4 import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Companion.TRUE -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.Cell -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.GetString -import org.thoughtcrime.securesms.ui.LoadingDialog +import org.thoughtcrime.securesms.ui.dialog.LoadingDialog import org.thoughtcrime.securesms.ui.components.SlimFillButtonRect import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.components.DropDown @@ -779,14 +779,14 @@ fun DebugMenu( onClick = { if (showingDeprecatedTimePicker) { sendCommand( - DebugMenuViewModel.Commands.OverrideDeprecatedTime( + OverrideDeprecatedTime( getPickedTime() ) ) showingDeprecatedTimePicker = false } else { sendCommand( - DebugMenuViewModel.Commands.OverrideDeprecatingStartTime( + OverrideDeprecatingStartTime( getPickedTime() ) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt index 407689f748..4a6b065b17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -43,10 +42,10 @@ import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.groups.ContactItem import org.thoughtcrime.securesms.groups.GroupMemberState import org.thoughtcrime.securesms.groups.InviteMembersViewModel -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.CollapsibleFooterAction import org.thoughtcrime.securesms.ui.CollapsibleFooterActionData -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.ProBadgeText import org.thoughtcrime.securesms.ui.RadioOption diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteAccountIdScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteAccountIdScreen.kt index 56610cbbe5..b6626a03f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteAccountIdScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/InviteAccountIdScreen.kt @@ -16,8 +16,10 @@ import kotlinx.coroutines.flow.emptyFlow import org.thoughtcrime.securesms.groups.InviteMembersViewModel import org.thoughtcrime.securesms.home.startconversation.newmessage.Callbacks import org.thoughtcrime.securesms.home.startconversation.newmessage.NewMessage +import org.thoughtcrime.securesms.home.startconversation.newmessage.NewMessageViewModel import org.thoughtcrime.securesms.home.startconversation.newmessage.State -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog +import org.thoughtcrime.securesms.ui.dialog.LinkAlertDialog +import org.thoughtcrime.securesms.ui.dialog.OpenURLAlertDialog @Composable internal fun InviteAccountIdScreen( @@ -56,7 +58,7 @@ private fun InviteAccountId( onHelp: () -> Unit = {}, onDismissHelpDialog: () -> Unit, onSendInvite: (Boolean) -> Unit, - onDismissInviteDialog: () -> Unit + onDismissInviteDialog: () -> Unit, ) { Scaffold( contentWindowInsets = WindowInsets.safeDrawing, @@ -87,10 +89,11 @@ private fun InviteAccountId( ) } - if(!state.showUrlDialog.isNullOrEmpty()) { - OpenURLAlertDialog( - url = state.showUrlDialog, - onDismissRequest = { onDismissHelpDialog() } + if (state.urlDialog != null) { + LinkAlertDialog( + data = state.urlDialog, + onDismissRequest = onDismissHelpDialog, + openOrJoinCommunity = {}//unused here ) } } @@ -104,7 +107,7 @@ fun PreviewInviteAccountId() { isTextErrorColor = false, error = null, loading = false, - showUrlDialog = null, + urlDialog = null, validIdFromQr = "", ), onBack = { }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupAdminsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupAdminsScreen.kt index 992fde9f85..0529a5b025 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupAdminsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupAdminsScreen.kt @@ -43,7 +43,7 @@ import org.thoughtcrime.securesms.ui.CollapsibleFooterItemData import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.ItemButton -import org.thoughtcrime.securesms.ui.LoadingDialog +import org.thoughtcrime.securesms.ui.dialog.LoadingDialog import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo import org.thoughtcrime.securesms.ui.components.annotatedStringResource import org.thoughtcrime.securesms.ui.getCellBottomShape diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupMembersScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupMembersScreen.kt index fd197fd347..ab598380a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupMembersScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/ManageGroupMembersScreen.kt @@ -46,15 +46,15 @@ import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.Se import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.SearchQueryChange import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.Commands.ToggleFooter import org.thoughtcrime.securesms.groups.ManageGroupMembersViewModel.OptionsItem -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.CollapsibleFooterActionData import org.thoughtcrime.securesms.ui.CollapsibleFooterItemData -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.ItemButton -import org.thoughtcrime.securesms.ui.LoadingDialog +import org.thoughtcrime.securesms.ui.dialog.LoadingDialog import org.thoughtcrime.securesms.ui.RadioOption import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo import org.thoughtcrime.securesms.ui.components.DialogTitledRadioButton diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/PromoteMembersScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/PromoteMembersScreen.kt index 5846d6844a..e0cfaf6313 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/PromoteMembersScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/PromoteMembersScreen.kt @@ -36,10 +36,10 @@ import org.thoughtcrime.securesms.groups.PromoteMembersViewModel.Commands.Search import org.thoughtcrime.securesms.groups.PromoteMembersViewModel.Commands.ShowConfirmDialog import org.thoughtcrime.securesms.groups.PromoteMembersViewModel.Commands.ShowPromoteDialog import org.thoughtcrime.securesms.groups.PromoteMembersViewModel.Commands.ToggleFooter -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.CollapsibleFooterActionData import org.thoughtcrime.securesms.ui.CollapsibleFooterItemData -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo import org.thoughtcrime.securesms.ui.components.annotatedStringResource diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index c5541df83e..5733c86925 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -297,10 +297,30 @@ class HomeActivity : ScreenLockActionBarActivity(), } } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + globalSearchViewModel.uiEvents.collect { event -> + when(event){ + is GlobalSearchViewModel.UiEvent.ShowUrlDialog -> { + homeViewModel.onCommand(HomeViewModel.Commands.ShowUrlDialog(event.linkType)) + } + + is GlobalSearchViewModel.UiEvent.ShowNewConversationDialog -> { + homeViewModel.onCommand(HomeViewModel.Commands.ShowNewConversationConfirmationDialog(event.address)) + } + } + } + } + } + lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { homeViewModel.uiEvents.collect { event -> when (event) { + is HomeViewModel.UiEvent.OpenConversation-> { + push(ConversationActivityV2.createIntent(this@HomeActivity, address = event.address)) + } + is HomeViewModel.UiEvent.OpenProSettings -> { startActivity( ProSettingsActivity.createIntent( diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt index 9831a9f6d2..b639eca838 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeDialogs.kt @@ -29,16 +29,17 @@ import org.thoughtcrime.securesms.home.HomeViewModel.Commands.OnLinkOpened import org.thoughtcrime.securesms.home.HomeViewModel.Commands.ShowDonationConfirmation import org.thoughtcrime.securesms.home.startconversation.StartConversationSheet import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsDestination -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.AnimatedSessionProCTA import org.thoughtcrime.securesms.ui.CTAFeature -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog +import org.thoughtcrime.securesms.ui.dialog.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.PinProCTA import org.thoughtcrime.securesms.ui.SimpleSessionProCTA import org.thoughtcrime.securesms.ui.UserProfileModal import org.thoughtcrime.securesms.ui.components.annotatedStringResource +import org.thoughtcrime.securesms.ui.dialog.LinkAlertDialog import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme @@ -234,12 +235,13 @@ fun HomeDialogs( ) } - if(dialogsState.showUrlDialog != null){ - OpenURLAlertDialog( - url = dialogsState.showUrlDialog, - onLinkOpened = { sendCommand(OnLinkOpened(dialogsState.showUrlDialog)) }, - onLinkCopied = { sendCommand(OnLinkCopied(dialogsState.showUrlDialog)) }, - onDismissRequest = { sendCommand(HideUrlDialog) } + if(dialogsState.urlDialog != null){ + LinkAlertDialog( + data = dialogsState.urlDialog, + onLinkOpened = { sendCommand(OnLinkOpened(dialogsState.urlDialog.url)) }, + onLinkCopied = { sendCommand(OnLinkCopied(dialogsState.urlDialog.url)) }, + onDismissRequest = { sendCommand(HideUrlDialog) }, + openOrJoinCommunity = { sendCommand(HomeViewModel.Commands.OpenOrJoinCommunity(it))} ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt index 995dcb7516..d2e21fbf19 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeViewModel.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.home import android.content.Context +import android.widget.Toast import androidx.lifecycle.ViewModel import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope @@ -27,12 +28,12 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.libsession_util.PRIORITY_HIDDEN import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.utilities.Address +import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.displayName @@ -41,11 +42,13 @@ import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.audio.AudioPlaybackManager import org.thoughtcrime.securesms.audio.model.AudioPlaybackState import org.thoughtcrime.securesms.auth.LoginStateRepository +import org.thoughtcrime.securesms.conversation.v3.ConversationV3Destination import org.thoughtcrime.securesms.database.RecipientRepository -import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.debugmenu.DebugLogGroup import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.links.LinkType import org.thoughtcrime.securesms.onboarding.OnBoardingPreferences.HAS_VIEWED_SEED import org.thoughtcrime.securesms.preferences.PreferenceStorage import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsDestination @@ -53,7 +56,7 @@ import org.thoughtcrime.securesms.pro.ProStatus import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository -import org.thoughtcrime.securesms.ui.SimpleDialogData +import org.thoughtcrime.securesms.ui.dialog.SimpleDialogData import org.thoughtcrime.securesms.ui.isWhitelistedFromDoze import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DonationManager @@ -85,6 +88,7 @@ class HomeViewModel @Inject constructor( private val dateUtils: DateUtils, private val donationManager: DonationManager, private val audioPlaybackManager: AudioPlaybackManager, + private val openGroupManager: OpenGroupManager, ) : ViewModel() { // SharedFlow that emits whenever the user asks us to reload the conversation private val manualReloadTrigger = MutableSharedFlow( @@ -412,7 +416,7 @@ class HomeViewModel @Inject constructor( } is Commands.HideUrlDialog -> { - _dialogsState.update { it.copy(showUrlDialog = null) } + _dialogsState.update { it.copy(urlDialog = null) } } is Commands.OnLinkOpened -> { @@ -428,6 +432,66 @@ class HomeViewModel @Inject constructor( donationManager.onDonationCopied() } } + + is Commands.OpenOrJoinCommunity -> openOrJoinCommunity(command.url) + + is Commands.ShowUrlDialog -> { + _dialogsState.update { it.copy(urlDialog = command.linkType) } + } + + is Commands.ShowNewConversationConfirmationDialog -> { + _dialogsState.update { + it.copy( + showSimpleDialog = SimpleDialogData( + title = context.getString(R.string.conversationsStart), + message = context.getString(R.string.globalSearchAccountId), + negativeText = context.getString(R.string.conversationsStart), + positiveText = context.getString(R.string.cancel), + positiveStyleDanger = false, + onNegative = { + viewModelScope.launch { + _uiEvents.emit(UiEvent.OpenConversation(command.address)) + } + }, + onPositive = { + onCommand(Commands.HideSimpleDialog) + }, + ) + ) + } + } + } + } + + private fun openOrJoinCommunity(url: String) { + val openGroup = try { + OpenGroupUrlParser.parseUrl(url) + } catch (_: OpenGroupUrlParser.Error) { + Toast.makeText(context, R.string.communityEnterUrlErrorInvalidDescription, Toast.LENGTH_SHORT) + .show() + return + } + + _dialogsState.update { it.copy(urlDialog = null) } + mutableIsSearchOpen.value = false + + viewModelScope.launch { + try { + openGroupManager.add( + server = openGroup.server, + room = openGroup.room, + publicKey = openGroup.serverPublicKey, + ) + + // after joining or if already joined, open the conversation + val communityAddress = Address.Community(openGroup.server, openGroup.room) + _uiEvents.emit(UiEvent.OpenConversation(communityAddress)) + + } catch (e: Exception) { + Log.e("HomeViewModel", "Error joining community", e) + Toast.makeText(context, R.string.communityErrorDescription, Toast.LENGTH_SHORT) + .show() + } } } @@ -437,7 +501,7 @@ class HomeViewModel @Inject constructor( } fun showUrlDialog(url: String) { - _dialogsState.update { it.copy(showUrlDialog = url) } + _dialogsState.update { it.copy(urlDialog = LinkType.GenericLink(url)) } } @@ -504,7 +568,7 @@ class HomeViewModel @Inject constructor( val proExpiredCTA: Boolean = false, val showSimpleDialog: SimpleDialogData? = null, val donationCTA: Boolean = false, - val showUrlDialog: String? = null, + val urlDialog: LinkType? = null, ) data class PinProCTA( @@ -521,6 +585,7 @@ class HomeViewModel @Inject constructor( ) sealed interface UiEvent { + data class OpenConversation(val address: Address.Conversable) : UiEvent data class OpenProSettings(val start: ProSettingsDestination) : UiEvent data object ShowWhiteListSystemDialog: UiEvent // once confirmed, this is for the system whitelist dialog } @@ -538,8 +603,11 @@ class HomeViewModel @Inject constructor( data object HideDonationCTADialog : Commands data object HideUserProfileModal : Commands data object HideUrlDialog : Commands + data class ShowUrlDialog(val linkType: LinkType) : Commands + data class ShowNewConversationConfirmationDialog(val address: Address.Conversable) : Commands data class OnLinkOpened(val url: String) : Commands data class OnLinkCopied(val url: String) : Commands + data class OpenOrJoinCommunity(val url: String) : Commands data class HandleUserProfileCommand( val upmCommand: UserProfileModalCommands ) : Commands diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt index 6549917798..068105a441 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchInputLayout.kt @@ -45,7 +45,6 @@ class GlobalSearchInputLayout @JvmOverloads constructor( super.onAttachedToWindow() binding.searchInput.onFocusChangeListener = this binding.searchInput.setOnEditorActionListener(this) - binding.searchInput.filters = arrayOf(LengthFilter(100)) // 100 char search limit binding.searchCancel.setOnClickListener { clearSearch() listener?.onCancelClicked() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt index 04b28c0ea4..7b5c4bf4fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchResult.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.home.search import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.links.LinkType import org.thoughtcrime.securesms.search.model.MessageResult import org.thoughtcrime.securesms.search.model.SearchResult @@ -10,7 +11,9 @@ data class GlobalSearchResult( val contacts: List = emptyList(), val threads: List = emptyList(), val messages: List = emptyList(), - val showNoteToSelf: Boolean = false + val showNoteToSelf: Boolean = false, + val urlDialog: LinkType? = null + ) { val isEmpty: Boolean get() = contacts.isEmpty() && threads.isEmpty() && messages.isEmpty() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt index bae01dc62d..1083a94a71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchViewModel.kt @@ -6,31 +6,45 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.withContext import network.loki.messenger.R +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.OpenGroupUrlParser +import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.links.LinkChecker +import org.thoughtcrime.securesms.links.LinkType import org.thoughtcrime.securesms.search.SearchRepository import javax.inject.Inject -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @HiltViewModel class GlobalSearchViewModel @Inject constructor( private val application: Application, private val searchRepository: SearchRepository, private val configFactory: ConfigFactory, private val threadDatabase: ThreadDatabase, + private val linkChecker: LinkChecker, + private val recipientRepository: RecipientRepository ) : ViewModel() { // The query text here is not the source of truth due to the limitation of Android view system @@ -45,6 +59,31 @@ class GlobalSearchViewModel @Inject constructor( val noteToSelfString: String by lazy { application.getString(R.string.noteToSelf).lowercase() } + private val _uiEvents = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1 + ) + val uiEvents: SharedFlow = _uiEvents + + init { + // Deriving UI events from query changes + _queryText + .debounce(300L) + .distinctUntilChanged() + .mapLatest { query -> + try { + deriveUiEvents(query) + } catch (exception: Exception) { + Log.e("GlobalSearchViewModel", "Error deriving UI events", exception) + emptyList() + } + } + .onEach { events -> + events.forEach(_uiEvents::tryEmit) + } + .launchIn(viewModelScope) + } + val result: SharedFlow = combine( _queryText, observeChangesAffectingSearch().onStart { emit(Unit) } @@ -62,7 +101,27 @@ class GlobalSearchViewModel @Inject constructor( ) } } else { - val results = searchRepository.query(query).toGlobalSearchResult() + var results = searchRepository.query(query).toGlobalSearchResult() + + // Special cases + // community URL detected + val communityUrl = linkChecker.check(query) as? LinkType.CommunityLink + if(communityUrl != null){ + // if the community is joined, add it to the result, + // otherwise a dialog is handled by the query event flow + if(communityUrl.joined){ + // community is already joined: add it to the result list + val openGroup = OpenGroupUrlParser.parseUrl(communityUrl.url) + results = results.copy( + threads = results.threads + recipientRepository.getRecipientSync( + Address.Community( + serverUrl = openGroup.server, + room = openGroup.room + ) + ) + ) + } + } // show "Note to Self" is the user searches for parts of"Note to Self" if(noteToSelfString.contains(query.lowercase())){ @@ -78,8 +137,37 @@ class GlobalSearchViewModel @Inject constructor( } .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 0) + private suspend fun deriveUiEvents(query: String): List = withContext(Dispatchers.Default) { + if (query.isBlank()) { + return@withContext emptyList() + } + + buildList { + val communityUrl = linkChecker.check(query) as? LinkType.CommunityLink + if (communityUrl != null && !communityUrl.joined) { + add( + UiEvent.ShowUrlDialog( + communityUrl.copy(displayType = LinkType.CommunityLink.DisplayType.SEARCH) + ) + ) + } + + val accountId = AccountId.fromStringOrNull(query) + if (accountId != null && + accountId.prefix == IdPrefix.STANDARD && + !searchRepository.queryContacts(query).any { it.address.toString() == query } + ) { + add(UiEvent.ShowNewConversationDialog(Address.Standard(accountId))) + } + } + } + fun setQuery(charSequence: CharSequence) { _queryText.value = charSequence.toString() } -} + sealed interface UiEvent { + data class ShowUrlDialog(val linkType: LinkType) : UiEvent + data class ShowNewConversationDialog(val address: Address.Conversable) : UiEvent + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt index 58a8a99a6d..683476877b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/StartConversationSheet.kt @@ -42,9 +42,10 @@ import org.thoughtcrime.securesms.home.startconversation.newmessage.NewMessageVi import org.thoughtcrime.securesms.home.startconversation.newmessage.State import org.thoughtcrime.securesms.ui.NavigationAction import org.thoughtcrime.securesms.ui.ObserveAsEvents -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog +import org.thoughtcrime.securesms.ui.dialog.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.ui.components.BaseBottomSheet +import org.thoughtcrime.securesms.ui.dialog.LinkAlertDialog import org.thoughtcrime.securesms.ui.handleIntent import org.thoughtcrime.securesms.ui.horizontalSlideComposable import org.thoughtcrime.securesms.ui.theme.PreviewTheme @@ -166,7 +167,10 @@ fun StartConversationNavHost( // New Message horizontalSlideComposable { - val viewModel = hiltViewModel() + val viewModel = hiltViewModel{ factory -> + factory.create(allowCommunityUrl = true) + } + val uiState by viewModel.state.collectAsState(State()) LaunchedEffect(Unit) { @@ -192,10 +196,16 @@ fun StartConversationNavHost( onClose = onClose, onHelp = { viewModel.onCommand(NewMessageViewModel.Commands.ShowUrlDialog) } ) - if (uiState.showUrlDialog != null) { - OpenURLAlertDialog( - url = uiState.showUrlDialog!!, - onDismissRequest = { viewModel.onCommand(NewMessageViewModel.Commands.DismissUrlDialog) } + if (uiState.urlDialog != null) { + LinkAlertDialog( + data = uiState.urlDialog!!, + onDismissRequest = { + // hide dialog + viewModel.onCommand(NewMessageViewModel.Commands.DismissUrlDialog) + }, + openOrJoinCommunity = { + viewModel.onCommand(NewMessageViewModel.Commands.OpenOrJoinCommunity(it)) + } ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityScreen.kt index 6387bb7817..3536ceb77b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityScreen.kt @@ -52,6 +52,7 @@ import org.thoughtcrime.securesms.ui.components.QRScannerScreen import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator +import org.thoughtcrime.securesms.ui.dialog.LinkAlertDialog import org.thoughtcrime.securesms.ui.qaTag import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions @@ -96,6 +97,19 @@ internal fun JoinCommunityScreen( } } } + + // Dialogs + if (state.urlDialog != null) { + LinkAlertDialog( + data = state.urlDialog, + onDismissRequest = { + sendCommand(JoinCommunityViewModel.Commands.OnDismissJoinedDialog) + }, + openOrJoinCommunity = { + sendCommand(JoinCommunityViewModel.Commands.JoinCommunity(it)) + }, + ) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt index a4f70c8c98..dab8348113 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt @@ -22,8 +22,13 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.utilities.Address import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY +import org.session.libsession.utilities.withUserConfigs import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.home.startconversation.group.CreateGroupEvent +import org.thoughtcrime.securesms.links.LinkChecker +import org.thoughtcrime.securesms.links.LinkType import org.thoughtcrime.securesms.ui.getSubbedString import org.thoughtcrime.securesms.util.State import javax.inject.Inject @@ -33,7 +38,9 @@ class JoinCommunityViewModel @Inject constructor( @param:ApplicationContext private val appContext: Context, private val openGroupManager: OpenGroupManager, private val officialCommunityRepository: OfficialCommunityRepository, -): ViewModel() { + private val linkChecker: LinkChecker, + + ): ViewModel() { private val _state = MutableStateFlow(JoinCommunityState(defaultCommunities = State.Loading)) val state: StateFlow = _state @@ -96,12 +103,19 @@ class JoinCommunityViewModel @Inject constructor( } } + // Check if we've already joined this community + val communityLink = linkChecker.check(url) as? LinkType.CommunityLink + + if (communityLink?.joined == true) { + _state.update { it.copy(urlDialog = communityLink.copy(allowCopyUrl = false)) } + return@launch + } + try { - val sanitizedServer = openGroup.server.removeSuffix("/") openGroupManager.add( - sanitizedServer, - openGroup.room, - openGroup.serverPublicKey, + server = openGroup.server, + room = openGroup.room, + publicKey = openGroup.serverPublicKey, ) _uiEvents.emit(UiEvent.NavigateToConversation( @@ -142,6 +156,10 @@ class JoinCommunityViewModel @Inject constructor( ) } } + + is Commands.OnDismissJoinedDialog -> { + _state.update { it.copy(urlDialog = null) } + } } } @@ -149,13 +167,15 @@ class JoinCommunityViewModel @Inject constructor( val loading: Boolean = false, val isJoinButtonEnabled: Boolean = false, val communityUrl: String = "", - val defaultCommunities: State> + val defaultCommunities: State>, + val urlDialog: LinkType? = null ) sealed interface Commands { data class OnQRScanned(val qr: String) : Commands data class JoinCommunity(val url: String): Commands data class OnUrlChanged(val url: String): Commands + data object OnDismissJoinedDialog: Commands } sealed interface UiEvent { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupScreen.kt index 80f53f2607..def02d25de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -39,6 +40,7 @@ import org.thoughtcrime.securesms.ui.components.AccentOutlineButton import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField import org.thoughtcrime.securesms.ui.components.SmallCircularProgressIndicator +import org.thoughtcrime.securesms.ui.dialog.LinkAlertDialog import org.thoughtcrime.securesms.ui.qaTag import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions @@ -91,6 +93,16 @@ fun CreateGroupScreen( onCreateClicked = viewModel::onCreateClicked, onBack = onBack, ) + + // Dialogs + val urlDialog by viewModel.urlDialog.collectAsState() + if (urlDialog != null) { + LinkAlertDialog( + data = urlDialog!!, + onDismissRequest = viewModel::onDismissUrlDialog, + openOrJoinCommunity = viewModel::openOrJoinCommunity, + ) + } } @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupViewModel.kt index 69f2698727..a580461a83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/group/CreateGroupViewModel.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.home.startconversation.group import android.content.Context +import android.widget.Toast import androidx.lifecycle.viewModelScope import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -18,12 +19,18 @@ import network.loki.messenger.R import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.utilities.Address +import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.textSizeInBytes import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.groups.SelectContactsViewModel +import org.thoughtcrime.securesms.links.LinkChecker +import org.thoughtcrime.securesms.links.LinkType +import org.thoughtcrime.securesms.links.LinkType.CommunityLink.DisplayType.GROUP import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.util.AvatarUtils @@ -34,6 +41,8 @@ class CreateGroupViewModel @AssistedInject constructor( @param:ApplicationContext private val appContext: Context, private val storage: StorageProtocol, private val groupManagerV2: GroupManagerV2, + private val linkChecker: LinkChecker, + private val openGroupManager: OpenGroupManager, avatarUtils: AvatarUtils, proStatusManager: ProStatusManager, groupDatabase: GroupDatabase, @@ -62,6 +71,10 @@ class CreateGroupViewModel @AssistedInject constructor( private val mutableIsLoading = MutableStateFlow(false) val isLoading: StateFlow get() = mutableIsLoading + // Community dialog + private val mutableUrlDialog = MutableStateFlow(null) + val urlDialog: StateFlow get() = mutableUrlDialog + // Events private val mutableEvents = MutableSharedFlow() val events: SharedFlow get() = mutableEvents @@ -99,6 +112,14 @@ class CreateGroupViewModel @AssistedInject constructor( return@launch } + // Special case: Check if someone entered a Community link + // If so show a dialog + val communityLink = linkChecker.check(groupName) as? LinkType.CommunityLink + if (communityLink != null) { + mutableUrlDialog.value = communityLink.copy(displayType = GROUP) + return@launch + } + // validate name length (needs to be less than 100 bytes) if(groupName.textSizeInBytes() > ConfigFactory.MAX_NAME_BYTES){ mutableGroupNameError.value = appContext.getString(R.string.groupNameEnterShorter) @@ -144,6 +165,41 @@ class CreateGroupViewModel @AssistedInject constructor( mutableGroupNameError.value = "" } + fun onDismissUrlDialog(){ + mutableUrlDialog.value = null + } + + fun openOrJoinCommunity(url: String) { + val openGroup = try { + OpenGroupUrlParser.parseUrl(url) + } catch (_: OpenGroupUrlParser.Error) { + Toast.makeText(appContext, R.string.communityEnterUrlErrorInvalidDescription, Toast.LENGTH_SHORT) + .show() + return + } + + onDismissUrlDialog() + + viewModelScope.launch { + try { + openGroupManager.add( + server = openGroup.server, + room = openGroup.room, + publicKey = openGroup.serverPublicKey, + ) + + // after joining or if already joined, open the conversation + val communityAddress = Address.Community(openGroup.server, openGroup.room) + mutableEvents.emit(CreateGroupEvent.NavigateToConversation(communityAddress)) + + } catch (e: Exception) { + Log.e("CreateGroupViewModel", "Error joining community", e) + Toast.makeText(appContext, R.string.communityErrorDescription, Toast.LENGTH_SHORT) + .show() + } + } + } + @AssistedFactory interface Factory { fun create(createFromLegacyGroupId: String?): CreateGroupViewModel diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessageViewModel.kt index 2835b52592..4cdc8139cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/newmessage/NewMessageViewModel.kt @@ -1,10 +1,15 @@ package org.thoughtcrime.securesms.home.startconversation.newmessage import android.app.Application +import android.widget.Toast import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.squareup.phrase.Phrase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow @@ -15,23 +20,37 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import network.loki.messenger.R import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.toAddress +import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY +import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.PublicKeyValidation import org.thoughtcrime.securesms.api.error.UnhandledStatusCodeException +import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.links.LinkChecker +import org.thoughtcrime.securesms.links.LinkType +import org.thoughtcrime.securesms.links.LinkType.CommunityLink.DisplayType.* import org.thoughtcrime.securesms.ui.GetString import java.net.IDN -import javax.inject.Inject -@HiltViewModel -class NewMessageViewModel @Inject constructor( +@HiltViewModel(assistedFactory = NewMessageViewModel.Factory::class) +class NewMessageViewModel @AssistedInject constructor( + @Assisted private val allowCommunityUrl: Boolean, private val application: Application, private val onsResolver: OnsResolver, + private val openGroupManager: OpenGroupManager, + private val linkChecker: LinkChecker, ) : ViewModel(), Callbacks { + @AssistedFactory + interface Factory { + fun create(allowCommunityUrl: Boolean): NewMessageViewModel + } + private val HELP_URL : String = "https://getsession.org/account-ids" private val _state = MutableStateFlow(State()) @@ -64,36 +83,43 @@ class NewMessageViewModel @Inject constructor( } override fun onContinue() { - val trimmed = state.value.newMessageIdOrOns.trim() - // Check if all characters are ASCII (code <= 127). - val idOrONS = if (trimmed.all { it.code <= 127 }) { - // Already ASCII (or punycode‐ready); no conversion needed. - trimmed - } else { - try { - // For non-ASCII input (e.g. with emojis), attempt to puny-encode - IDN.toASCII(trimmed, IDN.ALLOW_UNASSIGNED) - } catch (e: IllegalArgumentException) { - // if the above failed, resort to the original trimmed string - Log.w("", "IDN.toASCII failed. Returning: $trimmed") + viewModelScope.launch { + val trimmed = state.value.newMessageIdOrOns.trim() + // Check if all characters are ASCII (code <= 127). + val idOrONS = if (trimmed.all { it.code <= 127 }) { + // Already ASCII (or punycode-ready); no conversion needed. trimmed + } else { + try { + // For non-ASCII input (e.g. with emojis), attempt to puny-encode + IDN.toASCII(trimmed, IDN.ALLOW_UNASSIGNED) + } catch (e: IllegalArgumentException) { + // if the above failed, resort to the original trimmed string + Log.w("", "IDN.toASCII failed. Returning: $trimmed") + trimmed + } } - } - if (PublicKeyValidation.hasValidLength(idOrONS)) { - if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) { - onUnvalidatedPublicKey(idOrONS) - } else { - _state.update { - it.copy( - isTextErrorColor = true, - error = GetString(R.string.accountIdErrorInvalid), - loading = false - ) + // check if we have a community URL + val communityLink = linkChecker.check(idOrONS) as? LinkType.CommunityLink + + if (communityLink != null && allowCommunityUrl) { + onCommunityUrlDetected(communityLink.copy(displayType = ENTERED)) + } else if (AccountId.hasValidLength(idOrONS)) { + if (isValidStandardAddress(idOrONS)) { + onPublicKey(idOrONS) + } else { + _state.update { + it.copy( + isTextErrorColor = true, + error = GetString(R.string.accountIdErrorInvalid), + loading = false + ) + } } + } else { + resolveONS(idOrONS) } - } else { - resolveONS(idOrONS) } } @@ -101,20 +127,27 @@ class NewMessageViewModel @Inject constructor( val currentTime = System.currentTimeMillis() if (currentTime - lasQrScan > qrDebounceTime) { lasQrScan = currentTime - if (PublicKeyValidation.isValid( - value, - isPrefixRequired = false - ) && PublicKeyValidation.hasValidPrefix(value) - ) { - onChange(value) - _state.update { it.copy(validIdFromQr = value) } - } else { - _qrErrors.tryEmit(application.getString(R.string.qrNotAccountId)) - _state.update { it.copy(validIdFromQr = "") } + + viewModelScope.launch { + // check if we have a community URL + val communityLink = linkChecker.check(value) as? LinkType.CommunityLink + + if (communityLink != null) { + onCommunityUrlDetected(communityLink.copy(displayType = SCANNED)) + } else if (isValidStandardAddress(value)) { + onChange(value) + _state.update { it.copy(validIdFromQr = value) } + } else { + _qrErrors.tryEmit(application.getString(R.string.qrNotAccountId)) + _state.update { it.copy(validIdFromQr = "") } + } } } } + private fun isValidStandardAddress(address: String): Boolean = + AccountId.fromStringOrNull(address)?.prefix == IdPrefix.STANDARD + override fun onClearQrCode() { _state.update {it.copy(validIdFromQr = "") } } @@ -150,29 +183,52 @@ class NewMessageViewModel @Inject constructor( } } - private fun onPublicKey(publicKey: String) { + private fun onCommunityUrlDetected(communityLink: LinkType.CommunityLink){ _state.update { it.copy(loading = false) } - val address = publicKey.toAddress() - if (address is Address.Standard) { - viewModelScope.launch { _success.emit(Success(address)) } + _state.update { + it.copy(urlDialog = communityLink) } } - private fun onUnvalidatedPublicKey(publicKey: String) { - if (PublicKeyValidation.hasValidPrefix(publicKey)) { - onPublicKey(publicKey) - } else { - _state.update { - it.copy( - isTextErrorColor = true, - error = GetString(R.string.accountIdErrorInvalid), - loading = false + private fun openOrJoinCommunity(url: String){ + val openGroup = try { + OpenGroupUrlParser.parseUrl(url) + } catch (_: OpenGroupUrlParser.Error) { + Toast.makeText(application, R.string.communityEnterUrlErrorInvalidDescription, Toast.LENGTH_SHORT) + .show() + return + } + + viewModelScope.launch { + try { + openGroupManager.add( + server = openGroup.server, + room = openGroup.room, + publicKey = openGroup.serverPublicKey, ) + + // after joining or if already joined, open the conversation + _success.emit(Success(Address.Community(openGroup.server, openGroup.room))) + } catch (e: Exception) { + Log.e("", "Error joining community", e) + withContext(Dispatchers.Main) { + Toast.makeText(application, R.string.communityErrorDescription, Toast.LENGTH_SHORT) + .show() + } } } } + private fun onPublicKey(publicKey: String) { + _state.update { it.copy(loading = false) } + + val address = publicKey.toAddress() + if (address is Address.Standard) { + viewModelScope.launch { _success.emit(Success(address)) } + } + } + private fun Exception.toMessage() = when (this) { is UnhandledStatusCodeException -> application.getString(R.string.errorUnregisteredOns) else -> Phrase.from(application, R.string.errorNoLookupOns) @@ -183,22 +239,27 @@ class NewMessageViewModel @Inject constructor( fun onCommand(commands: Commands) { when (commands) { is Commands.ShowUrlDialog -> { - _state.update { it.copy(showUrlDialog = HELP_URL) } + _state.update { it.copy(urlDialog = LinkType.GenericLink(HELP_URL)) } } is Commands.DismissUrlDialog -> { _state.update { it.copy( - showUrlDialog = null + urlDialog = null ) } } + + is Commands.OpenOrJoinCommunity -> { + openOrJoinCommunity(commands.url) + } } } sealed interface Commands { data object ShowUrlDialog : Commands data object DismissUrlDialog : Commands + data class OpenOrJoinCommunity(val url: String) : Commands } } @@ -207,11 +268,11 @@ data class State( val isTextErrorColor: Boolean = false, val error: GetString? = null, val loading: Boolean = false, - val showUrlDialog: String? = null, + val urlDialog: LinkType? = null, val validIdFromQr: String = "", ) { val isNextButtonEnabled: Boolean get() = newMessageIdOrOns.isNotBlank() } -data class Success(val address: Address.Standard) \ No newline at end of file +data class Success(val address: Address.Conversable) diff --git a/app/src/main/java/org/thoughtcrime/securesms/links/CommunityLinkRule.kt b/app/src/main/java/org/thoughtcrime/securesms/links/CommunityLinkRule.kt new file mode 100644 index 0000000000..c062b8d813 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/links/CommunityLinkRule.kt @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.links + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.OpenGroupUrlParser +import org.session.libsession.utilities.withUserConfigs +import org.thoughtcrime.securesms.database.CommunityDatabase +import javax.inject.Inject + +class CommunityLinkRule @Inject constructor( + private val configFactory: ConfigFactoryProtocol, + private val communityDatabase: CommunityDatabase, +) : LinkRule { + + override suspend fun classify(url: String): LinkType? = withContext(Dispatchers.IO) { + val openGroup = try { + OpenGroupUrlParser.parseUrl(url) + } catch (_: OpenGroupUrlParser.Error) { + return@withContext null + } + + val joinedCommunity = configFactory.withUserConfigs { + it.userGroups.getCommunityInfo(openGroup.server, openGroup.room) + } + val roomInfo = communityDatabase.getRoomInfo(Address.Community(openGroup.server, openGroup.room)) + val name = roomInfo?.details?.name + ?.takeIf { it.isNotBlank() } + ?: openGroup.room + + return@withContext LinkType.CommunityLink( + url = url, + name = name, + joined = joinedCommunity != null, + displayType = LinkType.CommunityLink.DisplayType.CONVERSATION + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/links/LinkChecker.kt b/app/src/main/java/org/thoughtcrime/securesms/links/LinkChecker.kt new file mode 100644 index 0000000000..9ed05da1cc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/links/LinkChecker.kt @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.links + +import javax.inject.Inject +import javax.inject.Singleton + +sealed class LinkType(open val url: String) { + data class GenericLink( + override val url: String, + ) : LinkType(url) + + data class CommunityLink( + override val url: String, + val joined: Boolean, + val displayType: DisplayType, + val name: String = "", + val allowCopyUrl: Boolean = true + ) : LinkType(url){ + enum class DisplayType{ + CONVERSATION, ENTERED, SCANNED, GROUP, SEARCH + } + } +} + +internal fun interface LinkRule { + suspend fun classify(url: String): LinkType? +} + +@Singleton +class LinkChecker internal constructor( + private val rules: List, +) { + @Inject + constructor( + communityLinkRule: CommunityLinkRule, + ) : this(listOf(communityLinkRule)) + + suspend fun check( + url: String + ): LinkType { + val normalizedUrl = url.trim() + for (rule in rules) { + rule.classify(normalizedUrl)?.let { return it } + } + + return LinkType.GenericLink(normalizedUrl) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt index 7e4ad51095..4f7b5ef847 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/media/MediaOverviewScreen.kt @@ -41,9 +41,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import network.loki.messenger.R -import org.thoughtcrime.securesms.ui.AlertDialog -import org.thoughtcrime.securesms.ui.BasicSessionAlertDialog -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.BasicSessionAlertDialog +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.components.CircularProgressIndicator import org.thoughtcrime.securesms.ui.components.SessionTabRow diff --git a/app/src/main/java/org/thoughtcrime/securesms/migration/DatabaseMigrationScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/migration/DatabaseMigrationScreen.kt index 366e61b03f..ea272767c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migration/DatabaseMigrationScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/migration/DatabaseMigrationScreen.kt @@ -37,8 +37,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY -import org.thoughtcrime.securesms.ui.AlertDialog -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.components.AccentFillButton import org.thoughtcrime.securesms.ui.components.ExportLogsDialog diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/OnboardingBackPressAlertDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/OnboardingBackPressAlertDialog.kt index b985490675..acaa24af9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/OnboardingBackPressAlertDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/OnboardingBackPressAlertDialog.kt @@ -7,8 +7,8 @@ import androidx.compose.ui.res.stringResource import com.squareup.phrase.Phrase import network.loki.messenger.R import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY -import org.thoughtcrime.securesms.ui.AlertDialog -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.theme.LocalColors diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt index 8817cf7741..5027262a1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/landing/Landing.kt @@ -42,7 +42,7 @@ import org.thoughtcrime.securesms.conversation.v3.compose.message.MessageViewDat import org.thoughtcrime.securesms.conversation.v3.compose.message.PreviewMessageData.textGroup import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.ui.components.BorderlessHtmlButton -import org.thoughtcrime.securesms.ui.TCPolicyDialog +import org.thoughtcrime.securesms.ui.dialog.TCPolicyDialog import org.thoughtcrime.securesms.ui.components.AccentFillButton import org.thoughtcrime.securesms.ui.components.AccentOutlineButton import org.thoughtcrime.securesms.ui.qaTag diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsScreen.kt index 2f9a94bcd5..14548af812 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/BlockedContactsScreen.kt @@ -24,9 +24,9 @@ import network.loki.messenger.R import org.session.libsession.utilities.Address import org.thoughtcrime.securesms.groups.ContactItem import org.thoughtcrime.securesms.groups.compose.multiSelectMemberList -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.BottomFadingEdgeBox -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.SearchBar import org.thoughtcrime.securesms.ui.components.BackAppBar diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt index 547145b314..24d112b2f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -30,7 +30,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import network.loki.messenger.R import org.session.libsession.utilities.Address -import org.session.libsignal.utilities.PublicKeyValidation +import org.session.libsignal.utilities.AccountId +import org.session.libsignal.utilities.IdPrefix import org.thoughtcrime.securesms.ScreenLockActionBarActivity import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.permissions.Permissions @@ -80,12 +81,12 @@ class QRCodeActivity : ScreenLockActionBarActivity() { } private fun onScan(string: String) { - if (!PublicKeyValidation.isValid(string)) { + val accountId = AccountId.fromStringOrNull(string) + if (accountId?.prefix != IdPrefix.STANDARD) { errors.tryEmit(getString(R.string.qrNotAccountId)) } else if (!isFinishing) { - val address = Address.fromSerialized(string) as Address.Conversable startActivity( - ConversationActivityV2.createIntent(this, address = address) + ConversationActivityV2.createIntent(this, address = Address.Standard(accountId)) .setDataAndType(intent.data, intent.type) .addFlags(FLAG_ACTIVITY_SINGLE_TOP) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt index 7fa183210c..3929bfe489 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape @@ -106,16 +105,16 @@ import org.thoughtcrime.securesms.pro.previewAutoRenewingApple import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity import org.thoughtcrime.securesms.tokenpage.TokenPageActivity import org.thoughtcrime.securesms.ui.AccountIdHeader -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.AnimatedProfilePicProCTA import org.thoughtcrime.securesms.ui.CTAAnimatedImages import org.thoughtcrime.securesms.ui.Cell -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.ItemButton -import org.thoughtcrime.securesms.ui.LoadingDialog -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog +import org.thoughtcrime.securesms.ui.dialog.LoadingDialog +import org.thoughtcrime.securesms.ui.dialog.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.PathDot import org.thoughtcrime.securesms.ui.ProBadge import org.thoughtcrime.securesms.ui.ProBadgeText diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt index c5bddd98ae..1d1ca48c42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt @@ -64,7 +64,7 @@ import org.thoughtcrime.securesms.pro.ProStatus import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.pro.getDefaultSubscriptionStateData import org.thoughtcrime.securesms.reviews.InAppReviewManager -import org.thoughtcrime.securesms.ui.SimpleDialogData +import org.thoughtcrime.securesms.ui.dialog.SimpleDialogData import org.thoughtcrime.securesms.util.AnimatedImageUtils import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUtils diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppDisguiseSettings.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppDisguiseSettings.kt index ec25fccaf6..3ea46f558e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppDisguiseSettings.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppDisguiseSettings.kt @@ -45,9 +45,9 @@ import androidx.core.graphics.drawable.toBitmap import com.squareup.phrase.Phrase import network.loki.messenger.R import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.Cell -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.components.BackAppBar import org.thoughtcrime.securesms.ui.qaTag diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/compose/NotificationsPreferenceScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/compose/NotificationsPreferenceScreen.kt index de64716e49..45d9652258 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/compose/NotificationsPreferenceScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/compose/NotificationsPreferenceScreen.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.preferences.compose -import android.R.attr.onClick import android.app.Activity import android.media.RingtoneManager import android.net.Uri @@ -28,9 +27,9 @@ import org.thoughtcrime.securesms.preferences.compose.NotificationsPreferenceVie import org.thoughtcrime.securesms.preferences.compose.NotificationsPreferenceViewModel.NotificationPreferenceEvent.* import org.thoughtcrime.securesms.preferences.compose.NotificationsPreferenceViewModel.NotificationPrivacyOption import org.thoughtcrime.securesms.ui.ActionRowItem -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.CategoryCell -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.IconTextActionRowItem diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/compose/PrivacySettingsPreferenceScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/compose/PrivacySettingsPreferenceScreen.kt index 64a9d3da6b..1495eb1e09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/compose/PrivacySettingsPreferenceScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/compose/PrivacySettingsPreferenceScreen.kt @@ -28,9 +28,9 @@ import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.preferences.compose.PrivacySettingsPreferenceViewModel.Commands.* import org.thoughtcrime.securesms.preferences.compose.PrivacySettingsPreferenceViewModel.PrivacySettingsPreferenceEvent.* import org.thoughtcrime.securesms.service.KeyCachingService -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.CategoryCell -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.SwitchActionRowItem import org.thoughtcrime.securesms.ui.components.TypingIndicator diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt index aa0ba02bc3..7a2497e9f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/BaseProSettingsScreens.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope @@ -48,7 +47,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import network.loki.messenger.R import org.thoughtcrime.securesms.ui.Cell -import org.thoughtcrime.securesms.ui.DialogBg +import org.thoughtcrime.securesms.ui.dialog.DialogBg import org.thoughtcrime.securesms.ui.SessionProSettingsHeader import org.thoughtcrime.securesms.ui.components.AccentFillButtonRect import org.thoughtcrime.securesms.ui.components.BackAppBar diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsDialogs.kt index 9e0279a2ac..dab798baf0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsDialogs.kt @@ -4,11 +4,11 @@ import androidx.compose.runtime.Composable import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.HideSimpleDialog import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.HideTCPolicyDialog import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog -import org.thoughtcrime.securesms.ui.AlertDialog -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog -import org.thoughtcrime.securesms.ui.TCPolicyDialog +import org.thoughtcrime.securesms.ui.dialog.OpenURLAlertDialog +import org.thoughtcrime.securesms.ui.dialog.TCPolicyDialog import org.thoughtcrime.securesms.ui.components.annotatedStringResource import org.thoughtcrime.securesms.ui.theme.LocalColors diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt index ba32928537..0d10378d51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/prosettings/ProSettingsViewModel.kt @@ -60,7 +60,7 @@ import org.thoughtcrime.securesms.pro.isFromAnotherPlatform import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration import org.thoughtcrime.securesms.pro.subscription.SubscriptionCoordinator import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager -import org.thoughtcrime.securesms.ui.SimpleDialogData +import org.thoughtcrime.securesms.ui.dialog.SimpleDialogData import org.thoughtcrime.securesms.ui.UINavigator import org.thoughtcrime.securesms.util.CurrencyFormatter import org.thoughtcrime.securesms.util.DateUtils diff --git a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt index 8fcdd4ca1f..f63d3d00f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt @@ -27,9 +27,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import network.loki.messenger.R -import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.Cell -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.SessionShieldIcon import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo diff --git a/app/src/main/java/org/thoughtcrime/securesms/reviews/ui/InAppReview.kt b/app/src/main/java/org/thoughtcrime/securesms/reviews/ui/InAppReview.kt index ca784a9770..a5c2d4a20e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reviews/ui/InAppReview.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/reviews/ui/InAppReview.kt @@ -33,11 +33,11 @@ import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY import org.session.libsession.utilities.StringSubstitutionConstants.STORE_VARIANT_KEY import org.thoughtcrime.securesms.reviews.StoreReviewManager -import org.thoughtcrime.securesms.ui.AlertDialog -import org.thoughtcrime.securesms.ui.AlertDialogContent -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.AlertDialogContent +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog +import org.thoughtcrime.securesms.ui.dialog.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt index 75b77ab335..5df3a4abb6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.kt @@ -97,10 +97,11 @@ class SearchRepository @Inject constructor( .filter { searchName == null || when (it.data) { - // Search contacts by both nickname and name + // Search contacts by both nickname and name and ID is RecipientData.Contact -> { it.data.nickname?.contains(searchName, ignoreCase = true) == true || - it.data.name.contains(searchName, ignoreCase = true) + it.data.name.contains(searchName, ignoreCase = true) || + it.address.toString() == searchName } is RecipientData.BlindedContact -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPage.kt b/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPage.kt index ab03ce4a79..694e3cfcd7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/tokenpage/TokenPage.kt @@ -71,7 +71,7 @@ import org.session.libsession.utilities.StringSubstitutionConstants.STAKING_REWA import org.session.libsession.utilities.StringSubstitutionConstants.TOKEN_NAME_LONG_KEY import org.session.libsession.utilities.StringSubstitutionConstants.TOKEN_NAME_SHORT_KEY import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.ui.OpenURLAlertDialog +import org.thoughtcrime.securesms.ui.dialog.OpenURLAlertDialog import org.thoughtcrime.securesms.ui.SpeechBubbleTooltip import org.thoughtcrime.securesms.ui.adaptive.getAdaptiveInfo import org.thoughtcrime.securesms.ui.components.AccentOutlineButtonRect diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt index 3988fe5bca..06182d3c0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt @@ -97,6 +97,8 @@ import org.thoughtcrime.securesms.ui.components.Avatar import org.thoughtcrime.securesms.ui.components.BaseBottomSheet import org.thoughtcrime.securesms.ui.components.QrImage import org.thoughtcrime.securesms.ui.components.TertiaryFillButtonRect +import org.thoughtcrime.securesms.ui.dialog.BasicSessionAlertDialog +import org.thoughtcrime.securesms.ui.dialog.DialogBg import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -271,8 +273,10 @@ fun SessionProCTA( DialogBg { BoxWithConstraints(Modifier.fillMaxWidth()) { val heroMaxHeight = maxHeight * 0.4f - Column(modifier = Modifier.fillMaxWidth() - .verticalScroll(rememberScrollState())) { + Column( + modifier = Modifier.fillMaxWidth() + .verticalScroll(rememberScrollState()) + ) { // hero image BottomFadingEdgeBox( modifier = Modifier.heightIn(max = heroMaxHeight), @@ -335,10 +339,10 @@ fun SessionProCTA( modifier = Modifier .qaTag(R.string.qa_cta_button_positive) .then( - if (negativeButtonText != null) - Modifier.weight(1f) - else Modifier - ).shimmerOverlay(), + if (negativeButtonText != null) + Modifier.weight(1f) + else Modifier + ).shimmerOverlay(), text = it, onClick = onUpgrade ?: defaultUpgrade ) @@ -349,10 +353,10 @@ fun SessionProCTA( modifier = Modifier .qaTag(R.string.qa_cta_button_negative) .then( - if (positiveButtonText != null) - Modifier.weight(1f) - else Modifier - ), + if (positiveButtonText != null) + Modifier.weight(1f) + else Modifier + ), text = it, onClick = onCancel ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/UserProfileModal.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/UserProfileModal.kt index 7e56efeb67..a592524637 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/UserProfileModal.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/UserProfileModal.kt @@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.pro.ProStatus import org.thoughtcrime.securesms.ui.components.SlimAccentOutlineButton import org.thoughtcrime.securesms.ui.components.SlimOutlineCopyButton import org.thoughtcrime.securesms.ui.components.annotatedStringResource +import org.thoughtcrime.securesms.ui.dialog.AlertDialog import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -90,12 +91,14 @@ fun UserProfileModal( ProBadgeText( text = data.name, showBadge = data.showProBadge, - onBadgeClick = if(!data.currentUserPro){{ - sendCommand(UserProfileModalCommands.ShowProCTA) - }} else null + onBadgeClick = if (!data.currentUserPro) { + { + sendCommand(UserProfileModalCommands.ShowProCTA) + } + } else null ) - if(!data.subtitle.isNullOrEmpty()){ + if (!data.subtitle.isNullOrEmpty()) { Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) Text( text = data.subtitle, @@ -107,7 +110,7 @@ fun UserProfileModal( // account ID AccountIdHeader( - text = if(data.isBlinded) stringResource(R.string.blindedId) else stringResource(R.string.accountId), + text = if (data.isBlinded) stringResource(R.string.blindedId) else stringResource(R.string.accountId), textStyle = LocalType.current.small, textPaddingValues = PaddingValues( horizontal = LocalDimensions.current.smallSpacing, @@ -118,7 +121,7 @@ fun UserProfileModal( Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) Row { - if(!data.tooltipText.isNullOrEmpty()){ + if (!data.tooltipText.isNullOrEmpty()) { Spacer(modifier = Modifier.width(LocalDimensions.current.spacing)) } @@ -131,7 +134,7 @@ fun UserProfileModal( color = LocalColors.current.text ) - if(!data.tooltipText.isNullOrEmpty()){ + if (!data.tooltipText.isNullOrEmpty()) { val tooltipState = rememberTooltipState(isPersistent = true) val scope = rememberCoroutineScope() @@ -161,13 +164,13 @@ fun UserProfileModal( Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) // show a message if the user can't be messaged - if(data.isBlinded && !data.enableMessage){ + if (data.isBlinded && !data.enableMessage) { Text( modifier = Modifier.padding(horizontal = LocalDimensions.current.xsSpacing), text = annotatedStringResource( Phrase.from(LocalContext.current, R.string.messageRequestsTurnedOff) - .put(NAME_KEY, data.name) - .format() + .put(NAME_KEY, data.name) + .format() ), textAlign = TextAlign.Center, style = LocalType.current.small.copy(color = LocalColors.current.textSecondary) @@ -179,9 +182,9 @@ fun UserProfileModal( // buttons Row( verticalAlignment = Alignment.CenterVertically, - ){ + ) { var buttonModifier: Modifier = Modifier - if(data.isBlinded){ // this means there is no copy button so the message button should be full width + if (data.isBlinded) { // this means there is no copy button so the message button should be full width buttonModifier = buttonModifier.widthIn(LocalDimensions.current.minButtonWidth) } else { // the copy button will be there so allow for a max stretch with weight = 1f buttonModifier = buttonModifier.weight(1f) @@ -207,7 +210,7 @@ fun UserProfileModal( } ) - if(!data.isBlinded){ + if (!data.isBlinded) { Spacer(modifier = Modifier.width(LocalDimensions.current.xsSpacing)) SlimOutlineCopyButton( Modifier.weight(1f), diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/ExportLogsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/ExportLogsDialog.kt index f316caa18f..68ac698939 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/ExportLogsDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/ExportLogsDialog.kt @@ -11,13 +11,11 @@ import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.retain.retain import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.lifecycle.lifecycleScope import com.squareup.phrase.Phrase import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException @@ -32,10 +30,10 @@ import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsignal.utilities.ExternalStorageUtil import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.logging.PersistentLogger -import org.thoughtcrime.securesms.ui.AlertDialog -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString -import org.thoughtcrime.securesms.ui.LoadingDialog +import org.thoughtcrime.securesms.ui.dialog.LoadingDialog import org.thoughtcrime.securesms.util.FileProviderUtil import java.io.File import java.io.IOException diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt index 19a7d991e5..7ffc030521 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt @@ -8,8 +8,6 @@ import android.provider.Settings import androidx.camera.core.CameraSelector import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy -import androidx.camera.core.Preview -import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.CameraController import androidx.camera.view.LifecycleCameraController import androidx.camera.view.PreviewView @@ -70,8 +68,8 @@ import network.loki.messenger.R import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.permissions.Permissions -import org.thoughtcrime.securesms.ui.AlertDialog -import org.thoughtcrime.securesms.ui.DialogButtonData +import org.thoughtcrime.securesms.ui.dialog.AlertDialog +import org.thoughtcrime.securesms.ui.dialog.DialogButtonData import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.findActivity import org.thoughtcrime.securesms.ui.getSubbedString diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/AlertDialog.kt similarity index 90% rename from app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt rename to app/src/main/java/org/thoughtcrime/securesms/ui/dialog/AlertDialog.kt index d949dc72ec..a568f501ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/AlertDialog.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.ui +package org.thoughtcrime.securesms.ui.dialog import android.widget.Toast import androidx.compose.foundation.background @@ -44,10 +44,18 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import com.squareup.phrase.Phrase import network.loki.messenger.R +import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY +import org.thoughtcrime.securesms.links.LinkType import org.thoughtcrime.securesms.copyURLToClipboard +import org.thoughtcrime.securesms.ui.Cell +import org.thoughtcrime.securesms.ui.Divider +import org.thoughtcrime.securesms.ui.GetString +import org.thoughtcrime.securesms.ui.IconActionRowItem import org.thoughtcrime.securesms.ui.components.CircularProgressIndicator import org.thoughtcrime.securesms.ui.components.annotatedStringResource +import org.thoughtcrime.securesms.ui.openUrl +import org.thoughtcrime.securesms.ui.qaTag import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -258,52 +266,8 @@ fun AlertDialogContent( } -@Composable -fun OpenURLAlertDialog( - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, - url: String, - onLinkOpened: (String) -> Unit = {}, - onLinkCopied: (String) -> Unit = {}, - content: @Composable () -> Unit = {} -) { - val context = LocalContext.current - val unformattedText = Phrase.from(context.getText(R.string.urlOpenDescription)) - .put(URL_KEY, url).format() - AlertDialog( - modifier = modifier, - title = AnnotatedString(stringResource(R.string.urlOpen)), - text = annotatedStringResource(text = unformattedText), - maxLines = 5, - showCloseButton = true, // display the 'x' button - buttons = listOf( - DialogButtonData( - text = GetString(R.string.open), - color = LocalColors.current.danger, - dismissOnClick = false, - onClick = { - if(context.openUrl(url)){ - onLinkOpened(url) - onDismissRequest() - } - } - ), - DialogButtonData( - text = GetString(android.R.string.copyUrl), - onClick = { - onLinkCopied(url) - context.copyURLToClipboard(url) - Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show() - } - ) - ), - onDismissRequest = onDismissRequest, - content = content - ) -} - @Composable fun DialogButton( text: String, @@ -459,17 +423,6 @@ fun PreviewXCloseNoButtonsDialog() { } } -@Preview -@Composable -fun PreviewOpenURLDialog() { - PreviewTheme { - OpenURLAlertDialog( - url = "https://getsession.org/", - onDismissRequest = {} - ) - } -} - @Preview @Composable fun PreviewLoadingDialog() { @@ -556,4 +509,4 @@ private fun PreviewCPolicyDialog( onDismissRequest = {} ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/LinkDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/LinkDialog.kt new file mode 100644 index 0000000000..1a8b3fc9a2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/dialog/LinkDialog.kt @@ -0,0 +1,366 @@ +package org.thoughtcrime.securesms.ui.dialog + +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import com.squareup.phrase.Phrase +import network.loki.messenger.R +import org.session.libsession.utilities.StringSubstitutionConstants.COMMUNITY_NAME_KEY +import org.session.libsession.utilities.StringSubstitutionConstants.URL_KEY +import org.thoughtcrime.securesms.copyURLToClipboard +import org.thoughtcrime.securesms.links.LinkType +import org.thoughtcrime.securesms.links.LinkType.CommunityLink.DisplayType.* +import org.thoughtcrime.securesms.ui.GetString +import org.thoughtcrime.securesms.ui.components.annotatedStringResource +import org.thoughtcrime.securesms.ui.openUrl +import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.PreviewTheme + + +@Composable +fun LinkAlertDialog( + data: LinkType, + onDismissRequest: () -> Unit, + openOrJoinCommunity: (String) -> Unit, + modifier: Modifier = Modifier, + onLinkOpened: (String) -> Unit = {}, + onLinkCopied: (String) -> Unit = {}, + content: @Composable () -> Unit = {} +){ + when(data){ + is LinkType.GenericLink -> + OpenURLAlertDialog( + onDismissRequest = onDismissRequest, + modifier = modifier, + url = data.url, + onLinkOpened = onLinkOpened, + onLinkCopied = onLinkCopied, + content = content + ) + + is LinkType.CommunityLink -> + CommunityLinkAlertDialog( + data = data, + onDismissRequest = onDismissRequest, + openOrJoinCommunity = openOrJoinCommunity, + modifier = modifier + ) + } +} + +@Composable +fun OpenURLAlertDialog( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + url: String, + onLinkOpened: (String) -> Unit = {}, + onLinkCopied: (String) -> Unit = {}, + content: @Composable () -> Unit = {} +) { + val context = LocalContext.current + val unformattedText = Phrase.from(context.getText(R.string.urlOpenDescription)) + .put(URL_KEY, url).format() + + + AlertDialog( + modifier = modifier, + title = AnnotatedString(stringResource(R.string.urlOpen)), + text = annotatedStringResource(text = unformattedText), + maxLines = 5, + showCloseButton = true, // display the 'x' button + buttons = listOf( + DialogButtonData( + text = GetString(R.string.open), + color = LocalColors.current.danger, + dismissOnClick = false, + onClick = { + if(context.openUrl(url)){ + onLinkOpened(url) + onDismissRequest() + } + } + ), + DialogButtonData( + text = GetString(android.R.string.copyUrl), + onClick = { + onLinkCopied(url) + context.copyURLToClipboard(url) + Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show() + } + ) + ), + onDismissRequest = onDismissRequest, + content = content + ) +} + +@Composable +fun CommunityLinkAlertDialog( + data: LinkType.CommunityLink, + onDismissRequest: () -> Unit, + openOrJoinCommunity: (String) -> Unit, + modifier: Modifier = Modifier +) { + //todo comlink I need to verify both the strings and buttons from design + val context = LocalContext.current + val title = if (data.joined) { + stringResource(R.string.openCommunity) + } else { + stringResource(R.string.communityJoin) + } + val text: CharSequence = when(data.displayType){ + CONVERSATION -> { + if (data.joined) { + Phrase.from(context, R.string.joinedCommunityOpen) + .put(COMMUNITY_NAME_KEY, data.name) + .format() + } else { + annotatedStringResource(R.string.joinThisCommunity) + } + } + + ENTERED -> { + if (data.joined) { + Phrase.from(context, R.string.communityUrlOpenEntered) + .put(COMMUNITY_NAME_KEY, data.name) + .format() + } else { + annotatedStringResource(R.string.communityUrlJoinEntered) + } + } + + SCANNED -> { + if (data.joined) { + Phrase.from(context, R.string.communityUrlOpenScanned) + .put(COMMUNITY_NAME_KEY, data.name) + .format() + } else { + annotatedStringResource(R.string.communitUrlJoinScanned) + } + } + + GROUP -> { + if (data.joined) { + Phrase.from(context, R.string.groupNameContainedUrlOpenCommunity) + .put(COMMUNITY_NAME_KEY, data.name) + .format() + } else { + annotatedStringResource(R.string.groupNameContainedUrlJoinCommunity) + } + } + + SEARCH -> { + annotatedStringResource(R.string.globalSearchUrlJoinCommunity) + } + } + + val openOrJoinButton = DialogButtonData( + text = if(data.joined) GetString(R.string.open) else GetString(R.string.join), + onClick = { openOrJoinCommunity(data.url) } + ) + + val copyUrlButton = if(data.allowCopyUrl){ + DialogButtonData( + text = GetString(android.R.string.copyUrl), + onClick = { + context.copyURLToClipboard(data.url) + Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show() + } + ) + } else null + + val buttons = when (data.displayType) { + CONVERSATION -> listOfNotNull( + copyUrlButton, + openOrJoinButton + ) + + else -> + listOf( + DialogButtonData( + text = GetString(android.R.string.cancel) + ), + openOrJoinButton + ) + } + + + AlertDialog( + onDismissRequest = onDismissRequest, + modifier = modifier, + title = annotatedStringResource(title), + text = annotatedStringResource(text), + showCloseButton = data.displayType == CONVERSATION, + buttons = buttons, + ) +} + + +@Preview +@Composable +fun PreviewNewCommunityConvo() { + PreviewTheme { + CommunityLinkAlertDialog( + data = LinkType.CommunityLink( + url = "https://getsession.org/", + name = "Session", + joined = false, + displayType = CONVERSATION + ), + openOrJoinCommunity = {}, + onDismissRequest = {}, + ) + } +} + +@Preview +@Composable +fun PreviewExistingCommunityConvo() { + PreviewTheme { + CommunityLinkAlertDialog( + data = LinkType.CommunityLink( + url = "https://getsession.org/", + name = "Session", + joined = true, + displayType = CONVERSATION + ), + openOrJoinCommunity = {}, + onDismissRequest = {}, + ) + } +} + +@Preview +@Composable +fun PreviewNewCommunityScanned() { + PreviewTheme { + CommunityLinkAlertDialog( + data = LinkType.CommunityLink( + url = "https://getsession.org/", + name = "Session", + joined = false, + displayType = SCANNED + ), + openOrJoinCommunity = {}, + onDismissRequest = {}, + ) + } +} + +@Preview +@Composable +fun PreviewExistingCommunityScanned() { + PreviewTheme { + CommunityLinkAlertDialog( + data = LinkType.CommunityLink( + url = "https://getsession.org/", + name = "Session", + joined = true, + displayType = SCANNED + ), + openOrJoinCommunity = {}, + onDismissRequest = {}, + ) + } +} + +@Preview +@Composable +fun PreviewNewCommunityEntered() { + PreviewTheme { + CommunityLinkAlertDialog( + data = LinkType.CommunityLink( + url = "https://getsession.org/", + name = "Session", + joined = false, + displayType = ENTERED + ), + openOrJoinCommunity = {}, + onDismissRequest = {}, + ) + } +} + +@Preview +@Composable +fun PreviewExistingCommunityEntered() { + PreviewTheme { + CommunityLinkAlertDialog( + data = LinkType.CommunityLink( + url = "https://getsession.org/", + name = "Session", + joined = true, + displayType = ENTERED + ), + openOrJoinCommunity = {}, + onDismissRequest = {}, + ) + } +} + +@Preview +@Composable +fun PreviewNewCommunityGroup() { + PreviewTheme { + CommunityLinkAlertDialog( + data = LinkType.CommunityLink( + url = "https://getsession.org/", + name = "Session", + joined = false, + displayType = GROUP + ), + openOrJoinCommunity = {}, + onDismissRequest = {}, + ) + } +} + +@Preview +@Composable +fun PreviewExistingCommunityGroup() { + PreviewTheme { + CommunityLinkAlertDialog( + data = LinkType.CommunityLink( + url = "https://getsession.org/", + name = "Session", + joined = true, + displayType = GROUP + ), + openOrJoinCommunity = {}, + onDismissRequest = {}, + ) + } +} + +@Preview +@Composable +fun PreviewNewCommunitySearch() { + PreviewTheme { + CommunityLinkAlertDialog( + data = LinkType.CommunityLink( + url = "https://getsession.org/", + name = "Session", + joined = true, + displayType = SEARCH + ), + openOrJoinCommunity = {}, + onDismissRequest = {}, + ) + } +} + + +@Preview +@Composable +fun PreviewOpenURLDialog() { + PreviewTheme { + OpenURLAlertDialog( + url = "https://getsession.org/", + onDismissRequest = {} + ) + } +} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt index 67a717557f..038f641a93 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import network.loki.messenger.libsession_util.util.ExpiryMode import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat import org.junit.Before import org.junit.Rule @@ -33,6 +34,8 @@ import org.thoughtcrime.securesms.BaseViewModelTest import org.thoughtcrime.securesms.MainCoroutineRule import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.links.LinkChecker +import org.thoughtcrime.securesms.links.LinkType import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUtils @@ -84,7 +87,10 @@ class ConversationViewModelTest : BaseViewModelTest() { private val threadId = 12345L - private fun createViewModel(recipient: Recipient): ConversationViewModel { + private fun createViewModel( + recipient: Recipient, + linkChecker: LinkChecker = mock(), + ): ConversationViewModel { return ConversationViewModel( repository = repository, storage = mock{ @@ -130,6 +136,7 @@ class ConversationViewModelTest : BaseViewModelTest() { on { changesNotification } doReturn MutableSharedFlow() }, openGroupManager = mock(), + linkChecker = linkChecker, attachmentDownloadJobFactory = mock(), communityApiExecutor = mock(), deleteAllReactionsApiFactory = mock(), @@ -240,4 +247,26 @@ class ConversationViewModelTest : BaseViewModelTest() { // Then it should be removed assertThat(viewModel.uiMessages.value.size, equalTo(0)) } -} \ No newline at end of file + + @Test + fun `handle link shows community dialog when checker detects community`() = runBlockingTest { + val url = "https://session.example/room?public_key=${"a".repeat(64)}" + val communityLink = LinkType.CommunityLink( + url = url, + name = "Session Room", + joined = false, + displayType = LinkType.CommunityLink.DisplayType.CONVERSATION, + ) + val linkChecker = mock { + onBlocking { check(url) } doReturn communityLink + } + val viewModel = createViewModel( + recipient = standardRecipient, + linkChecker = linkChecker, + ) + + viewModel.onCommand(ConversationViewModel.Commands.HandleLink(url)) + + assertThat(viewModel.dialogsState.value.urlDialog, equalTo(communityLink)) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/links/LinkCheckerTest.kt b/app/src/test/java/org/thoughtcrime/securesms/links/LinkCheckerTest.kt new file mode 100644 index 0000000000..ef360720f5 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/links/LinkCheckerTest.kt @@ -0,0 +1,119 @@ +package org.thoughtcrime.securesms.links + +import com.google.common.truth.Truth.assertThat +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import network.loki.messenger.libsession_util.ReadableUserGroupsConfig +import network.loki.messenger.libsession_util.util.GroupInfo +import org.junit.Test +import org.session.libsession.messaging.open_groups.OpenGroupApi +import org.session.libsession.utilities.ConfigFactoryProtocol +import org.session.libsession.utilities.UserConfigs +import org.thoughtcrime.securesms.database.CommunityDatabase + +class LinkCheckerTest { + + @Test + fun `returns generic link when no rule matches`() = runTest { + val checker = LinkChecker(rules = emptyList()) + + assertThat(checker.check(" https://getsession.org ")).isEqualTo( + LinkType.GenericLink("https://getsession.org") + ) + } + + @Test + fun `returns generic link for a regular url`() = runTest { + val checker = checker( + joinedCommunity = null, + roomInfo = null, + ) + + assertThat(checker.check("https://getsession.org")).isEqualTo( + LinkType.GenericLink("https://getsession.org") + ) + } + + @Test + fun `detects community links and falls back to room token when no local name exists`() = runTest { + val checker = checker( + joinedCommunity = null, + roomInfo = null, + ) + + assertThat(checker.check(communityUrl())).isEqualTo( + LinkType.CommunityLink( + url = communityUrl(), + name = ROOM, + joined = false, + displayType = LinkType.CommunityLink.DisplayType.CONVERSATION, + ) + ) + } + + @Test + fun `uses cached room name and joined flag when matching community already exists`() = runTest { + val checker = checker( + joinedCommunity = mockJoinedCommunity(), + roomInfo = roomInfo(name = "Session Lounge"), + ) + + assertThat(checker.check(communityUrl())).isEqualTo( + LinkType.CommunityLink( + url = communityUrl(), + name = "Session Lounge", + joined = true, + displayType = LinkType.CommunityLink.DisplayType.CONVERSATION, + ) + ) + } + + private fun checker( + joinedCommunity: GroupInfo.CommunityGroupInfo?, + roomInfo: OpenGroupApi.RoomInfo?, + ): LinkChecker { + val configFactory = mockk(relaxed = true) + val userConfigs = mockk() + val userGroups = mockk() + val communityDatabase = mockk() + + every { configFactory.dangerouslyAccessUserConfigs() } returns (userConfigs to {}) + every { userConfigs.userGroups } returns userGroups + every { userGroups.getCommunityInfo(SERVER, ROOM) } returns joinedCommunity + every { communityDatabase.getRoomInfo(any()) } returns roomInfo + + return LinkChecker( + rules = listOf( + CommunityLinkRule( + configFactory = configFactory, + communityDatabase = communityDatabase, + ) + ) + ) + } + + private fun mockJoinedCommunity(): GroupInfo.CommunityGroupInfo { + return mockk(relaxed = true) + } + + private fun roomInfo(name: String): OpenGroupApi.RoomInfo { + return OpenGroupApi.RoomInfo( + token = ROOM, + details = OpenGroupApi.RoomInfoDetails( + token = ROOM, + name = name, + ) + ) + } + + private fun communityUrl(publicKey: String = PUBLIC_KEY): String { + return "$SERVER/$ROOM?public_key=$publicKey" + } + + private companion object { + const val SERVER = "https://session.example" + const val ROOM = "session-room" + val PUBLIC_KEY = "a".repeat(64) + } +}