From c113a447cf3cef563b26a004286c6c45e08c2bb5 Mon Sep 17 00:00:00 2001 From: ceokot Date: Fri, 14 Jan 2022 07:56:15 +0200 Subject: [PATCH] refactor: Use view binding to replace Kotlin synthetics (#824) * refactor: Migrate home screen to data binding * Add view binding * Migrate ConversationView to view binding * Migrate ConversationActivityV2 to view binding * View model refactor * Move more functionality to the view model * Add ui state events flow * Update conversation item bindings * Update profile picture view bindings * Replace Kotlin synthetics with view bindings * Fix qr code fragment binding and optimize imports * View binding refactors * Make TextSecurePreferences an interface and add an implementation to improve testability * Add conversation repository * Migrate remaining TextSecurePreferences functions into the interface * Add unit conversation unit tests * Add unit test coverage for remaining view model functions --- app/build.gradle | 49 +- .../components/LabeledSeparatorView.kt | 14 +- .../components/ProfilePictureView.kt | 37 +- .../contacts/ContactSelectionListAdapter.kt | 27 +- .../contacts/ContactSelectionListFragment.kt | 47 +- .../contacts/SelectContactsActivity.kt | 21 +- .../securesms/contacts/UserView.kt | 34 +- .../conversation/v2/ConversationActivityV2.kt | 720 ++++---- .../conversation/v2/ConversationAdapter.kt | 15 +- .../v2/ConversationRecyclerView.kt | 10 +- .../conversation/v2/ConversationViewModel.kt | 130 ++ .../v2/DeleteOptionsBottomSheet.kt | 24 +- .../conversation/v2/MessageDetailActivity.kt | 25 +- .../conversation/v2/ModalUrlBottomSheet.kt | 24 +- .../v2/components/AlbumThumbnailView.kt | 40 +- .../v2/components/LinkPreviewDraftView.kt | 24 +- .../MentionCandidateSelectionView.kt | 2 +- .../v2/components/MentionCandidateView.kt | 19 +- .../v2/components/OpenGroupGuidelinesView.kt | 16 +- .../TypingIndicatorViewContainer.kt | 10 +- .../conversation/v2/dialogs/BlockedDialog.kt | 14 +- .../conversation/v2/dialogs/DownloadDialog.kt | 14 +- .../v2/dialogs/JoinOpenGroupDialog.kt | 14 +- .../v2/dialogs/LinkPreviewDialog.kt | 11 +- .../conversation/v2/dialogs/SendSeedDialog.kt | 11 +- .../conversation/v2/input_bar/InputBar.kt | 69 +- .../v2/input_bar/InputBarEditText.kt | 4 +- .../v2/input_bar/InputBarRecordingView.kt | 62 +- .../mentions/MentionCandidateView.kt | 20 +- .../mentions/MentionCandidatesView.kt | 3 +- .../v2/menus/ConversationMenuHelper.kt | 25 +- .../v2/messages/ControlMessageView.kt | 24 +- .../v2/messages/DeletedMessageView.kt | 16 +- .../conversation/v2/messages/DocumentView.kt | 14 +- .../v2/messages/LinkPreviewView.kt | 17 +- .../v2/messages/OpenGroupInvitationView.kt | 20 +- .../conversation/v2/messages/QuoteView.kt | 131 +- .../v2/messages/UntrustedAttachmentView.kt | 12 +- .../v2/messages/VisibleMessageContentView.kt | 89 +- .../v2/messages/VisibleMessageView.kt | 165 +- .../v2/messages/VoiceMessageView.kt | 21 +- .../conversation/v2/search/SearchBottomBar.kt | 9 +- .../v2/utilities/KThumbnailView.kt | 19 +- .../securesms/dependencies/AppModule.kt | 22 + .../dms/CreatePrivateChatActivity.kt | 81 +- .../ClosedGroupEditingOptionsBottomSheet.kt | 15 +- .../groups/CreateClosedGroupActivity.kt | 27 +- .../groups/EditClosedGroupActivity.kt | 6 +- .../groups/JoinPublicChatActivity.kt | 65 +- .../groups/OpenGroupGuidelinesActivity.kt | 8 +- .../home/ConversationOptionsBottomSheet.kt | 64 +- .../securesms/home/ConversationView.kt | 75 +- .../securesms/home/HomeActivity.kt | 170 +- .../securesms/home/HomeAdapter.kt | 15 +- .../securesms/home/PathActivity.kt | 29 +- .../securesms/home/UserDetailsBottomSheet.kt | 115 +- .../securesms/mediasend/Camera1Fragment.java | 4 +- .../mediasend/MediaPickerItemFragment.java | 4 +- .../mediasend/MediaSendFragment.java | 4 +- .../onboarding/DisplayNameActivity.kt | 38 +- .../securesms/onboarding/FakeChatView.kt | 30 +- .../securesms/onboarding/LandingActivity.kt | 18 +- .../onboarding/LinkDeviceActivity.kt | 57 +- .../securesms/onboarding/PNModeActivity.kt | 50 +- .../RecoveryPhraseRestoreActivity.kt | 17 +- .../securesms/onboarding/RegisterActivity.kt | 19 +- .../securesms/onboarding/SeedActivity.kt | 57 +- .../securesms/onboarding/SeedReminderView.kt | 25 +- .../preferences/ClearAllDataDialog.kt | 42 +- .../securesms/preferences/QRCodeActivity.kt | 28 +- .../securesms/preferences/SeedDialog.kt | 12 +- .../securesms/preferences/SettingsActivity.kt | 96 +- .../securesms/preferences/ShareLogsDialog.kt | 13 +- .../repository/ConversationRepository.kt | 229 +++ .../securesms/repository/ResultOf.kt | 43 + .../securesms/util/ScanQRCodeFragment.kt | 33 +- .../util/ScanQRCodePlaceholderFragment.kt | 13 +- app/src/main/res/layout/activity_home.xml | 20 +- .../main/res/layout/message_audio_view.xml | 108 -- .../securesms/BaseCoroutineTest.kt | 15 + .../securesms/CoroutineTestRule.kt | 50 + .../securesms/LiveDataTestUtil.kt | 60 + .../securesms/BaseViewModelTest.kt | 11 + .../v2/ConversationViewModelTest.kt | 173 ++ .../securesms/jobs/FastJobStorageTest.java | 18 +- .../recipients/RecipientExporterTest.java | 55 +- .../service/VerificationCodeParserTest.java | 61 - .../securesms/util/DelimiterUtilTest.java | 56 - .../securesms/util/DelimiterUtilTest.kt | 50 + .../util/PhoneNumberFormatterTest.java | 67 - .../dynamiclanguage/LanguageStringTest.java | 1 + .../dynamiclanguage/LocaleParserTest.java | 2 + build.gradle | 4 +- gradle.properties | 10 +- gradle/wrapper/gradle-wrapper.properties | 5 +- libsession/build.gradle | 7 +- .../libsession/messaging/jobs/JobQueue.kt | 24 +- .../utilities/TextSecurePreferences.kt | 1551 ++++++++++++----- 98 files changed, 3579 insertions(+), 2365 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/repository/ResultOf.kt delete mode 100644 app/src/main/res/layout/message_audio_view.xml create mode 100644 app/src/sharedTest/java/org/thoughtcrime/securesms/BaseCoroutineTest.kt create mode 100644 app/src/sharedTest/java/org/thoughtcrime/securesms/CoroutineTestRule.kt create mode 100644 app/src/sharedTest/java/org/thoughtcrime/securesms/LiveDataTestUtil.kt create mode 100644 app/src/test/java/org/thoughtcrime/securesms/BaseViewModelTest.kt create mode 100644 app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/service/VerificationCodeParserTest.java delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/DelimiterUtilTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/DelimiterUtilTest.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/PhoneNumberFormatterTest.java diff --git a/app/build.gradle b/app/build.gradle index c32fff90f..92773b9b5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,18 +4,17 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.2' classpath files('libs/gradle-witness.jar') classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" - classpath "com.google.gms:google-services:4.3.3" - classpath 'com.google.dagger:hilt-android-gradle-plugin:2.38.1' + classpath "com.google.gms:google-services:4.3.10" + classpath "com.google.dagger:hilt-android-gradle-plugin:$daggerVersion" } } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'witness' apply plugin: 'kotlin-kapt' apply plugin: 'com.google.gms.google-services' @@ -32,16 +31,16 @@ dependencies { implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' - implementation 'androidx.exifinterface:exifinterface:1.2.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.1' - implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.lifecycle:lifecycle-common-java8:2.3.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1' + implementation 'androidx.exifinterface:exifinterface:1.3.3' + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion" implementation 'androidx.activity:activity-ktx:1.2.2' implementation 'androidx.fragment:fragment-ktx:1.3.2' implementation "androidx.core:core-ktx:1.3.2" @@ -62,9 +61,9 @@ dependencies { implementation 'org.apache.httpcomponents:httpclient-android:4.3.5' implementation 'commons-net:commons-net:3.7.2' implementation 'com.github.chrisbanes:PhotoView:2.1.3' - implementation 'com.github.bumptech.glide:glide:4.11.0' - annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0' - kapt 'com.github.bumptech.glide:compiler:4.11.0' + implementation "com.github.bumptech.glide:glide:$glideVersion" + annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" + kapt "com.github.bumptech.glide:compiler:$glideVersion" implementation 'com.makeramen:roundedimageview:2.1.0' implementation 'com.pnikosis:materialish-progress:1.5' implementation 'org.greenrobot:eventbus:3.0.0' @@ -72,8 +71,8 @@ dependencies { implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0' implementation 'com.melnykov:floatingactionbutton:1.3.0' implementation 'com.google.zxing:android-integration:3.1.0' - implementation "com.google.dagger:hilt-android:2.38.1" - kapt "com.google.dagger:hilt-compiler:2.38.1" + implementation "com.google.dagger:hilt-android:$daggerVersion" + kapt "com.google.dagger:hilt-compiler:$daggerVersion" implementation 'mobi.upod:time-duration-picker:1.1.3' implementation 'com.google.zxing:core:3.2.1' implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') { @@ -103,7 +102,7 @@ dependencies { } implementation project(":libsignal") implementation project(":libsession") - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion" implementation "org.whispersystems:curve25519-java:$curve25519Version" implementation 'com.goterl:lazysodium-android:5.0.2@aar' implementation "net.java.dev.jna:jna:5.8.0@aar" @@ -111,7 +110,7 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" implementation "nl.komponents.kovenant:kovenant-android:$kovenantVersion" implementation "com.github.lelloman:android-identicons:v11" @@ -122,12 +121,15 @@ dependencies { implementation "com.opencsv:opencsv:4.6" testImplementation 'junit:junit:4.12' testImplementation 'org.assertj:assertj-core:3.11.1' - testImplementation 'org.mockito:mockito-core:1.10.8' + testImplementation "org.mockito:mockito-inline:4.0.0" + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" testImplementation 'org.powermock:powermock-api-mockito:1.6.1' testImplementation 'org.powermock:powermock-module-junit4:1.6.1' testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1' testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1' testImplementation 'androidx.test:core:1.3.0' + testImplementation "androidx.arch.core:core-testing:2.1.0" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" // Core library androidTestImplementation 'androidx.test:core:1.4.0' @@ -231,6 +233,12 @@ android { } } + sourceSets { + String sharedTestDir = 'src/sharedTest/java' + test.java.srcDirs += sharedTestDir + androidTest.java.srcDirs += sharedTestDir + } + buildTypes { release { minifyEnabled false @@ -279,6 +287,7 @@ android { buildFeatures { dataBinding true + viewBinding true } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt index 34273e565..df36719db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt @@ -7,13 +7,14 @@ import android.graphics.Path import android.util.AttributeSet import android.view.LayoutInflater import android.widget.RelativeLayout -import kotlinx.android.synthetic.main.view_separator.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewSeparatorBinding import org.thoughtcrime.securesms.util.toPx import org.session.libsession.utilities.ThemeUtil class LabeledSeparatorView : RelativeLayout { + private lateinit var binding: ViewSeparatorBinding private val path = Path() private val paint: Paint by lazy { @@ -43,10 +44,9 @@ class LabeledSeparatorView : RelativeLayout { } private fun setUpViewHierarchy() { - val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - val contentView = inflater.inflate(R.layout.view_separator, null) + binding = ViewSeparatorBinding.inflate(LayoutInflater.from(context)) val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - addView(contentView, layoutParams) + addView(binding.root, layoutParams) setWillNotDraw(false) } // endregion @@ -59,9 +59,9 @@ class LabeledSeparatorView : RelativeLayout { val hMargin = toPx(16, resources).toFloat() path.reset() path.moveTo(0.0f, h / 2) - path.lineTo(titleTextView.left - hMargin, h / 2) - path.addRoundRect(titleTextView.left - hMargin, toPx(1, resources).toFloat(), titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW) - path.moveTo(titleTextView.right + hMargin, h / 2) + path.lineTo(binding.titleTextView.left - hMargin, h / 2) + path.addRoundRect(binding.titleTextView.left - hMargin, toPx(1, resources).toFloat(), binding.titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW) + path.moveTo(binding.titleTextView.right + hMargin, h / 2) path.lineTo(w, h / 2) path.close() c.drawPath(path, paint) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt index 59dfd6c96..df8544a39 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt @@ -8,8 +8,8 @@ import android.widget.ImageView import android.widget.RelativeLayout import androidx.annotation.DimenRes import com.bumptech.glide.load.engine.DiskCacheStrategy -import kotlinx.android.synthetic.main.view_profile_picture.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewProfilePictureBinding import org.session.libsession.avatars.ProfileContactPhoto import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.Address @@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator class ProfilePictureView : RelativeLayout { + private lateinit var binding: ViewProfilePictureBinding lateinit var glide: GlideRequests var publicKey: String? = null var displayName: String? = null @@ -35,14 +36,12 @@ class ProfilePictureView : RelativeLayout { constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { initialize() } private fun initialize() { - val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - val contentView = inflater.inflate(R.layout.view_profile_picture, null) - addView(contentView) + binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this, true) } // endregion // region Updating - fun update(recipient: Recipient, threadID: Long) { + fun update(recipient: Recipient) { fun getUserDisplayName(publicKey: String): String { val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey) return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey @@ -75,27 +74,27 @@ class ProfilePictureView : RelativeLayout { val publicKey = publicKey ?: return val additionalPublicKey = additionalPublicKey if (additionalPublicKey != null) { - setProfilePictureIfNeeded(doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size) - setProfilePictureIfNeeded(doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size) - doubleModeImageViewContainer.visibility = View.VISIBLE + setProfilePictureIfNeeded(binding.doubleModeImageView1, publicKey, displayName, R.dimen.small_profile_picture_size) + setProfilePictureIfNeeded(binding.doubleModeImageView2, additionalPublicKey, additionalDisplayName, R.dimen.small_profile_picture_size) + binding.doubleModeImageViewContainer.visibility = View.VISIBLE } else { - glide.clear(doubleModeImageView1) - glide.clear(doubleModeImageView2) - doubleModeImageViewContainer.visibility = View.INVISIBLE + glide.clear(binding.doubleModeImageView1) + glide.clear(binding.doubleModeImageView2) + binding.doubleModeImageViewContainer.visibility = View.INVISIBLE } if (additionalPublicKey == null && !isLarge) { - setProfilePictureIfNeeded(singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size) - singleModeImageView.visibility = View.VISIBLE + setProfilePictureIfNeeded(binding.singleModeImageView, publicKey, displayName, R.dimen.medium_profile_picture_size) + binding.singleModeImageView.visibility = View.VISIBLE } else { - glide.clear(singleModeImageView) - singleModeImageView.visibility = View.INVISIBLE + glide.clear(binding.singleModeImageView) + binding.singleModeImageView.visibility = View.INVISIBLE } if (additionalPublicKey == null && isLarge) { - setProfilePictureIfNeeded(largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size) - largeSingleModeImageView.visibility = View.VISIBLE + setProfilePictureIfNeeded(binding.largeSingleModeImageView, publicKey, displayName, R.dimen.large_profile_picture_size) + binding.largeSingleModeImageView.visibility = View.VISIBLE } else { - glide.clear(largeSingleModeImageView) - largeSingleModeImageView.visibility = View.INVISIBLE + glide.clear(binding.largeSingleModeImageView) + binding.largeSingleModeImageView.visibility = View.INVISIBLE } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt index 73be89270..d7a45c317 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt @@ -1,15 +1,12 @@ package org.thoughtcrime.securesms.contacts import android.content.Context -import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import kotlinx.android.synthetic.main.contact_selection_list_divider.view.* -import network.loki.messenger.R -import org.thoughtcrime.securesms.contacts.UserView -import org.thoughtcrime.securesms.mms.GlideRequests +import androidx.recyclerview.widget.RecyclerView +import network.loki.messenger.databinding.ContactSelectionListDividerBinding import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.mms.GlideRequests class ContactSelectionListAdapter(private val context: Context, private val multiSelect: Boolean) : RecyclerView.Adapter() { lateinit var glide: GlideRequests @@ -24,7 +21,15 @@ class ContactSelectionListAdapter(private val context: Context, private val mult } class UserViewHolder(val view: UserView) : RecyclerView.ViewHolder(view) - class DividerViewHolder(val view: View) : RecyclerView.ViewHolder(view) + class DividerViewHolder( + private val binding: ContactSelectionListDividerBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: ContactSelectionListItem.Header) { + with(binding){ + label.text = item.name + } + } + } override fun getItemCount(): Int { return items.size @@ -41,8 +46,9 @@ class ContactSelectionListAdapter(private val context: Context, private val mult return if (viewType == ViewType.Contact) { UserViewHolder(UserView(context)) } else { - val view = LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false) - DividerViewHolder(view) + DividerViewHolder( + ContactSelectionListDividerBinding.inflate(LayoutInflater.from(context), parent, false) + ) } } @@ -58,8 +64,7 @@ class ContactSelectionListAdapter(private val context: Context, private val mult if (multiSelect) UserView.ActionIndicator.Tick else UserView.ActionIndicator.None, isSelected) } else if (viewHolder is DividerViewHolder) { - item as ContactSelectionListItem.Header - viewHolder.view.label.text = item.name + viewHolder.bind(item as ContactSelectionListItem.Header) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt index b32e5a20b..24637c434 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt @@ -1,23 +1,21 @@ package org.thoughtcrime.securesms.contacts import android.os.Bundle -import androidx.fragment.app.Fragment -import androidx.loader.app.LoaderManager -import androidx.loader.content.Loader -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener -import androidx.recyclerview.widget.LinearLayoutManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import kotlinx.android.synthetic.main.contact_selection_list_fragment.* -import network.loki.messenger.R +import androidx.fragment.app.Fragment +import androidx.loader.app.LoaderManager +import androidx.loader.content.Loader +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import network.loki.messenger.databinding.ContactSelectionListFragmentBinding +import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.mms.GlideApp -import org.session.libsession.utilities.recipients.Recipient -import org.thoughtcrime.securesms.contacts.ContactSelectionListItem -import org.thoughtcrime.securesms.contacts.ContactSelectionListLoader class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks>, ContactClickListener { + private lateinit var binding: ContactSelectionListFragmentBinding private var cursorFilter: String? = null var onContactSelectedListener: OnContactSelectedListener? = null @@ -46,20 +44,21 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks> { @@ -107,8 +106,8 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks> { + private lateinit var binding: ActivitySelectContactsBinding private var members = listOf() set(value) { field = value; selectContactsAdapter.members = value } private lateinit var usersToExclude: Set @@ -36,18 +33,18 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) - - setContentView(R.layout.activity_select_contacts) + binding = ActivitySelectContactsBinding.inflate(layoutInflater) + setContentView(binding.root) supportActionBar!!.title = resources.getString(R.string.activity_select_contacts_title) usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf() val emptyStateText = intent.getStringExtra(emptyStateTextKey) if (emptyStateText != null) { - emptyStateMessageTextView.text = emptyStateText + binding.emptyStateMessageTextView.text = emptyStateText } - recyclerView.adapter = selectContactsAdapter - recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = selectContactsAdapter + binding.recyclerView.layoutManager = LinearLayoutManager(this) LoaderManager.getInstance(this).initLoader(0, null, this) } @@ -73,8 +70,8 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana private fun update(members: List) { this.members = members - mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE - emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE + binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE + binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE invalidateOptionsMenu() } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt index ebd8c93c6..7597be474 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt @@ -5,9 +5,8 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout -import kotlinx.android.synthetic.main.view_conversation.view.profilePictureView -import kotlinx.android.synthetic.main.view_user.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewUserBinding import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities @@ -15,6 +14,7 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.GlideRequests class UserView : LinearLayout { + private lateinit var binding: ViewUserBinding var openGroupThreadID: Long = -1 // FIXME: This is a bit ugly enum class ActionIndicator { @@ -41,9 +41,7 @@ class UserView : LinearLayout { } private fun setUpViewHierarchy() { - val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - val contentView = inflater.inflate(R.layout.view_user, null) - addView(contentView) + binding = ViewUserBinding.inflate(LayoutInflater.from(context), this, true) } // endregion @@ -56,28 +54,32 @@ class UserView : LinearLayout { val threadID = DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(user) MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, context) // FIXME: This is a bad place to do this val address = user.address.serialize() - profilePictureView.glide = glide - profilePictureView.update(user, threadID) - actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24) - nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address) + binding.profilePictureView.glide = glide + binding.profilePictureView.update(user) + binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24) + binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address) when (actionIndicator) { ActionIndicator.None -> { - actionIndicatorImageView.visibility = View.GONE + binding.actionIndicatorImageView.visibility = View.GONE } ActionIndicator.Menu -> { - actionIndicatorImageView.visibility = View.VISIBLE - actionIndicatorImageView.setImageResource(R.drawable.ic_more_horiz_white) + binding.actionIndicatorImageView.visibility = View.VISIBLE + binding.actionIndicatorImageView.setImageResource(R.drawable.ic_more_horiz_white) } ActionIndicator.Tick -> { - actionIndicatorImageView.visibility = View.VISIBLE - actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle) + binding.actionIndicatorImageView.visibility = View.VISIBLE + binding.actionIndicatorImageView.setImageResource( + if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle + ) } } } fun toggleCheckbox(isSelected: Boolean = false) { - actionIndicatorImageView.visibility = View.VISIBLE - actionIndicatorImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle) + binding.actionIndicatorImageView.visibility = View.VISIBLE + binding.actionIndicatorImageView.setImageResource( + if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle + ) } fun unbind() { 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 267403ff1..3140f5dea 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 @@ -3,65 +3,65 @@ package org.thoughtcrime.securesms.conversation.v2 import android.Manifest import android.animation.FloatEvaluator import android.animation.ValueAnimator -import android.content.* +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.DialogInterface +import android.content.Intent import android.content.res.Resources import android.database.Cursor import android.graphics.Rect import android.graphics.Typeface import android.net.Uri -import android.os.* +import android.os.AsyncTask +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.TextUtils import android.util.Log import android.util.Pair import android.util.TypedValue -import android.view.* +import android.view.ActionMode +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager import android.widget.LinearLayout import android.widget.RelativeLayout import android.widget.Toast +import androidx.activity.viewModels import androidx.annotation.DimenRes import androidx.appcompat.app.AlertDialog -import androidx.core.view.children import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.lifecycleScope import androidx.loader.app.LoaderManager import androidx.loader.content.Loader import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.annimon.stream.Stream import dagger.hilt.android.AndroidEntryPoint -import kotlinx.android.synthetic.main.activity_conversation_v2.* -import kotlinx.android.synthetic.main.activity_conversation_v2.view.* -import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.* -import kotlinx.android.synthetic.main.activity_home.* -import kotlinx.android.synthetic.main.view_conversation.view.* -import kotlinx.android.synthetic.main.view_input_bar.view.* -import kotlinx.android.synthetic.main.view_input_bar_recording.* -import kotlinx.android.synthetic.main.view_input_bar_recording.view.* -import kotlinx.android.synthetic.main.view_visible_message.view.* import network.loki.messenger.R -import nl.komponents.kovenant.ui.failUi +import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding +import network.loki.messenger.databinding.ActivityConversationV2Binding import nl.komponents.kovenant.ui.successUi -import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.MentionsManager import org.session.libsession.messaging.messages.control.DataExtractionNotification -import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage -import org.session.libsession.messaging.messages.visible.OpenGroupInvitation import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel -import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized -import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.concurrent.SimpleTask @@ -71,14 +71,14 @@ import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.utilities.ListenableFuture import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.hexEncodedPrivateKey -import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.audio.AudioRecorder -import org.thoughtcrime.securesms.contacts.SelectContactsActivity import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher -import org.thoughtcrime.securesms.conversation.v2.dialogs.* +import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog +import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog +import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate @@ -86,14 +86,27 @@ import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCand import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper -import org.thoughtcrime.securesms.conversation.v2.messages.* +import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate +import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView +import org.thoughtcrime.securesms.conversation.v2.messages.VoiceMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel -import org.thoughtcrime.securesms.conversation.v2.utilities.* +import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.database.* -import org.thoughtcrime.securesms.database.DraftDatabase.Drafts +import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.LokiAPIDatabase +import org.thoughtcrime.securesms.database.LokiMessageDatabase +import org.thoughtcrime.securesms.database.LokiThreadDatabase +import org.thoughtcrime.securesms.database.MmsDatabase +import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.SessionContactDatabase +import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.giph.ui.GiphyActivity @@ -103,17 +116,30 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivity -import org.thoughtcrime.securesms.mms.* +import org.thoughtcrime.securesms.mms.AudioSlide +import org.thoughtcrime.securesms.mms.GifSlide +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.mms.ImageSlide +import org.thoughtcrime.securesms.mms.MediaConstraints +import org.thoughtcrime.securesms.mms.Slide +import org.thoughtcrime.securesms.mms.SlideDeck +import org.thoughtcrime.securesms.mms.VideoSlide import org.thoughtcrime.securesms.notifications.MarkReadReceiver import org.thoughtcrime.securesms.permissions.Permissions -import org.thoughtcrime.securesms.util.* -import java.util.* +import org.thoughtcrime.securesms.util.ActivityDispatcher +import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.SaveAttachmentTask +import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.util.toPx +import java.util.Locale import java.util.concurrent.ExecutionException import javax.inject.Inject -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set -import kotlin.math.* +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.math.sqrt // Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually // part of the conversation activity layout. This is just because it makes the layout a lot simpler. The @@ -124,23 +150,36 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener, SearchBottomBar.EventListener, VoiceMessageViewDelegate { + private lateinit var binding: ActivityConversationV2Binding + private lateinit var actionBarBinding: ActivityConversationV2ActionBarBinding + + @Inject lateinit var textSecurePreferences: TextSecurePreferences @Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var mmsSmsDb: MmsSmsDatabase - @Inject lateinit var draftDb: DraftDatabase @Inject lateinit var lokiThreadDb: LokiThreadDatabase @Inject lateinit var sessionContactDb: SessionContactDatabase @Inject lateinit var groupDb: GroupDatabase @Inject lateinit var lokiApiDb: LokiAPIDatabase - @Inject lateinit var recipientDb: RecipientDatabase @Inject lateinit var smsDb: SmsDatabase @Inject lateinit var mmsDb: MmsDatabase @Inject lateinit var lokiMessageDb: LokiMessageDatabase - - + @Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory private val screenWidth = Resources.getSystem().displayMetrics.widthPixels - private var linkPreviewViewModel: LinkPreviewViewModel? = null - private var threadID: Long = -1 + private val linkPreviewViewModel: LinkPreviewViewModel by lazy { + ViewModelProvider(this, LinkPreviewViewModel.Factory(LinkPreviewRepository(this))) + .get(LinkPreviewViewModel::class.java) + } + private val viewModel: ConversationViewModel by viewModels { + var threadId = intent.getLongExtra(THREAD_ID, -1L) + if (threadId == -1L) { + intent.getParcelableExtra
(ADDRESS)?.let { address -> + val recipient = Recipient.from(this, address, false) + threadId = threadDb.getOrCreateThreadIdFor(recipient) + } ?: finish() + } + viewModelFactory.create(threadId) + } private var actionMode: ActionMode? = null private var unreadCount = 0 // Attachments @@ -157,7 +196,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private var currentMentionStartIndex = -1 private var isShowingMentionCandidatesView = false // Search - var searchViewModel: SearchViewModel? = null + val searchViewModel: SearchViewModel by viewModels() var searchViewItem: MenuItem? = null private val isScrolledToBottom: Boolean @@ -167,7 +206,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } private val layoutManager: LinearLayoutManager - get() { return conversationRecyclerView.layoutManager as LinearLayoutManager } + get() { return binding.conversationRecyclerView.layoutManager as LinearLayoutManager } private val seed by lazy { var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED) @@ -181,7 +220,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } private val adapter by lazy { - val cursor = mmsSmsDb.getConversation(threadID) + val cursor = mmsSmsDb.getConversation(viewModel.threadId) val adapter = ConversationAdapter( this, cursor, @@ -200,10 +239,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe adapter } - private val thread by lazy { - threadDb.getRecipientForThreadId(threadID)!! - } - private val glide by lazy { GlideApp.with(this) } private val lockViewHitMargin by lazy { toPx(40, resources) } private val gifButton by lazy { InputBarButton(this, R.drawable.ic_gif_white_24dp, hasOpaqueBackground = true, isGIFButton = true) } @@ -231,14 +266,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) - setContentView(R.layout.activity_conversation_v2) - threadID = intent.getLongExtra(THREAD_ID, -1L) - if (threadID == -1L) { - val address = intent.getParcelableExtra
(ADDRESS) ?: return finish() - val recipient = Recipient.from(this, address, false) - threadID = threadDb.getOrCreateThreadIdFor(recipient) - } - val thread = threadDb.getRecipientForThreadId(threadID) + binding = ActivityConversationV2Binding.inflate(layoutInflater) + setContentView(binding.root) + val thread = threadDb.getRecipientForThreadId(viewModel.threadId) if (thread == null) { Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() return finish() @@ -248,28 +278,28 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe setUpInputBar() setUpLinkPreviewObserver() restoreDraftIfNeeded() - addOpenGroupGuidelinesIfNeeded() - scrollToBottomButton.setOnClickListener { - val layoutManager = conversationRecyclerView.layoutManager ?: return@setOnClickListener + setUpUiStateObserver() + binding.scrollToBottomButton.setOnClickListener { + val layoutManager = binding.conversationRecyclerView.layoutManager ?: return@setOnClickListener if (layoutManager.isSmoothScrolling) { - conversationRecyclerView.scrollToPosition(0) + binding.conversationRecyclerView.scrollToPosition(0) } else { - conversationRecyclerView.smoothScrollToPosition(0) + binding.conversationRecyclerView.smoothScrollToPosition(0) } } - unreadCount = mmsSmsDb.getUnreadCount(threadID) + unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId) updateUnreadCountIndicator() setUpTypingObserver() setUpRecipientObserver() updateSubtitle() getLatestOpenGroupInfoIfNeeded() setUpBlockedBanner() - searchBottomBar.setEventListener(this) + binding.searchBottomBar.setEventListener(this) setUpSearchResultObserver() scrollToFirstUnreadMessageIfNeeded() showOrHideInputIfNeeded() - if (this.thread.isOpenGroupRecipient) { - val openGroup = lokiThreadDb.getOpenGroupChat(threadID) + if (viewModel.recipient.isOpenGroupRecipient) { + val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) if (openGroup == null) { Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() return finish() @@ -279,7 +309,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun onResume() { super.onResume() - ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadID) + ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId) markAllAsRead() } @@ -305,14 +335,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } private fun setUpRecyclerView() { - conversationRecyclerView.adapter = adapter + binding.conversationRecyclerView.adapter = adapter val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true) - conversationRecyclerView.layoutManager = layoutManager + binding.conversationRecyclerView.layoutManager = layoutManager // Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will) LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks { override fun onCreateLoader(id: Int, bundle: Bundle?): Loader { - return ConversationLoader(threadID, this@ConversationActivityV2) + return ConversationLoader(viewModel.threadId, this@ConversationActivityV2) } override fun onLoadFinished(loader: Loader, cursor: Cursor?) { @@ -323,7 +353,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe adapter.changeCursor(null) } }) - conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { handleRecyclerViewScrolled() @@ -333,42 +363,43 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private fun setUpToolBar() { val actionBar = supportActionBar!! - actionBar.setCustomView(R.layout.activity_conversation_v2_action_bar) + actionBarBinding = ActivityConversationV2ActionBarBinding.inflate(layoutInflater) + actionBar.title = "" + actionBar.customView = actionBarBinding.root actionBar.setDisplayShowCustomEnabled(true) - conversationTitleView.text = thread.toShortString() - @DimenRes val sizeID: Int - if (thread.isClosedGroupRecipient) { - sizeID = R.dimen.medium_profile_picture_size + actionBarBinding.conversationTitleView.text = viewModel.recipient.toShortString() + @DimenRes val sizeID: Int = if (viewModel.recipient.isClosedGroupRecipient) { + R.dimen.medium_profile_picture_size } else { - sizeID = R.dimen.small_profile_picture_size + R.dimen.small_profile_picture_size } val size = resources.getDimension(sizeID).roundToInt() - profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size) - profilePictureView.glide = glide - MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(threadID, this) - profilePictureView.update(thread, threadID) + actionBarBinding.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size) + actionBarBinding.profilePictureView.glide = glide + MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this) + actionBarBinding.profilePictureView.update(viewModel.recipient) } private fun setUpInputBar() { - inputBar.delegate = this - inputBarRecordingView.delegate = this + binding.inputBar.delegate = this + binding.inputBarRecordingView.delegate = this // GIF button - gifButtonContainer.addView(gifButton) + binding.gifButtonContainer.addView(gifButton) gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) gifButton.onUp = { showGIFPicker() } gifButton.snIsEnabled = false // Document button - documentButtonContainer.addView(documentButton) + binding.documentButtonContainer.addView(documentButton) documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) documentButton.onUp = { showDocumentPicker() } documentButton.snIsEnabled = false // Library button - libraryButtonContainer.addView(libraryButton) + binding.libraryButtonContainer.addView(libraryButton) libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) libraryButton.onUp = { pickFromLibrary() } libraryButton.snIsEnabled = false // Camera button - cameraButtonContainer.addView(cameraButton) + binding.cameraButtonContainer.addView(cameraButton) cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) cameraButton.onUp = { showCamera() } cameraButton.snIsEnabled = false @@ -380,7 +411,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe if (mediaURI != null && mediaType != null) { if (AttachmentManager.MediaType.IMAGE == mediaType || AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType) { val media = Media(mediaURI, MediaUtil.getMimeType(this, mediaURI)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent()) - startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), thread, ""), ConversationActivityV2.PICK_FROM_LIBRARY) + startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient, ""), PICK_FROM_LIBRARY) return } else { prepMediaForSending(mediaURI, mediaType).addListener(object : ListenableFuture.Listener { @@ -397,98 +428,107 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } else if (intent.hasExtra(Intent.EXTRA_TEXT)) { val dataTextExtra = intent.getCharSequenceExtra(Intent.EXTRA_TEXT) ?: "" - inputBar.text = dataTextExtra.toString() + binding.inputBar.text = dataTextExtra.toString() } else { - val drafts = draftDb.getDrafts(threadID) - draftDb.clearDrafts(threadID) - val text = drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value ?: return - inputBar.text = text + viewModel.getDraft()?.let { text -> + binding.inputBar.text = text + } } } - private fun addOpenGroupGuidelinesIfNeeded() { - val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return - val isOxenHostedOpenGroup = openGroup.room == "session" || openGroup.room == "oxen" - || openGroup.room == "lokinet" || openGroup.room == "crypto" + private fun addOpenGroupGuidelinesIfNeeded(isOxenHostedOpenGroup: Boolean) { if (!isOxenHostedOpenGroup) { return } - openGroupGuidelinesView.visibility = View.VISIBLE - val recyclerViewLayoutParams = conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams + binding.openGroupGuidelinesView.visibility = View.VISIBLE + val recyclerViewLayoutParams = binding.conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams recyclerViewLayoutParams.topMargin = toPx(57, resources) // The height of the open group guidelines view is hardcoded to this - conversationRecyclerView.layoutParams = recyclerViewLayoutParams + binding.conversationRecyclerView.layoutParams = recyclerViewLayoutParams } private fun setUpTypingObserver() { - ApplicationContext.getInstance(this).typingStatusRepository.getTypists(threadID).observe(this) { state -> + ApplicationContext.getInstance(this).typingStatusRepository.getTypists(viewModel.threadId).observe(this) { state -> val recipients = if (state != null) state.typists else listOf() // FIXME: Also checking isScrolledToBottom is a quick fix for an issue where the // typing indicator overlays the recycler view when scrolled up - typingIndicatorViewContainer.isVisible = recipients.isNotEmpty() && isScrolledToBottom - typingIndicatorViewContainer.setTypists(recipients) - inputBarHeightChanged(inputBar.height) + binding.typingIndicatorViewContainer.isVisible = recipients.isNotEmpty() && isScrolledToBottom + binding.typingIndicatorViewContainer.setTypists(recipients) + inputBarHeightChanged(binding.inputBar.height) } - if (TextSecurePreferences.isTypingIndicatorsEnabled(this)) { - inputBar.inputBarEditText.addTextChangedListener(object : SimpleTextWatcher() { + if (textSecurePreferences.isTypingIndicatorsEnabled()) { + binding.inputBar.addTextChangedListener(object : SimpleTextWatcher() { override fun onTextChanged(text: String?) { - ApplicationContext.getInstance(this@ConversationActivityV2).typingStatusSender.onTypingStarted(threadID) + ApplicationContext.getInstance(this@ConversationActivityV2).typingStatusSender.onTypingStarted(viewModel.threadId) } }) } } private fun setUpRecipientObserver() { - thread.addListener(this) + viewModel.recipient.addListener(this) } private fun getLatestOpenGroupInfoIfNeeded() { - val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return + val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) ?: return OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() } } private fun setUpBlockedBanner() { - if (thread.isGroupRecipient) { return } - val contactDB = sessionContactDb - val sessionID = thread.address.toString() - val contact = contactDB.getContactWithSessionID(sessionID) + if (viewModel.recipient.isGroupRecipient) { return } + val sessionID = viewModel.recipient.address.toString() + val contact = sessionContactDb.getContactWithSessionID(sessionID) val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID - blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name) - blockedBanner.isVisible = thread.isBlocked - blockedBanner.setOnClickListener { unblock() } + binding.blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name) + binding.blockedBanner.isVisible = viewModel.recipient.isBlocked + binding.blockedBanner.setOnClickListener { viewModel.unblock() } } private fun setUpLinkPreviewObserver() { - val linkPreviewViewModel = ViewModelProviders.of(this, LinkPreviewViewModel.Factory(LinkPreviewRepository(this)))[LinkPreviewViewModel::class.java] - this.linkPreviewViewModel = linkPreviewViewModel - if (!TextSecurePreferences.isLinkPreviewsEnabled(this)) { + if (!textSecurePreferences.isLinkPreviewsEnabled()) { linkPreviewViewModel.onUserCancel(); return } - linkPreviewViewModel.linkPreviewState.observe(this, { previewState: LinkPreviewState? -> + linkPreviewViewModel.linkPreviewState.observe(this) { previewState: LinkPreviewState? -> if (previewState == null) return@observe - if (previewState.isLoading) { - inputBar.draftLinkPreview() - } else if (previewState.linkPreview.isPresent) { - inputBar.updateLinkPreviewDraft(glide, previewState.linkPreview.get()) - } else { - inputBar.cancelLinkPreviewDraft() + when { + previewState.isLoading -> { + binding.inputBar.draftLinkPreview() + } + previewState.linkPreview.isPresent -> { + binding.inputBar.updateLinkPreviewDraft(glide, previewState.linkPreview.get()) + } + else -> { + binding.inputBar.cancelLinkPreviewDraft() + } } - }) + } + } + + private fun setUpUiStateObserver() { + lifecycleScope.launchWhenStarted { + viewModel.uiState.collect { uiState -> + uiState.uiMessages.firstOrNull()?.let { + Toast.makeText(this@ConversationActivityV2, it.message, Toast.LENGTH_LONG).show() + viewModel.messageShown(it.id) + } + addOpenGroupGuidelinesIfNeeded(uiState.isOxenHostedOpenGroup) + } + } } private fun scrollToFirstUnreadMessageIfNeeded() { - val lastSeenTimestamp = threadDb.getLastSeenAndHasSent(threadID).first() + val lastSeenTimestamp = threadDb.getLastSeenAndHasSent(viewModel.threadId).first() val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return if (lastSeenItemPosition <= 3) { return } - conversationRecyclerView.scrollToPosition(lastSeenItemPosition) + binding.conversationRecyclerView.scrollToPosition(lastSeenItemPosition) } override fun onPrepareOptionsMenu(menu: Menu): Boolean { - ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, threadID, this) { onOptionsItemSelected(it) } + ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, viewModel.recipient, viewModel.threadId, this) { onOptionsItemSelected(it) } super.onPrepareOptionsMenu(menu) return true } override fun onDestroy() { - saveDraft() + viewModel.saveDraft(binding.inputBar.text.trim()) super.onDestroy() } // endregion @@ -496,28 +536,28 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // region Animation & Updating override fun onModified(recipient: Recipient) { runOnUiThread { - if (thread.isContactRecipient) { - blockedBanner.isVisible = thread.isBlocked + if (viewModel.recipient.isContactRecipient) { + binding.blockedBanner.isVisible = viewModel.recipient.isBlocked } updateSubtitle() showOrHideInputIfNeeded() - profilePictureView.update(recipient, threadID) + actionBarBinding.profilePictureView.update(recipient) } } private fun showOrHideInputIfNeeded() { - if (thread.isClosedGroupRecipient) { - val group = groupDb.getGroup(thread.address.toGroupString()).orNull() + if (viewModel.recipient.isClosedGroupRecipient) { + val group = groupDb.getGroup(viewModel.recipient.address.toGroupString()).orNull() val isActive = (group?.isActive == true) - inputBar.showInput = isActive + binding.inputBar.showInput = isActive } else { - inputBar.showInput = true + binding.inputBar.showInput = true } } private fun markAllAsRead() { - val messages = threadDb.setRead(threadID, true) - if (thread.isGroupRecipient) { + val messages = threadDb.setRead(viewModel.threadId, true) + if (viewModel.recipient.isGroupRecipient) { for (message in messages) { MarkReadReceiver.scheduleDeletion(this, message.expirationInfo) } @@ -531,41 +571,41 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe @Suppress("NAME_SHADOWING") val newValue = max(newValue, resources.getDimension(R.dimen.input_bar_height).roundToInt()) // 36 DP is the exact height of the typing indicator view. It's also exactly 18 * 2, and 18 is the large message // corner radius. This makes 36 DP look "correct" in the context of other messages on the screen. - val typingIndicatorHeight = if (typingIndicatorViewContainer.isVisible) toPx(36, resources) else 0 + val typingIndicatorHeight = if (binding.typingIndicatorViewContainer.isVisible) toPx(36, resources) else 0 // Recycler view - val recyclerViewLayoutParams = conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams + val recyclerViewLayoutParams = binding.conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams recyclerViewLayoutParams.bottomMargin = newValue + typingIndicatorHeight - conversationRecyclerView.layoutParams = recyclerViewLayoutParams + binding.conversationRecyclerView.layoutParams = recyclerViewLayoutParams // Additional content container - val additionalContentContainerLayoutParams = additionalContentContainer.layoutParams as RelativeLayout.LayoutParams + val additionalContentContainerLayoutParams = binding.additionalContentContainer.layoutParams as RelativeLayout.LayoutParams additionalContentContainerLayoutParams.bottomMargin = newValue - additionalContentContainer.layoutParams = additionalContentContainerLayoutParams + binding.additionalContentContainer.layoutParams = additionalContentContainerLayoutParams // Attachment options - val attachmentButtonHeight = inputBar.attachmentsButtonContainer.height - val bottomMargin = (newValue - inputBar.additionalContentHeight - attachmentButtonHeight) / 2 + val attachmentButtonHeight = binding.inputBar.attachmentButtonsContainerHeight + val bottomMargin = (newValue - binding.inputBar.additionalContentHeight - attachmentButtonHeight) / 2 val margin = toPx(8, resources) - val attachmentOptionsContainerLayoutParams = attachmentOptionsContainer.layoutParams as RelativeLayout.LayoutParams + val attachmentOptionsContainerLayoutParams = binding.attachmentOptionsContainer.layoutParams as RelativeLayout.LayoutParams attachmentOptionsContainerLayoutParams.bottomMargin = bottomMargin + attachmentButtonHeight + margin - attachmentOptionsContainer.layoutParams = attachmentOptionsContainerLayoutParams + binding.attachmentOptionsContainer.layoutParams = attachmentOptionsContainerLayoutParams // Scroll to bottom button - val scrollToBottomButtonLayoutParams = scrollToBottomButton.layoutParams as RelativeLayout.LayoutParams - scrollToBottomButtonLayoutParams.bottomMargin = newValue + additionalContentContainer.height + toPx(12, resources) - scrollToBottomButton.layoutParams = scrollToBottomButtonLayoutParams + val scrollToBottomButtonLayoutParams = binding.scrollToBottomButton.layoutParams as RelativeLayout.LayoutParams + scrollToBottomButtonLayoutParams.bottomMargin = newValue + binding.additionalContentContainer.height + toPx(12, resources) + binding.scrollToBottomButton.layoutParams = scrollToBottomButtonLayoutParams } override fun inputBarEditTextContentChanged(newContent: CharSequence) { - if (TextSecurePreferences.isLinkPreviewsEnabled(this)) { - linkPreviewViewModel?.onTextChanged(this, inputBar.text, 0, 0) + if (textSecurePreferences.isLinkPreviewsEnabled()) { + linkPreviewViewModel.onTextChanged(this, binding.inputBar.text, 0, 0) } showOrHideMentionCandidatesIfNeeded(newContent) if (LinkPreviewUtil.findWhitelistedUrls(newContent.toString()).isNotEmpty() - && !TextSecurePreferences.isLinkPreviewsEnabled(this) && !TextSecurePreferences.hasSeenLinkPreviewSuggestionDialog(this)) { + && !textSecurePreferences.isLinkPreviewsEnabled() && !textSecurePreferences.hasSeenLinkPreviewSuggestionDialog()) { LinkPreviewDialog { setUpLinkPreviewObserver() - linkPreviewViewModel?.onEnabled() - linkPreviewViewModel?.onTextChanged(this, inputBar.text, 0, 0) + linkPreviewViewModel.onEnabled() + linkPreviewViewModel.onTextChanged(this, binding.inputBar.text, 0, 0) }.show(supportFragmentManager, "Link Preview Dialog") - TextSecurePreferences.setHasSeenLinkPreviewSuggestionDialog(this) + textSecurePreferences.setHasSeenLinkPreviewSuggestionDialog() } } @@ -603,14 +643,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private fun showOrUpdateMentionCandidatesIfNeeded(query: String = "") { if (!isShowingMentionCandidatesView) { - additionalContentContainer.removeAllViews() + binding.additionalContentContainer.removeAllViews() val view = MentionCandidatesView(this) view.glide = glide view.onCandidateSelected = { handleMentionSelected(it) } - additionalContentContainer.addView(view) - val candidates = MentionsManager.getMentionCandidates(query, threadID, thread.isOpenGroupRecipient) + binding.additionalContentContainer.addView(view) + val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient) this.mentionCandidatesView = view - view.show(candidates, threadID) + view.show(candidates, viewModel.threadId) view.alpha = 0.0f val animation = ValueAnimator.ofObject(FloatEvaluator(), view.alpha, 1.0f) animation.duration = 250L @@ -619,7 +659,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } animation.start() } else { - val candidates = MentionsManager.getMentionCandidates(query, threadID, thread.isOpenGroupRecipient) + val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient) this.mentionCandidatesView!!.setMentionCandidates(candidates) } isShowingMentionCandidatesView = true @@ -632,7 +672,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe animation.duration = 250L animation.addUpdateListener { animator -> mentionCandidatesView.alpha = animator.animatedValue as Float - if (animator.animatedFraction == 1.0f) { additionalContentContainer.removeAllViews() } + if (animator.animatedFraction == 1.0f) { binding.additionalContentContainer.removeAllViews() } } animation.start() } @@ -641,7 +681,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun toggleAttachmentOptions() { val targetAlpha = if (isShowingAttachmentOptions) 0.0f else 1.0f - val allButtonContainers = listOf( cameraButtonContainer, libraryButtonContainer, documentButtonContainer, gifButtonContainer) + val allButtonContainers = listOf( binding.cameraButtonContainer, binding.libraryButtonContainer, binding.documentButtonContainer, binding.gifButtonContainer) val isReversed = isShowingAttachmentOptions // Run the animation in reverse val count = allButtonContainers.size allButtonContainers.indices.forEach { index -> @@ -660,39 +700,39 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun showVoiceMessageUI() { - inputBarRecordingView.show() - inputBar.alpha = 0.0f + binding.inputBarRecordingView.show() + binding.inputBar.alpha = 0.0f val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) animation.duration = 250L animation.addUpdateListener { animator -> - inputBar.alpha = animator.animatedValue as Float + binding.inputBar.alpha = animator.animatedValue as Float } animation.start() } private fun expandVoiceMessageLockView() { - val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.10f) + val animation = ValueAnimator.ofObject(FloatEvaluator(), binding.inputBarRecordingView.lockView.scaleX, 1.10f) animation.duration = 250L animation.addUpdateListener { animator -> - lockView.scaleX = animator.animatedValue as Float - lockView.scaleY = animator.animatedValue as Float + binding.inputBarRecordingView.lockView.scaleX = animator.animatedValue as Float + binding.inputBarRecordingView.lockView.scaleY = animator.animatedValue as Float } animation.start() } private fun collapseVoiceMessageLockView() { - val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.0f) + val animation = ValueAnimator.ofObject(FloatEvaluator(), binding.inputBarRecordingView.lockView.scaleX, 1.0f) animation.duration = 250L animation.addUpdateListener { animator -> - lockView.scaleX = animator.animatedValue as Float - lockView.scaleY = animator.animatedValue as Float + binding.inputBarRecordingView.lockView.scaleX = animator.animatedValue as Float + binding.inputBarRecordingView.lockView.scaleY = animator.animatedValue as Float } animation.start() } private fun hideVoiceMessageUI() { - val chevronImageView = inputBarRecordingView.inputBarChevronImageView - val slideToCancelTextView = inputBarRecordingView.inputBarSlideToCancelTextView + val chevronImageView = binding.inputBarRecordingView.chevronImageView + val slideToCancelTextView = binding.inputBarRecordingView.slideToCancelTextView listOf( chevronImageView, slideToCancelTextView ).forEach { view -> val animation = ValueAnimator.ofObject(FloatEvaluator(), view.translationX, 0.0f) animation.duration = 250L @@ -701,15 +741,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } animation.start() } - inputBarRecordingView.hide() + binding.inputBarRecordingView.hide() } override fun handleVoiceMessageUIHidden() { - inputBar.alpha = 1.0f + binding.inputBar.alpha = 1.0f val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) animation.duration = 250L animation.addUpdateListener { animator -> - inputBar.alpha = animator.animatedValue as Float + binding.inputBar.alpha = animator.animatedValue as Float } animation.start() } @@ -717,45 +757,45 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private fun handleRecyclerViewScrolled() { // FIXME: Checking isScrolledToBottom is a quick fix for an issue where the // typing indicator overlays the recycler view when scrolled up - val wasTypingIndicatorVisibleBefore = typingIndicatorViewContainer.isVisible - typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom - val isTypingIndicatorVisibleAfter = typingIndicatorViewContainer.isVisible + val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible + binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom + val isTypingIndicatorVisibleAfter = binding.typingIndicatorViewContainer.isVisible if (isTypingIndicatorVisibleAfter != wasTypingIndicatorVisibleBefore) { - inputBarHeightChanged(inputBar.height) + inputBarHeightChanged(binding.inputBar.height) } - scrollToBottomButton.isVisible = !isScrolledToBottom + binding.scrollToBottomButton.isVisible = !isScrolledToBottom unreadCount = min(unreadCount, layoutManager.findFirstVisibleItemPosition()) updateUnreadCountIndicator() } private fun updateUnreadCountIndicator() { val formattedUnreadCount = if (unreadCount < 100) unreadCount.toString() else "99+" - unreadCountTextView.text = formattedUnreadCount + binding.unreadCountTextView.text = formattedUnreadCount val textSize = if (unreadCount < 100) 12.0f else 9.0f - unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) - unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL) - unreadCountIndicator.isVisible = (unreadCount != 0) + binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) + binding.unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL) + binding.unreadCountIndicator.isVisible = (unreadCount != 0) } private fun updateSubtitle() { - muteIconImageView.isVisible = thread.isMuted - conversationSubtitleView.isVisible = true - if (thread.isMuted) { - if (thread.mutedUntil != Long.MAX_VALUE) { - conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(thread.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault())) + actionBarBinding.muteIconImageView.isVisible = viewModel.recipient.isMuted + actionBarBinding.conversationSubtitleView.isVisible = true + if (viewModel.recipient.isMuted) { + if (viewModel.recipient.mutedUntil != Long.MAX_VALUE) { + actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(viewModel.recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault())) } else { - conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever) + actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever) } - } else if (thread.isGroupRecipient) { - val openGroup = lokiThreadDb.getOpenGroupChat(threadID) + } else if (viewModel.recipient.isGroupRecipient) { + val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) if (openGroup != null) { val userCount = lokiApiDb.getUserCount(openGroup.room, openGroup.server) ?: 0 - conversationSubtitleView.text = getString(R.string.ConversationActivity_member_count, userCount) + actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_member_count, userCount) } else { - conversationSubtitleView.isVisible = false + actionBarBinding.conversationSubtitleView.isVisible = false } } else { - conversationSubtitleView.isVisible = false + actionBarBinding.conversationSubtitleView.isVisible = false } } // endregion @@ -765,7 +805,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe if (item.itemId == android.R.id.home) { return false } - return ConversationMenuHelper.onOptionItemSelected(this, item, thread) + return ConversationMenuHelper.onOptionItemSelected(this, item, viewModel.recipient) } // `position` is the adapter position; not the visual position @@ -773,7 +813,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val actionMode = this.actionMode if (actionMode != null) { adapter.toggleSelection(message, position) - val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) + val actionModeCallback = ConversationActionModeCallback(adapter, viewModel.threadId, this) actionModeCallback.delegate = this actionModeCallback.updateActionModeMenu(actionMode.menu) if (adapter.selectedItems.isEmpty()) { @@ -791,22 +831,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // `position` is the adapter position; not the visual position private fun handleSwipeToReply(message: MessageRecord, position: Int) { - inputBar.draftQuote(thread, message, glide) + binding.inputBar.draftQuote(viewModel.recipient, message, glide) } // `position` is the adapter position; not the visual position private fun handleLongPress(message: MessageRecord, position: Int) { val actionMode = this.actionMode - val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) + val actionModeCallback = ConversationActionModeCallback(adapter, viewModel.threadId, this) actionModeCallback.delegate = this searchViewItem?.collapseActionView() if (actionMode == null) { // Nothing should be selected if this is the case adapter.toggleSelection(message, position) - this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - startActionMode(actionModeCallback, ActionMode.TYPE_PRIMARY) - } else { - startActionMode(actionModeCallback) - } + this.actionMode = startActionMode(actionModeCallback, ActionMode.TYPE_PRIMARY) } else { adapter.toggleSelection(message, position) actionModeCallback.updateActionModeMenu(actionMode.menu) @@ -819,8 +855,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun onMicrophoneButtonMove(event: MotionEvent) { val rawX = event.rawX - val chevronImageView = inputBarRecordingView.inputBarChevronImageView - val slideToCancelTextView = inputBarRecordingView.inputBarSlideToCancelTextView + val chevronImageView = binding.inputBarRecordingView.chevronImageView + val slideToCancelTextView = binding.inputBarRecordingView.slideToCancelTextView if (rawX < screenWidth / 2) { val translationX = rawX - screenWidth / 2 val sign = -1.0f @@ -855,9 +891,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val x = event.rawX.roundToInt() val y = event.rawY.roundToInt() if (isValidLockViewLocation(x, y)) { - inputBarRecordingView.lock() + binding.inputBarRecordingView.lock() } else { - val recordButtonOverlay = inputBarRecordingView.recordButtonOverlay + val recordButtonOverlay = binding.inputBarRecordingView.recordButtonOverlay val location = IntArray(2) { 0 } recordButtonOverlay.getLocationOnScreen(location) val hitRect = Rect(location[0], location[1], location[0] + recordButtonOverlay.width, location[1] + recordButtonOverlay.height) @@ -873,24 +909,19 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // We can be anywhere above the lock view and a bit to the side of it (at most `lockViewHitMargin` // to the side) val lockViewLocation = IntArray(2) { 0 } - lockView.getLocationOnScreen(lockViewLocation) + binding.inputBarRecordingView.lockView.getLocationOnScreen(lockViewLocation) val hitRect = Rect(lockViewLocation[0] - lockViewHitMargin, 0, - lockViewLocation[0] + lockView.width + lockViewHitMargin, lockViewLocation[1] + lockView.height) + lockViewLocation[0] + binding.inputBarRecordingView.lockView.width + lockViewHitMargin, lockViewLocation[1] + binding.inputBarRecordingView.lockView.height) return hitRect.contains(x, y) } - private fun unblock() { - if (!thread.isContactRecipient) { return } - recipientDb.setBlocked(thread, false) - } - private fun handleMentionSelected(mention: Mention) { if (currentMentionStartIndex == -1) { return } mentions.add(mention) - val previousText = inputBar.text + val previousText = binding.inputBar.text val newText = previousText.substring(0, currentMentionStartIndex) + "@" + mention.displayName + " " - inputBar.text = newText - inputBar.inputBarEditText.setSelection(newText.length) + binding.inputBar.text = newText + binding.inputBar.setSelection(newText.length) currentMentionStartIndex = -1 hideMentionCandidates() this.previousText = newText @@ -898,27 +929,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun scrollToMessageIfPossible(timestamp: Long) { val lastSeenItemPosition = adapter.getItemPositionForTimestamp(timestamp) ?: return - conversationRecyclerView.scrollToPosition(lastSeenItemPosition) + binding.conversationRecyclerView.scrollToPosition(lastSeenItemPosition) } override fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) { if (indexInAdapter < 0 || indexInAdapter >= adapter.itemCount) { return } - val viewHolder = conversationRecyclerView.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder - val nextVisibleMessageView = viewHolder?.view ?: return - nextVisibleMessageView.messageContentView.mainContainer.children.forEach { view -> - if (view is VoiceMessageView) { - return@forEach view.togglePlayback() - } - } + val viewHolder = binding.conversationRecyclerView.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder + viewHolder?.view?.playVoiceMessage() } override fun sendMessage() { - if (thread.isContactRecipient && thread.isBlocked) { - BlockedDialog(thread).show(supportFragmentManager, "Blocked Dialog") + if (viewModel.recipient.isContactRecipient && viewModel.recipient.isBlocked) { + BlockedDialog(viewModel.recipient).show(supportFragmentManager, "Blocked Dialog") return } - if (inputBar.linkPreview != null || inputBar.quote != null) { - sendAttachments(listOf(), getMessageBody(), inputBar.quote, inputBar.linkPreview) + if (binding.inputBar.linkPreview != null || binding.inputBar.quote != null) { + sendAttachments(listOf(), getMessageBody(), binding.inputBar.quote, binding.inputBar.linkPreview) } else { sendTextOnlyMessage() } @@ -926,13 +952,13 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun commitInputContent(contentUri: Uri) { val media = Media(contentUri, MediaUtil.getMimeType(this, contentUri)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent()) - startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), thread, getMessageBody()), ConversationActivityV2.PICK_FROM_LIBRARY) + startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient, getMessageBody()), PICK_FROM_LIBRARY) } private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false) { val text = getMessageBody() - val userPublicKey = TextSecurePreferences.getLocalNumber(this) - val isNoteToSelf = (thread.isContactRecipient && thread.address.toString() == userPublicKey) + val userPublicKey = textSecurePreferences.getLocalNumber() + val isNoteToSelf = (viewModel.recipient.isContactRecipient && viewModel.recipient.address.toString() == userPublicKey) if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) { val dialog = SendSeedDialog { sendTextOnlyMessage(true) } return dialog.show(supportFragmentManager, "Send Seed Dialog") @@ -941,21 +967,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val message = VisibleMessage() message.sentTimestamp = System.currentTimeMillis() message.text = text - val outgoingTextMessage = OutgoingTextMessage.from(message, thread) + val outgoingTextMessage = OutgoingTextMessage.from(message, viewModel.recipient) // Clear the input bar - inputBar.text = "" - inputBar.cancelQuoteDraft() - inputBar.cancelLinkPreviewDraft() + binding.inputBar.text = "" + binding.inputBar.cancelQuoteDraft() + binding.inputBar.cancelLinkPreviewDraft() // Clear mentions previousText = "" currentMentionStartIndex = -1 mentions.clear() // Put the message in the database - message.id = smsDb.insertMessageOutbox(threadID, outgoingTextMessage, false, message.sentTimestamp!!) { } + message.id = smsDb.insertMessageOutbox(viewModel.threadId, outgoingTextMessage, false, message.sentTimestamp!!) { } // Send it - MessageSender.send(message, thread.address) + MessageSender.send(message, viewModel.recipient.address) // Send a typing stopped message - ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID) + ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId) } private fun sendAttachments(attachments: List, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) { @@ -965,14 +991,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe message.text = body val quote = quotedMessage?.let { val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf() - val sender = if (it.isOutgoing) fromSerialized(TextSecurePreferences.getLocalNumber(this)!!) else it.individualRecipient.address + val sender = if (it.isOutgoing) fromSerialized(textSecurePreferences.getLocalNumber()!!) else it.individualRecipient.address QuoteModel(it.dateSent, sender, it.body, false, quotedAttachments) } - val outgoingTextMessage = OutgoingMediaMessage.from(message, thread, attachments, quote, linkPreview) + val outgoingTextMessage = OutgoingMediaMessage.from(message, viewModel.recipient, attachments, quote, linkPreview) // Clear the input bar - inputBar.text = "" - inputBar.cancelQuoteDraft() - inputBar.cancelLinkPreviewDraft() + binding.inputBar.text = "" + binding.inputBar.cancelQuoteDraft() + binding.inputBar.cancelLinkPreviewDraft() // Clear mentions previousText = "" currentMentionStartIndex = -1 @@ -982,43 +1008,43 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // Reset attachments button if needed if (isShowingAttachmentOptions) { toggleAttachmentOptions() } // Put the message in the database - message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, threadID, false) { } + message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, viewModel.threadId, false) { } // Send it - MessageSender.send(message, thread.address, attachments, quote, linkPreview) + MessageSender.send(message, viewModel.recipient.address, attachments, quote, linkPreview) // Send a typing stopped message - ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID) + ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId) } private fun showGIFPicker() { - val hasSeenGIFMetaDataWarning: Boolean = TextSecurePreferences.hasSeenGIFMetaDataWarning(this) + val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning() if (!hasSeenGIFMetaDataWarning) { val builder = AlertDialog.Builder(this) builder.setTitle("Search GIFs?") builder.setMessage("You will not have full metadata protection when sending GIFs.") - builder.setPositiveButton("OK") { dialog: DialogInterface, which: Int -> - TextSecurePreferences.setHasSeenGIFMetaDataWarning(this) - AttachmentManager.selectGif(this, ConversationActivityV2.PICK_GIF) + builder.setPositiveButton("OK") { dialog: DialogInterface, _: Int -> + textSecurePreferences.setHasSeenGIFMetaDataWarning() + AttachmentManager.selectGif(this, PICK_GIF) dialog.dismiss() } builder.setNegativeButton( "Cancel" - ) { dialog: DialogInterface, which: Int -> dialog.dismiss() } + ) { dialog: DialogInterface, _: Int -> dialog.dismiss() } builder.create().show() } else { - AttachmentManager.selectGif(this, ConversationActivityV2.PICK_GIF) + AttachmentManager.selectGif(this, PICK_GIF) } } private fun showDocumentPicker() { - AttachmentManager.selectDocument(this, ConversationActivityV2.PICK_DOCUMENT) + AttachmentManager.selectDocument(this, PICK_DOCUMENT) } private fun pickFromLibrary() { - AttachmentManager.selectGallery(this, ConversationActivityV2.PICK_FROM_LIBRARY, thread, inputBar.text.trim()) + AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, viewModel.recipient, binding.inputBar.text.trim()) } private fun showCamera() { - attachmentManager.capturePhoto(this, ConversationActivityV2.TAKE_PHOTO, thread); + attachmentManager.capturePhoto(this, TAKE_PHOTO, viewModel.recipient); } override fun onAttachmentChanged() { @@ -1080,23 +1106,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe sendAttachments(slideDeck.asAttachments(), body) } INVITE_CONTACTS -> { - if (!thread.isOpenGroupRecipient) { return } + if (!viewModel.recipient.isOpenGroupRecipient) { return } val extras = intent?.extras ?: return - if (!intent.hasExtra(SelectContactsActivity.selectedContactsKey)) { return } + if (!intent.hasExtra(selectedContactsKey)) { return } val selectedContacts = extras.getStringArray(selectedContactsKey)!! - val openGroup = lokiThreadDb.getOpenGroupChat(threadID) - for (contact in selectedContacts) { - val recipient = Recipient.from(this, fromSerialized(contact), true) - val message = VisibleMessage() - message.sentTimestamp = System.currentTimeMillis() - val openGroupInvitation = OpenGroupInvitation() - openGroupInvitation.name = openGroup!!.name - openGroupInvitation.url = openGroup!!.joinURL - message.openGroupInvitation = openGroupInvitation - val outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation(openGroupInvitation, recipient, message.sentTimestamp) - smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!) - MessageSender.send(message, recipient.address) + val recipients = selectedContacts.map { contact -> + Recipient.from(this, fromSerialized(contact), true) } + viewModel.inviteContacts(recipients) } } } @@ -1151,91 +1168,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) } - private fun buildUnsendRequest(message: MessageRecord): UnsendRequest? { - if (this.thread.isOpenGroupRecipient) return null - val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider - messageDataProvider.getServerHashForMessage(message.id) ?: return null - val unsendRequest = UnsendRequest() - if (message.isOutgoing) { - unsendRequest.author = TextSecurePreferences.getLocalNumber(this) - } else { - unsendRequest.author = message.individualRecipient.address.contactIdentifier() - } - unsendRequest.timestamp = message.timestamp - - return unsendRequest - } - - private fun deleteLocally(message: MessageRecord) { - buildUnsendRequest(message)?.let { unsendRequest -> - TextSecurePreferences.getLocalNumber(this@ConversationActivityV2)?.let { - MessageSender.send(unsendRequest, Address.fromSerialized(it)) - } - } - MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(message.id, !message.isMms) - } - - private fun deleteForEveryone(message: MessageRecord) { - buildUnsendRequest(message)?.let { unsendRequest -> - MessageSender.send(unsendRequest, thread.address) - } - val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider - val openGroup = lokiThreadDb.getOpenGroupChat(threadID) - if (openGroup != null) { - lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID -> - OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server) - .success { - messageDataProvider.deleteMessage(message.id, !message.isMms) - }.failUi { error -> - Toast.makeText(this@ConversationActivityV2, "Couldn't delete message due to error: $error", Toast.LENGTH_LONG).show() - } - } - } else { - messageDataProvider.deleteMessage(message.id, !message.isMms) - messageDataProvider.getServerHashForMessage(message.id)?.let { serverHash -> - var publicKey = thread.address.serialize() - if (thread.isClosedGroupRecipient) { publicKey = GroupUtil.doubleDecodeGroupID(publicKey).toHexString() } - SnodeAPI.deleteMessage(publicKey, listOf(serverHash)) - .failUi { error -> - Toast.makeText(this@ConversationActivityV2, "Couldn't delete message due to error: $error", Toast.LENGTH_LONG).show() - } - } - } - } - // Remove this after the unsend request is enabled fun deleteMessagesWithoutUnsendRequest(messages: Set) { val messageCount = messages.size - val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val builder = AlertDialog.Builder(this) builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) builder.setCancelable(true) - val openGroup = lokiThreadDb.getOpenGroupChat(threadID) builder.setPositiveButton(R.string.delete) { _, _ -> - if (openGroup != null) { - val messageServerIDs = mutableMapOf() - for (message in messages) { - val messageServerID = lokiMessageDb.getServerID(message.id, !message.isMms) ?: continue - messageServerIDs[messageServerID] = message - } - for ((messageServerID, message) in messageServerIDs) { - OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server) - .success { - messageDataProvider.deleteMessage(message.id, !message.isMms) - }.failUi { error -> - Toast.makeText(this@ConversationActivityV2, "Couldn't delete message due to error: $error", Toast.LENGTH_LONG).show() - } - } - } else { - for (message in messages) { - if (message.isMms) { - mmsDb.deleteMessage(message.id) - } else { - smsDb.deleteMessage(message.id) - } - } - } + viewModel.deleteMessagesWithoutUnsendRequest(messages) endActionMode() } builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> @@ -1252,7 +1193,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } val allSentByCurrentUser = messages.all { it.isOutgoing } val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id) != null } - if (thread.isOpenGroupRecipient) { + if (viewModel.recipient.isOpenGroupRecipient) { val messageCount = messages.size val builder = AlertDialog.Builder(this) builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) @@ -1260,7 +1201,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe builder.setCancelable(true) builder.setPositiveButton(R.string.delete) { _, _ -> for (message in messages) { - this.deleteForEveryone(message) + viewModel.deleteForEveryone(message) } endActionMode() } @@ -1271,17 +1212,17 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe builder.show() } else if (allSentByCurrentUser && allHasHash) { val bottomSheet = DeleteOptionsBottomSheet() - bottomSheet.recipient = thread + bottomSheet.recipient = viewModel.recipient bottomSheet.onDeleteForMeTapped = { for (message in messages) { - this.deleteLocally(message) + viewModel.deleteLocally(message) } bottomSheet.dismiss() endActionMode() } bottomSheet.onDeleteForEveryoneTapped = { for (message in messages) { - this.deleteForEveryone(message) + viewModel.deleteForEveryone(message) } bottomSheet.dismiss() endActionMode() @@ -1299,7 +1240,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe builder.setCancelable(true) builder.setPositiveButton(R.string.delete) { _, _ -> for (message in messages) { - this.deleteLocally(message) + viewModel.deleteLocally(message) } endActionMode() } @@ -1313,17 +1254,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun banUser(messages: Set) { val builder = AlertDialog.Builder(this) - val sessionID = messages.first().individualRecipient.address.toString() builder.setTitle(R.string.ConversationFragment_ban_selected_user) builder.setMessage("This will ban the selected user from this room. It won't ban them from other rooms.") builder.setCancelable(true) - val openGroup = lokiThreadDb.getOpenGroupChat(threadID)!! builder.setPositiveButton(R.string.ban) { _, _ -> - OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server).successUi { - Toast.makeText(this@ConversationActivityV2, "Successfully banned user", Toast.LENGTH_LONG).show() - }.failUi { error -> - Toast.makeText(this@ConversationActivityV2, "Couldn't ban user due to error: $error", Toast.LENGTH_LONG).show() - } + viewModel.banUser(messages.first().individualRecipient) endActionMode() } builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> @@ -1335,17 +1270,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun banAndDeleteAll(messages: Set) { val builder = AlertDialog.Builder(this) - val sessionID = messages.first().individualRecipient.address.toString() builder.setTitle(R.string.ConversationFragment_ban_selected_user) builder.setMessage("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.") builder.setCancelable(true) - val openGroup = lokiThreadDb.getOpenGroupChat(threadID)!! builder.setPositiveButton(R.string.ban) { _, _ -> - OpenGroupAPIV2.banAndDeleteAll(sessionID, openGroup.room, openGroup.server).successUi { - Toast.makeText(this@ConversationActivityV2, "Successfully banned user and deleted all their messages", Toast.LENGTH_LONG).show() - }.failUi { error -> - Toast.makeText(this@ConversationActivityV2, "Couldn't execute request due to error: $error", Toast.LENGTH_LONG).show() - } + viewModel.banAndDeleteAll(messages.first().individualRecipient) endActionMode() } builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> @@ -1362,7 +1291,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val messageIterator = sortedMessages.iterator() while (messageIterator.hasNext()) { val message = messageIterator.next() - val body = MentionUtilities.highlightMentions(message.body, threadID, this) + val body = MentionUtilities.highlightMentions(message.body, viewModel.threadId, this) if (TextUtils.isEmpty(body)) { continue } if (messageSize > 1) { val formattedTimestamp = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), message.timestamp) @@ -1442,16 +1371,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun reply(messages: Set) { - inputBar.draftQuote(thread, messages.first(), glide) + binding.inputBar.draftQuote(viewModel.recipient, messages.first(), glide) endActionMode() } private fun sendMediaSavedNotification() { - if (thread.isGroupRecipient) { return } + if (viewModel.recipient.isGroupRecipient) { return } val timestamp = System.currentTimeMillis() val kind = DataExtractionNotification.Kind.MediaSaved(timestamp) val message = DataExtractionNotification(kind) - MessageSender.send(message, thread.address) + MessageSender.send(message, viewModel.recipient.address) } private fun endActionMode() { @@ -1462,7 +1391,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // region General private fun getMessageBody(): String { - var result = inputBar.inputBarEditText.text?.trim() ?: "" + var result = binding.inputBar.text.trim() for (mention in mentions) { try { val startIndex = result.indexOf("@" + mention.displayName) @@ -1472,22 +1401,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe Log.d("Loki", "Failed to process mention due to error: $exception") } } - return result.toString() - } - - private fun saveDraft() { - val text = inputBar?.text?.trim() ?: return - if (text.isEmpty()) { return } - val drafts = Drafts() - drafts.add(DraftDatabase.Draft(DraftDatabase.Draft.TEXT, text)) - draftDb.insertDrafts(threadID, drafts) + return result } // endregion // region Search private fun setUpSearchResultObserver() { - val searchViewModel = ViewModelProvider(this).get(SearchViewModel::class.java) - this.searchViewModel = searchViewModel searchViewModel.searchResults.observe(this, Observer { result: SearchViewModel.SearchResult? -> if (result == null) return@Observer if (result.getResults().isNotEmpty()) { @@ -1495,31 +1414,48 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs, Runnable { searchViewModel.onMissingResult() }) } } - this.searchBottomBar.setData(result.position, result.getResults().size) + binding.searchBottomBar.setData(result.position, result.getResults().size) }) } - fun onSearchQueryUpdated(query: String?) { + fun onSearchOpened() { + searchViewModel.onSearchOpened() + binding.searchBottomBar.visibility = View.VISIBLE + binding.searchBottomBar.setData(0, 0) + binding.inputBar.visibility = View.GONE + } + + fun onSearchClosed() { + searchViewModel.onSearchClosed() + binding.searchBottomBar.visibility = View.GONE + binding.inputBar.visibility = View.VISIBLE + adapter.onSearchQueryUpdated(null) + invalidateOptionsMenu() + } + + fun onSearchQueryUpdated(query: String) { + searchViewModel.onQueryUpdated(query, viewModel.threadId) + binding.searchBottomBar.showLoading() adapter.onSearchQueryUpdated(query) } override fun onSearchMoveUpPressed() { - this.searchViewModel?.onMoveUp() + this.searchViewModel.onMoveUp() } override fun onSearchMoveDownPressed() { - this.searchViewModel?.onMoveDown() + this.searchViewModel.onMoveDown() } private fun jumpToMessage(author: Address, timestamp: Long, onMessageNotFound: Runnable?) { SimpleTask.run(lifecycle, { - mmsSmsDb.getMessagePositionInConversation(threadID, timestamp, author) + mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, timestamp, author) }) { p: Int -> moveToMessagePosition(p, onMessageNotFound) } } private fun moveToMessagePosition(position: Int, onMessageNotFound: Runnable?) { if (position >= 0) { - conversationRecyclerView.scrollToPosition(position) + binding.conversationRecyclerView.scrollToPosition(position) } else { onMessageNotFound?.run() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index c033009ad..b3558ffe2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -4,9 +4,7 @@ import android.content.Context import android.database.Cursor import android.view.MotionEvent import android.view.ViewGroup -import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView.ViewHolder -import kotlinx.android.synthetic.main.view_visible_message.view.* import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView @@ -49,15 +47,9 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @Suppress("NAME_SHADOWING") val viewType = ViewType.allValues[viewType] - when (viewType) { - ViewType.Visible -> { - val view = VisibleMessageView(context) - return VisibleMessageViewHolder(view) - } - ViewType.Control -> { - val view = ControlMessageView(context) - return ControlMessageViewHolder(view) - } + return when (viewType) { + ViewType.Visible -> VisibleMessageViewHolder(VisibleMessageView(context)) + ViewType.Control -> ControlMessageViewHolder(ControlMessageView(context)) else -> throw IllegalStateException("Unexpected view type: $viewType.") } } @@ -71,7 +63,6 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr val view = viewHolder.view val isSelected = selectedItems.contains(message) view.snIsSelected = isSelected - view.messageTimestampTextView.isVisible = isSelected view.indexInAdapter = position view.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery) if (!message.isDeleted) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt index 475efaba3..f0e8db7e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt @@ -2,16 +2,12 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Context import android.util.AttributeSet -import android.util.Log import android.view.MotionEvent import android.view.VelocityTracker -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.activity_conversation_v2.* import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.toPx import kotlin.math.abs -import kotlin.math.max class ConversationRecyclerView : RecyclerView { private val maxLongPressVelocityY = toPx(10, resources) @@ -37,10 +33,10 @@ class ConversationRecyclerView : RecyclerView { if (abs(vy) > maxLongPressVelocityY && abs(vx) < minSwipeVelocityX) { return super.onInterceptTouchEvent(e) } // Return false if abs(v.x) > abs(v.y) so that only swipes that are more horizontal than vertical // get passed on to the message view - if (abs(vx) > abs(vy)) { - return false + return if (abs(vx) > abs(vy)) { + false } else { - return super.onInterceptTouchEvent(e) + super.onInterceptTouchEvent(e) } } 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 new file mode 100644 index 000000000..852d0a998 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -0,0 +1,130 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.repository.ConversationRepository +import java.util.UUID + +class ConversationViewModel( + val threadId: Long, + private val repository: ConversationRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(ConversationUiState()) + val uiState: StateFlow = _uiState + + val recipient: Recipient by lazy { + repository.getRecipientForThreadId(threadId) + } + + init { + _uiState.update { + it.copy(isOxenHostedOpenGroup = repository.isOxenHostedOpenGroup(threadId)) + } + } + + fun saveDraft(text: String) { + repository.saveDraft(threadId, text) + } + + fun getDraft(): String? { + return repository.getDraft(threadId) + } + + fun inviteContacts(contacts: List) { + repository.inviteContacts(threadId, contacts) + } + + fun unblock() { + if (recipient.isContactRecipient) { + repository.unblock(recipient) + } + } + + fun deleteLocally(message: MessageRecord) { + repository.deleteLocally(recipient, message) + } + + fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch { + repository.deleteForEveryone(threadId, recipient, message) + .onFailure { + showMessage("Couldn't delete message due to error: $it") + } + } + + fun deleteMessagesWithoutUnsendRequest(messages: Set) = viewModelScope.launch { + repository.deleteMessageWithoutUnsendRequest(threadId, messages) + .onFailure { + showMessage("Couldn't delete message due to error: $it") + } + } + + fun banUser(recipient: Recipient) = viewModelScope.launch { + repository.banUser(threadId, recipient) + .onSuccess { + showMessage("Successfully banned user") + } + .onFailure { + showMessage("Couldn't ban user due to error: $it") + } + } + + fun banAndDeleteAll(recipient: Recipient) = viewModelScope.launch { + repository.banAndDeleteAll(threadId, recipient) + .onSuccess { + showMessage("Successfully banned user and deleted all their messages") + } + .onFailure { + showMessage("Couldn't execute request due to error: $it") + } + } + + private fun showMessage(message: String) { + _uiState.update { currentUiState -> + val messages = currentUiState.uiMessages + UiMessage( + id = UUID.randomUUID().mostSignificantBits, + message = message + ) + currentUiState.copy(uiMessages = messages) + } + } + + fun messageShown(messageId: Long) { + _uiState.update { currentUiState -> + val messages = currentUiState.uiMessages.filterNot { it.id == messageId } + currentUiState.copy(uiMessages = messages) + } + } + + @dagger.assisted.AssistedFactory + interface AssistedFactory { + fun create(threadId: Long): Factory + } + + @Suppress("UNCHECKED_CAST") + class Factory @AssistedInject constructor( + @Assisted private val threadId: Long, + private val repository: ConversationRepository + ) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return ConversationViewModel(threadId, repository) as T + } + } +} + +data class UiMessage(val id: Long, val message: String) + +data class ConversationUiState( + val isOxenHostedOpenGroup: Boolean = false, + val uiMessages: List = emptyList() +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt index 6272a9c22..66f33cf29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt @@ -7,8 +7,8 @@ import android.view.ViewGroup import androidx.core.view.isVisible import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint -import kotlinx.android.synthetic.main.fragment_delete_message_bottom_sheet.* import network.loki.messenger.R +import network.loki.messenger.databinding.FragmentDeleteMessageBottomSheetBinding import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.database.SessionContactDatabase @@ -22,6 +22,7 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen lateinit var contactDatabase: SessionContactDatabase lateinit var recipient: Recipient + private lateinit var binding: FragmentDeleteMessageBottomSheetBinding val contact by lazy { val senderId = recipient.address.serialize() // this dialog won't show for open group contacts @@ -37,15 +38,16 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.fragment_delete_message_bottom_sheet, container, false) + ): View { + binding = FragmentDeleteMessageBottomSheetBinding.inflate(inflater, container, false) + return binding.root } override fun onClick(v: View?) { when (v) { - deleteForMeTextView -> onDeleteForMeTapped?.invoke() - deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke() - cancelTextView -> onCancelTapped?.invoke() + binding.deleteForMeTextView -> onDeleteForMeTapped?.invoke() + binding.deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke() + binding.cancelTextView -> onCancelTapped?.invoke() } } @@ -55,13 +57,13 @@ class DeleteOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListen return dismiss() } if (!recipient.isGroupRecipient && !contact.isNullOrEmpty()) { - deleteForEveryoneTextView.text = + binding.deleteForEveryoneTextView.text = resources.getString(R.string.delete_message_for_me_and_recipient, contact) } - deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient - deleteForMeTextView.setOnClickListener(this) - deleteForEveryoneTextView.setOnClickListener(this) - cancelTextView.setOnClickListener(this) + binding.deleteForEveryoneTextView.isVisible = !recipient.isClosedGroupRecipient + binding.deleteForMeTextView.setOnClickListener(this) + binding.deleteForEveryoneTextView.setOnClickListener(this) + binding.cancelTextView.setOnClickListener(this) } override fun onStart() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 7899a2e47..72757ce25 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.conversation.v2 import android.os.Bundle import android.view.View -import kotlinx.android.synthetic.main.activity_message_detail.* import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityMessageDetailBinding import org.session.libsession.utilities.Address import org.session.libsession.utilities.ExpirationUtil import org.session.libsession.utilities.TextSecurePreferences @@ -13,11 +13,11 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.DateUtils import java.text.SimpleDateFormat -import java.util.* - +import java.util.Date +import java.util.Locale class MessageDetailActivity: PassphraseRequiredActionBarActivity() { - + private lateinit var binding: ActivityMessageDetailBinding var messageRecord: MessageRecord? = null // region Settings @@ -29,7 +29,8 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { super.onCreate(savedInstanceState, ready) - setContentView(R.layout.activity_message_detail) + binding = ActivityMessageDetailBinding.inflate(layoutInflater) + setContentView(binding.root) title = resources.getString(R.string.conversation_context__menu_message_details) val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L) // We only show this screen for messages fail to send, @@ -37,7 +38,7 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!) messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) updateContent() - resend_button.setOnClickListener { + binding.resendButton.setOnClickListener { ResendMessageUtilities.resend(messageRecord!!) finish() } @@ -46,20 +47,20 @@ class MessageDetailActivity: PassphraseRequiredActionBarActivity() { fun updateContent() { val dateLocale = Locale.getDefault() val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale) - sent_time.text = dateFormatter.format(Date(messageRecord!!.dateSent)) + binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent)) val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId()) ?: "Message failed to send." - error_message.text = errorMessage + binding.errorMessage.text = errorMessage - if (messageRecord!!.getExpiresIn() <= 0 || messageRecord!!.getExpireStarted() <= 0) { - expires_container.visibility = View.GONE + if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) { + binding.expiresContainer.visibility = View.GONE } else { - expires_container.visibility = View.VISIBLE + binding.expiresContainer.visibility = View.VISIBLE val elapsed = System.currentTimeMillis() - messageRecord!!.expireStarted val remaining = messageRecord!!.expiresIn - elapsed val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1)) - expires_in.text = duration + binding.expiresIn.text = duration } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt index 859f208a5..28c86b331 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt @@ -15,14 +15,16 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import kotlinx.android.synthetic.main.fragment_modal_url_bottom_sheet.* import network.loki.messenger.R +import network.loki.messenger.databinding.FragmentModalUrlBottomSheetBinding import org.thoughtcrime.securesms.util.UiModeUtilities class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener { - - override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_modal_url_bottom_sheet, container, false) + private lateinit var binding: FragmentModalUrlBottomSheetBinding + + override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View { + binding = FragmentModalUrlBottomSheetBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -31,10 +33,10 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), val spannable = SpannableStringBuilder(explanation) val startIndex = explanation.indexOf(url) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - openURLExplanationTextView.text = spannable - cancelButton.setOnClickListener(this) - copyButton.setOnClickListener(this) - openURLButton.setOnClickListener(this) + binding.openURLExplanationTextView.text = spannable + binding.cancelButton.setOnClickListener(this) + binding.copyButton.setOnClickListener(this) + binding.openURLButton.setOnClickListener(this) } private fun open() { @@ -64,9 +66,9 @@ class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), override fun onClick(v: View?) { when (v) { - openURLButton -> open() - copyButton -> copy() - cancelButton -> dismiss() + binding.openURLButton -> open() + binding.copyButton -> copy() + binding.cancelButton -> dismiss() } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt index b1283e1a2..abf2eb4b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt @@ -11,8 +11,8 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.core.view.children import androidx.core.view.isVisible -import kotlinx.android.synthetic.main.album_thumbnail_view.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.AlbumThumbnailViewBinding import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress @@ -32,6 +32,8 @@ import org.thoughtcrime.securesms.util.ActivityDispatcher import kotlin.math.roundToInt class AlbumThumbnailView : FrameLayout { + + private lateinit var binding: AlbumThumbnailViewBinding companion object { const val MAX_ALBUM_DISPLAY_SIZE = 5 @@ -55,7 +57,7 @@ class AlbumThumbnailView : FrameLayout { private var slideSize: Int = 0 private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this) + binding = AlbumThumbnailViewBinding.inflate(LayoutInflater.from(context), this, true) } override fun dispatchDraw(canvas: Canvas?) { @@ -73,7 +75,7 @@ class AlbumThumbnailView : FrameLayout { // Z-check in specific order val testRect = Rect() // test "Read More" - albumCellBodyTextReadMore.getGlobalVisibleRect(testRect) + binding.albumCellBodyTextReadMore.getGlobalVisibleRect(testRect) if (testRect.contains(eventRect)) { // dispatch to activity view ActivityDispatcher.get(context)?.dispatchIntent { context -> @@ -81,15 +83,15 @@ class AlbumThumbnailView : FrameLayout { } return } - val intersectedSpans = albumCellBodyText.getIntersectedModalSpans(eventRect) + val intersectedSpans = binding.albumCellBodyText.getIntersectedModalSpans(eventRect) if (intersectedSpans.isNotEmpty()) { intersectedSpans.forEach { span -> - span.onClick(albumCellBodyText) + span.onClick(binding.albumCellBodyText) } return } // test each album child - albumCellContainer.findViewById(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child -> + binding.albumCellContainer.findViewById(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child -> child.getGlobalVisibleRect(testRect) if (testRect.contains(eventRect)) { // hit intersects with this particular child @@ -122,10 +124,10 @@ class AlbumThumbnailView : FrameLayout { // recreate cell views if different size to what we have already (for recycling) if (slides.size != this.slideSize) { - albumCellContainer.removeAllViews() - LayoutInflater.from(context).inflate(layoutRes(slides.size), albumCellContainer) + binding.albumCellContainer.removeAllViews() + LayoutInflater.from(context).inflate(layoutRes(slides.size), binding.albumCellContainer) val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE - albumCellContainer.findViewById(R.id.album_cell_overflow_text)?.let { overflowText -> + binding.albumCellContainer.findViewById(R.id.album_cell_overflow_text)?.let { overflowText -> // overflowText will be null if !overflowed overflowText.isVisible = overflowed // more than max album size overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE) @@ -137,17 +139,17 @@ class AlbumThumbnailView : FrameLayout { val thumbnailView = getThumbnailView(position) thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message) } - albumCellBodyParent.isVisible = message.body.isNotEmpty() + binding.albumCellBodyParent.isVisible = message.body.isNotEmpty() val body = VisibleMessageContentView.getBodySpans(context, message, null) - albumCellBodyText.text = body + binding.albumCellBodyText.text = body post { // post to await layout of text - albumCellBodyText.layout?.let { layout -> + binding.albumCellBodyText.layout?.let { layout -> val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) } ?: 0 // show read more text if at least one line is ellipsized - ViewUtil.setPaddingTop(albumCellBodyTextParent, if (maxEllipsis > 0) resources.getDimension(R.dimen.small_spacing).roundToInt() else resources.getDimension(R.dimen.medium_spacing).roundToInt()) - albumCellBodyTextReadMore.isVisible = maxEllipsis > 0 + ViewUtil.setPaddingTop(binding.albumCellBodyTextParent, if (maxEllipsis > 0) resources.getDimension(R.dimen.small_spacing).roundToInt() else resources.getDimension(R.dimen.medium_spacing).roundToInt()) + binding.albumCellBodyTextReadMore.isVisible = maxEllipsis > 0 } } } @@ -165,11 +167,11 @@ class AlbumThumbnailView : FrameLayout { } fun getThumbnailView(position: Int): KThumbnailView = when (position) { - 0 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_1) - 1 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_2) - 2 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_3) - 3 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_4) - 4 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_5) + 0 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_1) + 1 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_2) + 2 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_3) + 3 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_4) + 4 -> binding.albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_5) else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt index b056c4f9a..c1fce3f50 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt @@ -5,14 +5,14 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout import androidx.core.view.isVisible -import kotlinx.android.synthetic.main.view_link_preview_draft.view.* -import network.loki.messenger.R +import network.loki.messenger.databinding.ViewLinkPreviewDraftBinding import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview -import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.ImageSlide +import org.thoughtcrime.securesms.util.toPx class LinkPreviewDraftView : LinearLayout { + private lateinit var binding: ViewLinkPreviewDraftBinding var delegate: LinkPreviewDraftViewDelegate? = null constructor(context: Context) : super(context) { initialize() } @@ -21,22 +21,22 @@ class LinkPreviewDraftView : LinearLayout { private fun initialize() { // Start out with the loader showing and the content view hidden - LayoutInflater.from(context).inflate(R.layout.view_link_preview_draft, this) - linkPreviewDraftContainer.isVisible = false - thumbnailImageView.clipToOutline = true - linkPreviewDraftCancelButton.setOnClickListener { cancel() } + binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true) + binding.linkPreviewDraftContainer.isVisible = false + binding.thumbnailImageView.clipToOutline = true + binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() } } fun update(glide: GlideRequests, linkPreview: LinkPreview) { // Hide the loader and show the content view - linkPreviewDraftContainer.isVisible = true - linkPreviewDraftLoader.isVisible = false - thumbnailImageView.radius = toPx(4, resources) + binding.linkPreviewDraftContainer.isVisible = true + binding.linkPreviewDraftLoader.isVisible = false + binding.thumbnailImageView.radius = toPx(4, resources) if (linkPreview.getThumbnail().isPresent) { // This internally fetches the thumbnail - thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false) + binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false) } - linkPreviewDraftTitleTextView.text = linkPreview.title + binding.linkPreviewDraftTitleTextView.text = linkPreview.title } private fun cancel() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt index ee1e40846..303c17c5a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt @@ -45,7 +45,7 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS } override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View { - val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent) + val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context) val mentionCandidate = getItem(position) cell.glide = glide cell.mentionCandidate = mentionCandidate diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt index 7c7d8b624..5486cf0e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt @@ -4,32 +4,29 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.LinearLayout -import kotlinx.android.synthetic.main.view_mention_candidate.view.* -import network.loki.messenger.R +import network.loki.messenger.databinding.ViewMentionCandidateBinding import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.thoughtcrime.securesms.mms.GlideRequests -class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { +class MentionCandidateView : LinearLayout { + private lateinit var binding: ViewMentionCandidateBinding var mentionCandidate = Mention("", "") set(newValue) { field = newValue; update() } var glide: GlideRequests? = null var openGroupServer: String? = null var openGroupRoom: String? = null - constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context) : this(context, null) + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } - companion object { - - fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView { - return layoutInflater.inflate(R.layout.view_mention_candidate, parent, false) as MentionCandidateView - } + private fun initialize() { + binding = ViewMentionCandidateBinding.inflate(LayoutInflater.from(context), this, true) } - private fun update() { + private fun update() = with(binding) { mentionCandidateNameTextView.text = mentionCandidate.displayName profilePictureView.publicKey = mentionCandidate.publicKey profilePictureView.displayName = mentionCandidate.displayName diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/OpenGroupGuidelinesView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/OpenGroupGuidelinesView.kt index d6cffd08d..c07154283 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/OpenGroupGuidelinesView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/OpenGroupGuidelinesView.kt @@ -5,8 +5,7 @@ import android.content.Intent import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout -import kotlinx.android.synthetic.main.view_open_group_guidelines.view.* -import network.loki.messenger.R +import network.loki.messenger.databinding.ViewOpenGroupGuidelinesBinding import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.groups.OpenGroupGuidelinesActivity import org.thoughtcrime.securesms.util.push @@ -18,13 +17,12 @@ class OpenGroupGuidelinesView : FrameLayout { constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - val contentView = inflater.inflate(R.layout.view_open_group_guidelines, null) - addView(contentView) - readButton.setOnClickListener { - val activity = context as ConversationActivityV2 - val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java) - activity.push(intent) + ViewOpenGroupGuidelinesBinding.inflate(LayoutInflater.from(context), this, true).apply { + readButton.setOnClickListener { + val activity = context as ConversationActivityV2 + val intent = Intent(activity, OpenGroupGuidelinesActivity::class.java) + activity.push(intent) + } } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt index 0628b63b7..768d49146 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt @@ -4,22 +4,22 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout -import kotlinx.android.synthetic.main.view_conversation_typing_container.view.* -import network.loki.messenger.R +import network.loki.messenger.databinding.ViewConversationTypingContainerBinding import org.session.libsession.utilities.recipients.Recipient class TypingIndicatorViewContainer : LinearLayout { + private lateinit var binding: ViewConversationTypingContainerBinding constructor(context: Context) : super(context) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_conversation_typing_container, this) + binding = ViewConversationTypingContainerBinding.inflate(LayoutInflater.from(context), this, true) } fun setTypists(typists: List) { - if (typists.isEmpty()) { typingIndicator.stopAnimation(); return } - typingIndicator.startAnimation() + if (typists.isEmpty()) { binding.typingIndicator.stopAnimation(); return } + binding.typingIndicator.startAnimation() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt index fb4501c13..39ca7c691 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt @@ -6,8 +6,8 @@ import android.text.SpannableStringBuilder import android.text.style.StyleSpan import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog -import kotlinx.android.synthetic.main.dialog_blocked.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.DialogBlockedBinding import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog @@ -17,21 +17,21 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent class BlockedDialog(private val recipient: Recipient) : BaseDialog() { override fun setContentView(builder: AlertDialog.Builder) { - val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_blocked, null) + val binding = DialogBlockedBinding.inflate(LayoutInflater.from(requireContext())) val contactDB = DatabaseComponent.get(requireContext()).sessionContactDatabase() val sessionID = recipient.address.toString() val contact = contactDB.getContactWithSessionID(sessionID) val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID val title = resources.getString(R.string.dialog_blocked_title, name) - contentView.blockedTitleTextView.text = title + binding.blockedTitleTextView.text = title val explanation = resources.getString(R.string.dialog_blocked_explanation, name) val spannable = SpannableStringBuilder(explanation) val startIndex = explanation.indexOf(name) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - contentView.blockedExplanationTextView.text = spannable - contentView.cancelButton.setOnClickListener { dismiss() } - contentView.unblockButton.setOnClickListener { unblock() } - builder.setView(contentView) + binding.blockedExplanationTextView.text = spannable + binding.cancelButton.setOnClickListener { dismiss() } + binding.unblockButton.setOnClickListener { unblock() } + builder.setView(binding.root) } private fun unblock() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt index 72033be87..42cca1ad3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt @@ -7,8 +7,8 @@ import android.text.style.StyleSpan import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import dagger.hilt.android.AndroidEntryPoint -import kotlinx.android.synthetic.main.dialog_download.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.DialogDownloadBinding import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.JobQueue @@ -26,20 +26,20 @@ class DownloadDialog(private val recipient: Recipient) : BaseDialog() { @Inject lateinit var contactDB: SessionContactDatabase override fun setContentView(builder: AlertDialog.Builder) { - val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_download, null) + val binding = DialogDownloadBinding.inflate(LayoutInflater.from(requireContext())) val sessionID = recipient.address.toString() val contact = contactDB.getContactWithSessionID(sessionID) val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID val title = resources.getString(R.string.dialog_download_title, name) - contentView.downloadTitleTextView.text = title + binding.downloadTitleTextView.text = title val explanation = resources.getString(R.string.dialog_download_explanation, name) val spannable = SpannableStringBuilder(explanation) val startIndex = explanation.indexOf(name) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - contentView.downloadExplanationTextView.text = spannable - contentView.cancelButton.setOnClickListener { dismiss() } - contentView.downloadButton.setOnClickListener { trust() } - builder.setView(contentView) + binding.downloadExplanationTextView.text = spannable + binding.cancelButton.setOnClickListener { dismiss() } + binding.downloadButton.setOnClickListener { trust() } + builder.setView(binding.root) } private fun trust() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt index 0d4c30508..5c4c7444d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt @@ -7,8 +7,8 @@ import android.text.style.StyleSpan import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import kotlinx.android.synthetic.main.dialog_join_open_group.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.DialogJoinOpenGroupBinding import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsignal.utilities.ThreadUtils import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog @@ -19,17 +19,17 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities class JoinOpenGroupDialog(private val name: String, private val url: String) : BaseDialog() { override fun setContentView(builder: AlertDialog.Builder) { - val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_join_open_group, null) + val binding = DialogJoinOpenGroupBinding.inflate(LayoutInflater.from(requireContext())) val title = resources.getString(R.string.dialog_join_open_group_title, name) - contentView.joinOpenGroupTitleTextView.text = title + binding.joinOpenGroupTitleTextView.text = title val explanation = resources.getString(R.string.dialog_join_open_group_explanation, name) val spannable = SpannableStringBuilder(explanation) val startIndex = explanation.indexOf(name) spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + name.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - contentView.joinOpenGroupExplanationTextView.text = spannable - contentView.cancelButton.setOnClickListener { dismiss() } - contentView.joinButton.setOnClickListener { join() } - builder.setView(contentView) + binding.joinOpenGroupExplanationTextView.text = spannable + binding.cancelButton.setOnClickListener { dismiss() } + binding.joinButton.setOnClickListener { join() } + builder.setView(binding.root) } private fun join() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt index f9fa6c381..a16ca86f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt @@ -2,8 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog -import kotlinx.android.synthetic.main.dialog_link_preview.view.* -import network.loki.messenger.R +import network.loki.messenger.databinding.DialogLinkPreviewBinding import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog @@ -12,10 +11,10 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog class LinkPreviewDialog(private val onEnabled: () -> Unit) : BaseDialog() { override fun setContentView(builder: AlertDialog.Builder) { - val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_link_preview, null) - contentView.cancelButton.setOnClickListener { dismiss() } - contentView.enableLinkPreviewsButton.setOnClickListener { enable() } - builder.setView(contentView) + val binding = DialogLinkPreviewBinding.inflate(LayoutInflater.from(requireContext())) + binding.cancelButton.setOnClickListener { dismiss() } + binding.enableLinkPreviewsButton.setOnClickListener { enable() } + builder.setView(binding.root) } private fun enable() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt index b215e1f65..f51261d49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt @@ -2,18 +2,17 @@ package org.thoughtcrime.securesms.conversation.v2.dialogs import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog -import kotlinx.android.synthetic.main.dialog_send_seed.view.* -import network.loki.messenger.R +import network.loki.messenger.databinding.DialogSendSeedBinding import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog /** Shown if the user is about to send their recovery phrase to someone. */ class SendSeedDialog(private val proceed: (() -> Unit)? = null) : BaseDialog() { override fun setContentView(builder: AlertDialog.Builder) { - val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_send_seed, null) - contentView.cancelButton.setOnClickListener { dismiss() } - contentView.sendSeedButton.setOnClickListener { send() } - builder.setView(contentView) + val binding = DialogSendSeedBinding.inflate(LayoutInflater.from(requireContext())) + binding.cancelButton.setOnClickListener { dismiss() } + binding.sendSeedButton.setOnClickListener { send() } + builder.setView(binding.root) } private fun send() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index 3d57dfcea..cfa7fb08a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -4,13 +4,14 @@ import android.content.Context import android.content.res.Resources import android.net.Uri import android.text.InputType +import android.text.TextWatcher import android.util.AttributeSet import android.view.LayoutInflater import android.view.MotionEvent import android.widget.RelativeLayout import androidx.core.view.isVisible -import kotlinx.android.synthetic.main.view_input_bar.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewInputBarBinding import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient @@ -27,6 +28,7 @@ import kotlin.math.max import kotlin.math.roundToInt class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, LinkPreviewDraftViewDelegate { + private lateinit var binding: ViewInputBarBinding private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val vMargin by lazy { toDp(4, resources) } private val minHeight by lazy { toPx(56, resources) } @@ -39,8 +41,11 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li set(value) { field = value; showOrHideInputIfNeeded() } var text: String - get() { return inputBarEditText.text?.toString() ?: "" } - set(value) { inputBarEditText.setText(value) } + get() { return binding.inputBarEditText.text?.toString() ?: "" } + set(value) { binding.inputBarEditText.setText(value) } + + val attachmentButtonsContainerHeight: Int + get() = binding.attachmentsButtonContainer.height private val attachmentsButton by lazy { InputBarButton(context, R.drawable.ic_plus_24) } private val microphoneButton by lazy { InputBarButton(context, R.drawable.ic_microphone) } @@ -52,36 +57,36 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_input_bar, this) + binding = ViewInputBarBinding.inflate(LayoutInflater.from(context), this, true) // Attachments button - attachmentsButtonContainer.addView(attachmentsButton) - attachmentsButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + binding.attachmentsButtonContainer.addView(attachmentsButton) + attachmentsButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) attachmentsButton.onPress = { toggleAttachmentOptions() } // Microphone button - microphoneOrSendButtonContainer.addView(microphoneButton) - microphoneButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + binding.microphoneOrSendButtonContainer.addView(microphoneButton) + microphoneButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) microphoneButton.onLongPress = { startRecordingVoiceMessage() } microphoneButton.onMove = { delegate?.onMicrophoneButtonMove(it) } microphoneButton.onCancel = { delegate?.onMicrophoneButtonCancel(it) } microphoneButton.onUp = { delegate?.onMicrophoneButtonUp(it) } // Send button - microphoneOrSendButtonContainer.addView(sendButton) - sendButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT) + binding.microphoneOrSendButtonContainer.addView(sendButton) + sendButton.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) sendButton.isVisible = false sendButton.onUp = { delegate?.sendMessage() } // Edit text val incognitoFlag = if (TextSecurePreferences.isIncognitoKeyboardEnabled(context)) 16777216 else 0 - inputBarEditText.imeOptions = inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled - inputBarEditText.inputType = inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES - inputBarEditText.delegate = this + binding.inputBarEditText.imeOptions = binding.inputBarEditText.imeOptions or incognitoFlag // Always use incognito keyboard if setting enabled + binding.inputBarEditText.inputType = binding.inputBarEditText.inputType or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + binding.inputBarEditText.delegate = this } // endregion // region General private fun setHeight(newHeight: Int) { - val layoutParams = inputBarLinearLayout.layoutParams as LayoutParams + val layoutParams = binding.inputBarLinearLayout.layoutParams as LayoutParams layoutParams.height = newHeight - inputBarLinearLayout.layoutParams = layoutParams + binding.inputBarLinearLayout.layoutParams = layoutParams delegate?.inputBarHeightChanged(newHeight) } // endregion @@ -94,7 +99,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li } override fun inputBarEditTextHeightChanged(newValue: Int) { - val newHeight = max(newValue + 2 * vMargin, minHeight) + inputBarAdditionalContentContainer.height + val newHeight = max(newValue + 2 * vMargin, minHeight) + binding.inputBarAdditionalContentContainer.height setHeight(newHeight) } @@ -117,10 +122,10 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li quote = message linkPreview = null linkPreviewDraftView = null - inputBarAdditionalContentContainer.removeAllViews() + binding.inputBarAdditionalContentContainer.removeAllViews() val quoteView = QuoteView(context, QuoteView.Mode.Draft) quoteView.delegate = this - inputBarAdditionalContentContainer.addView(quoteView) + binding.inputBarAdditionalContentContainer.addView(quoteView) val attachments = (message as? MmsMessageRecord)?.slideDeck // The max content width is the screen width - 2 times the horizontal input bar padding - the // quote view content area's start and end margins. This unfortunately has to be calculated manually @@ -132,15 +137,15 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li // The 6 DP below is the padding the quote view applies to itself, which isn't included in the // intrinsic height calculation. val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources) - val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + quoteViewIntrinsicHeight + val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight) + quoteViewIntrinsicHeight additionalContentHeight = quoteViewIntrinsicHeight setHeight(newHeight) } override fun cancelQuoteDraft() { quote = null - inputBarAdditionalContentContainer.removeAllViews() - val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + binding.inputBarAdditionalContentContainer.removeAllViews() + val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight) additionalContentHeight = 0 setHeight(newHeight) } @@ -148,12 +153,12 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li fun draftLinkPreview() { quote = null val linkPreviewDraftHeight = toPx(88, resources) - inputBarAdditionalContentContainer.removeAllViews() + binding.inputBarAdditionalContentContainer.removeAllViews() val linkPreviewDraftView = LinkPreviewDraftView(context) linkPreviewDraftView.delegate = this this.linkPreviewDraftView = linkPreviewDraftView - inputBarAdditionalContentContainer.addView(linkPreviewDraftView) - val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + linkPreviewDraftHeight + binding.inputBarAdditionalContentContainer.addView(linkPreviewDraftView) + val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight) + linkPreviewDraftHeight additionalContentHeight = linkPreviewDraftHeight setHeight(newHeight) } @@ -167,24 +172,32 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li override fun cancelLinkPreviewDraft() { if (quote != null) { return } linkPreview = null - inputBarAdditionalContentContainer.removeAllViews() - val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + binding.inputBarAdditionalContentContainer.removeAllViews() + val newHeight = max(binding.inputBarEditText.height + 2 * vMargin, minHeight) additionalContentHeight = 0 setHeight(newHeight) } private fun showOrHideInputIfNeeded() { if (showInput) { - setOf( inputBarEditText, attachmentsButton ).forEach { it.isVisible = true } + setOf( binding.inputBarEditText, attachmentsButton ).forEach { it.isVisible = true } microphoneButton.isVisible = text.isEmpty() sendButton.isVisible = text.isNotEmpty() } else { cancelQuoteDraft() cancelLinkPreviewDraft() - val views = setOf( inputBarEditText, attachmentsButton, microphoneButton, sendButton ) + val views = setOf( binding.inputBarEditText, attachmentsButton, microphoneButton, sendButton ) views.forEach { it.isVisible = false } } } + + fun addTextChangedListener(textWatcher: TextWatcher) { + binding.inputBarEditText.addTextChangedListener(textWatcher) + } + + fun setSelection(index: Int) { + binding.inputBarEditText.setSelection(index) + } // endregion } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt index 1d1446100..90afabb80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt @@ -45,8 +45,8 @@ class InputBarEditText : AppCompatEditText { delegate?.inputBarEditTextHeightChanged(constrainedHeight.roundToInt()) } - override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection { - val ic: InputConnection = super.onCreateInputConnection(editorInfo) + override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { + val ic = super.onCreateInputConnection(editorInfo) ?: return null EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/png", "image/gif", "image/jpg")) val callback = diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt index cc8166212..ec45b6ca8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt @@ -8,40 +8,56 @@ import android.os.Handler import android.os.Looper import android.util.AttributeSet import android.view.LayoutInflater +import android.widget.ImageView +import android.widget.LinearLayout import android.widget.RelativeLayout +import android.widget.TextView import androidx.core.content.res.ResourcesCompat import androidx.core.view.isVisible -import kotlinx.android.synthetic.main.view_input_bar_recording.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewInputBarRecordingBinding +import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.animateSizeChange import org.thoughtcrime.securesms.util.disableClipping import org.thoughtcrime.securesms.util.toPx -import org.thoughtcrime.securesms.util.DateUtils -import java.util.* +import java.util.Date class InputBarRecordingView : RelativeLayout { + private lateinit var binding: ViewInputBarRecordingBinding private var startTimestamp = 0L private val snHandler = Handler(Looper.getMainLooper()) private var dotViewAnimation: ValueAnimator? = null private var pulseAnimation: ValueAnimator? = null var delegate: InputBarRecordingViewDelegate? = null + val lockView: LinearLayout + get() = binding.lockView + + val chevronImageView: ImageView + get() = binding.inputBarChevronImageView + + val slideToCancelTextView: TextView + get() = binding.inputBarSlideToCancelTextView + + val recordButtonOverlay: RelativeLayout + get() = binding.recordButtonOverlay + constructor(context: Context) : super(context) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_input_bar_recording, this) - inputBarMiddleContentContainer.disableClipping() - inputBarCancelButton.setOnClickListener { hide() } + binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true) + binding.inputBarMiddleContentContainer.disableClipping() + binding.inputBarCancelButton.setOnClickListener { hide() } } fun show() { startTimestamp = Date().time - recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme)) - inputBarCancelButton.alpha = 0.0f - inputBarMiddleContentContainer.alpha = 1.0f - lockView.alpha = 1.0f + binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme)) + binding.inputBarCancelButton.alpha = 0.0f + binding.inputBarMiddleContentContainer.alpha = 1.0f + binding.lockView.alpha = 1.0f isVisible = true alpha = 0.0f val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) @@ -77,7 +93,7 @@ class InputBarRecordingView : RelativeLayout { dotViewAnimation = animation animation.duration = 500L animation.addUpdateListener { animator -> - dotView.alpha = animator.animatedValue as Float + binding.dotView.alpha = animator.animatedValue as Float } animation.repeatCount = ValueAnimator.INFINITE animation.repeatMode = ValueAnimator.REVERSE @@ -87,12 +103,12 @@ class InputBarRecordingView : RelativeLayout { private fun pulse() { val collapsedSize = toPx(80.0f, resources) val expandedSize = toPx(104.0f, resources) - pulseView.animateSizeChange(collapsedSize, expandedSize, 1000) + binding.pulseView.animateSizeChange(collapsedSize, expandedSize, 1000) val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.5, 0.0f) pulseAnimation = animation animation.duration = 1000L animation.addUpdateListener { animator -> - pulseView.alpha = animator.animatedValue as Float + binding.pulseView.alpha = animator.animatedValue as Float if (animator.animatedFraction == 1.0f && isVisible) { pulse() } } animation.start() @@ -101,21 +117,21 @@ class InputBarRecordingView : RelativeLayout { private fun animateLockViewUp() { val startMarginBottom = toPx(32, resources) val endMarginBottom = toPx(72, resources) - val layoutParams = lockView.layoutParams as LayoutParams + val layoutParams = binding.lockView.layoutParams as LayoutParams layoutParams.bottomMargin = startMarginBottom - lockView.layoutParams = layoutParams + binding.lockView.layoutParams = layoutParams val animation = ValueAnimator.ofObject(IntEvaluator(), startMarginBottom, endMarginBottom) animation.duration = 250L animation.addUpdateListener { animator -> layoutParams.bottomMargin = animator.animatedValue as Int - lockView.layoutParams = layoutParams + binding.lockView.layoutParams = layoutParams } animation.start() } private fun updateTimer() { val duration = (Date().time - startTimestamp) / 1000L - recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration) + binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration) snHandler.postDelayed({ updateTimer() }, 500) } @@ -123,19 +139,19 @@ class InputBarRecordingView : RelativeLayout { val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) fadeOutAnimation.duration = 250L fadeOutAnimation.addUpdateListener { animator -> - inputBarMiddleContentContainer.alpha = animator.animatedValue as Float - lockView.alpha = animator.animatedValue as Float + binding.inputBarMiddleContentContainer.alpha = animator.animatedValue as Float + binding.lockView.alpha = animator.animatedValue as Float } fadeOutAnimation.start() val fadeInAnimation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) fadeInAnimation.duration = 250L fadeInAnimation.addUpdateListener { animator -> - inputBarCancelButton.alpha = animator.animatedValue as Float + binding.inputBarCancelButton.alpha = animator.animatedValue as Float } fadeInAnimation.start() - recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme)) - recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() } - inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() } + binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_arrow_up, context.theme)) + binding.recordButtonOverlay.setOnClickListener { delegate?.sendVoiceMessage() } + binding.inputBarCancelButton.setOnClickListener { delegate?.cancelVoiceMessage() } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt index 2159a5dac..2266a1c56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt @@ -4,33 +4,29 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout import android.widget.RelativeLayout -import kotlinx.android.synthetic.main.view_mention_candidate.view.* -import network.loki.messenger.R +import network.loki.messenger.databinding.ViewMentionCandidateV2Binding import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.thoughtcrime.securesms.mms.GlideRequests -class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : RelativeLayout(context, attrs, defStyleAttr) { +class MentionCandidateView : RelativeLayout { + private lateinit var binding: ViewMentionCandidateV2Binding var candidate = Mention("", "") set(newValue) { field = newValue; update() } var glide: GlideRequests? = null var openGroupServer: String? = null var openGroupRoom: String? = null - constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context) : this(context, null) + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } - companion object { - - fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView { - return layoutInflater.inflate(R.layout.view_mention_candidate_v2, parent, false) as MentionCandidateView - } + private fun initialize() { + binding = ViewMentionCandidateV2Binding.inflate(LayoutInflater.from(context), this, true) } - private fun update() { + private fun update() = with(binding) { mentionCandidateNameTextView.text = candidate.displayName profilePictureView.publicKey = candidate.publicKey profilePictureView.displayName = candidate.displayName diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt index 6e8f07741..401ccaa3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions import android.content.Context import android.util.AttributeSet -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter @@ -42,7 +41,7 @@ class MentionCandidatesView(context: Context, attrs: AttributeSet?, defStyleAttr override fun getItem(position: Int): Mention { return candidates[position] } override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View { - val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent) + val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView(context) val mentionCandidate = getItem(position) cell.glide = glide cell.candidate = mentionCandidate diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt index adfbf297e..bb581f25f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -12,7 +12,6 @@ import android.os.AsyncTask import android.view.Menu import android.view.MenuInflater import android.view.MenuItem -import android.view.View import android.widget.ImageView import android.widget.TextView import android.widget.Toast @@ -24,7 +23,6 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat -import kotlinx.android.synthetic.main.activity_conversation_v2.* import network.loki.messenger.R import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.sending_receiving.MessageSender @@ -35,7 +33,12 @@ import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.toHexString -import org.thoughtcrime.securesms.* +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.ExpirationDialog +import org.thoughtcrime.securesms.MediaOverviewActivity +import org.thoughtcrime.securesms.MuteDialog +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +import org.thoughtcrime.securesms.ShortcutLauncherActivity import org.thoughtcrime.securesms.contacts.SelectContactsActivity import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils @@ -101,15 +104,12 @@ object ConversationMenuHelper { val searchViewItem = menu.findItem(R.id.menu_search) (context as ConversationActivityV2).searchViewItem = searchViewItem val searchView = searchViewItem.actionView as SearchView - val searchViewModel = context.searchViewModel!! val queryListener = object : OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { return true } override fun onQueryTextChange(query: String): Boolean { - searchViewModel.onQueryUpdated(query, threadId) - context.searchBottomBar.showLoading() context.onSearchQueryUpdated(query) return true } @@ -117,10 +117,7 @@ object ConversationMenuHelper { searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { override fun onMenuItemActionExpand(item: MenuItem): Boolean { searchView.setOnQueryTextListener(queryListener) - searchViewModel.onSearchOpened() - context.searchBottomBar.visibility = View.VISIBLE - context.searchBottomBar.setData(0, 0) - context.inputBar.visibility = View.GONE + context.onSearchOpened() for (i in 0 until menu.size()) { if (menu.getItem(i) != searchViewItem) { menu.getItem(i).isVisible = false @@ -131,11 +128,7 @@ object ConversationMenuHelper { override fun onMenuItemActionCollapse(item: MenuItem): Boolean { searchView.setOnQueryTextListener(null) - searchViewModel.onSearchClosed() - context.searchBottomBar.visibility = View.GONE - context.inputBar.visibility = View.VISIBLE - context.onSearchQueryUpdated(null) - context.invalidateOptionsMenu() + context.onSearchClosed() return true } }) @@ -169,7 +162,7 @@ object ConversationMenuHelper { } private fun search(context: Context) { - val searchViewModel = (context as ConversationActivityV2).searchViewModel!! + val searchViewModel = (context as ConversationActivityV2).searchViewModel searchViewModel.onSearchOpened() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt index 2b3a545b0..73e5ac338 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt @@ -7,35 +7,41 @@ import android.view.View import android.widget.LinearLayout import androidx.core.content.res.ResourcesCompat import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.view_control_message.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewControlMessageBinding import org.thoughtcrime.securesms.database.model.MessageRecord class ControlMessageView : LinearLayout { + private lateinit var binding: ViewControlMessageBinding + // region Lifecycle constructor(context: Context) : super(context) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_control_message, this) + binding = ViewControlMessageBinding.inflate(LayoutInflater.from(context), this, true) layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) } // endregion // region Updating fun bind(message: MessageRecord, previous: MessageRecord?) { - dateBreakTextView.showDateBreak(message, previous) - iconImageView.visibility = View.GONE + binding.dateBreakTextView.showDateBreak(message, previous) + binding.iconImageView.visibility = View.GONE if (message.isExpirationTimerUpdate) { - iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme)) - iconImageView.visibility = View.VISIBLE + binding.iconImageView.setImageDrawable( + ResourcesCompat.getDrawable(resources, R.drawable.ic_timer, context.theme) + ) + binding.iconImageView.visibility = View.VISIBLE } else if (message.isMediaSavedNotification) { - iconImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme)) - iconImageView.visibility = View.VISIBLE + binding.iconImageView.setImageDrawable( + ResourcesCompat.getDrawable(resources, R.drawable.ic_file_download_white_36dp, context.theme) + ) + binding.iconImageView.visibility = View.VISIBLE } - textView.text = message.getDisplayBody(context) + binding.textView.text = message.getDisplayBody(context) } fun recycle() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt index a91473352..99af8c2ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt @@ -6,32 +6,28 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout import androidx.annotation.ColorInt -import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.view.* -import kotlinx.android.synthetic.main.view_deleted_message.view.* -import kotlinx.android.synthetic.main.view_document.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewDeletedMessageBinding import org.thoughtcrime.securesms.database.model.MessageRecord -import org.thoughtcrime.securesms.database.model.MmsMessageRecord -import java.util.* class DeletedMessageView : LinearLayout { - + private lateinit var binding: ViewDeletedMessageBinding // region Lifecycle constructor(context: Context) : super(context) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_deleted_message, this) + binding = ViewDeletedMessageBinding.inflate(LayoutInflater.from(context), this, true) } // endregion // region Updating fun bind(message: MessageRecord, @ColorInt textColor: Int) { assert(message.isDeleted) - deleteTitleTextView.text = context.getString(R.string.deleted_message) - deleteTitleTextView.setTextColor(textColor) - deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) + binding.deleteTitleTextView.text = context.getString(R.string.deleted_message) + binding.deleteTitleTextView.setTextColor(textColor) + binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) } // endregion } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt index c9daca6c7..e0a0630e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt @@ -6,29 +6,27 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout import androidx.annotation.ColorInt -import kotlinx.android.synthetic.main.view_document.view.* -import network.loki.messenger.R -import org.thoughtcrime.securesms.database.model.MessageRecord +import network.loki.messenger.databinding.ViewDocumentBinding import org.thoughtcrime.securesms.database.model.MmsMessageRecord class DocumentView : LinearLayout { - + private lateinit var binding: ViewDocumentBinding // region Lifecycle constructor(context: Context) : super(context) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_document, this) + binding = ViewDocumentBinding.inflate(LayoutInflater.from(context), this, true) } // endregion // region Updating fun bind(message: MmsMessageRecord, @ColorInt textColor: Int) { val document = message.slideDeck.documentSlide!! - documentTitleTextView.text = document.fileName.or("Untitled File") - documentTitleTextView.setTextColor(textColor) - documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) + binding.documentTitleTextView.text = document.fileName.or("Untitled File") + binding.documentTitleTextView.setTextColor(textColor) + binding.documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) } // endregion } \ No newline at end of file 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 45921122f..9d751d3fb 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 @@ -11,8 +11,8 @@ import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.content.res.ResourcesCompat import androidx.core.view.isVisible -import kotlinx.android.synthetic.main.view_link_preview.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewLinkPreviewBinding import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities @@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.util.UiModeUtilities class LinkPreviewView : LinearLayout { + private lateinit var binding: ViewLinkPreviewBinding private val cornerMask by lazy { CornerMask(this) } private var url: String? = null lateinit var bodyTextView: TextView @@ -33,7 +34,7 @@ class LinkPreviewView : LinearLayout { constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_link_preview, this) + binding = ViewLinkPreviewBinding.inflate(LayoutInflater.from(context), this, true) } // endregion @@ -44,20 +45,20 @@ class LinkPreviewView : LinearLayout { // Thumbnail if (linkPreview.getThumbnail().isPresent) { // This internally fetches the thumbnail - thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message) - thumbnailImageView.loadIndicator.isVisible = false + binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message) + binding.thumbnailImageView.loadIndicator.isVisible = false } // Title - titleTextView.text = linkPreview.title + binding.titleTextView.text = linkPreview.title val textColorID = if (message.isOutgoing && UiModeUtilities.isDayUiMode(context)) { R.color.white } else { if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.white } - titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme)) + binding.titleTextView.setTextColor(ResourcesCompat.getColor(resources, textColorID, context.theme)) // Body bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) - mainLinkPreviewContainer.addView(bodyTextView) + binding.mainLinkPreviewContainer.addView(bodyTextView) // Corner radii val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) cornerMask.setTopLeftRadius(cornerRadii[0]) @@ -78,7 +79,7 @@ class LinkPreviewView : LinearLayout { val rawYInt = event.rawY.toInt() val hitRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt) val previewRect = Rect() - mainLinkPreviewParent.getGlobalVisibleRect(previewRect) + binding.mainLinkPreviewParent.getGlobalVisibleRect(previewRect) if (previewRect.contains(hitRect)) { openURL() return diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt index ebcdcac2d..4cfc5b556 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt @@ -6,15 +6,15 @@ import android.view.LayoutInflater import android.widget.LinearLayout import androidx.annotation.ColorInt import androidx.appcompat.app.AppCompatActivity -import kotlinx.android.synthetic.main.view_open_group_invitation.view.* import network.loki.messenger.R -import org.session.libsession.messaging.open_groups.OpenGroupV2 +import network.loki.messenger.databinding.ViewOpenGroupInvitationBinding import org.session.libsession.messaging.utilities.UpdateMessageData import org.session.libsession.utilities.OpenGroupUrlParser import org.thoughtcrime.securesms.conversation.v2.dialogs.JoinOpenGroupDialog import org.thoughtcrime.securesms.database.model.MessageRecord class OpenGroupInvitationView : LinearLayout { + private lateinit var binding: ViewOpenGroupInvitationBinding private var data: UpdateMessageData.Kind.OpenGroupInvitation? = null constructor(context: Context): super(context) { initialize() } @@ -22,7 +22,7 @@ class OpenGroupInvitationView : LinearLayout { constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_open_group_invitation, this) + binding = ViewOpenGroupInvitationBinding.inflate(LayoutInflater.from(context), this, true) } fun bind(message: MessageRecord, @ColorInt textColor: Int) { @@ -31,12 +31,14 @@ class OpenGroupInvitationView : LinearLayout { val data = umd.kind as UpdateMessageData.Kind.OpenGroupInvitation this.data = data val iconID = if (message.isOutgoing) R.drawable.ic_globe else R.drawable.ic_plus - openGroupInvitationIconImageView.setImageResource(iconID) - openGroupTitleTextView.text = data.groupName - openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl) - openGroupTitleTextView.setTextColor(textColor) - openGroupJoinMessageTextView.setTextColor(textColor) - openGroupURLTextView.setTextColor(textColor) + with(binding){ + openGroupInvitationIconImageView.setImageResource(iconID) + openGroupTitleTextView.text = data.groupName + openGroupURLTextView.text = OpenGroupUrlParser.trimQueryParameter(data.groupUrl) + openGroupTitleTextView.setTextColor(textColor) + openGroupJoinMessageTextView.setTextColor(textColor) + openGroupURLTextView.setTextColor(textColor) + } } fun joinOpenGroup() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt index fee6ec02d..fa9f31044 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt @@ -11,8 +11,8 @@ import androidx.core.content.res.ResourcesCompat import androidx.core.text.toSpannable import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint -import kotlinx.android.synthetic.main.view_quote.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewQuoteBinding import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities @@ -39,6 +39,7 @@ class QuoteView : LinearLayout { @Inject lateinit var contactDb: SessionContactDatabase + private lateinit var binding: ViewQuoteBinding private lateinit var mode: Mode private val vPadding by lazy { toPx(6, resources) } var delegate: QuoteViewDelegate? = null @@ -52,19 +53,19 @@ class QuoteView : LinearLayout { constructor(context: Context, mode: Mode) : super(context) { this.mode = mode - LayoutInflater.from(context).inflate(R.layout.view_quote, this) - // Add padding here (not on mainQuoteViewContainer) to get a bit of a top inset while avoiding + binding = ViewQuoteBinding.inflate(LayoutInflater.from(context), this, true) + // Add padding here (not on binding.mainQuoteViewContainer) to get a bit of a top inset while avoiding // the clipping issue described in getIntrinsicHeight(maxContentWidth:). setPadding(0, toPx(6, resources), 0, 0) when (mode) { - Mode.Draft -> quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() } + Mode.Draft -> binding.quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() } Mode.Regular -> { - quoteViewCancelButton.isVisible = false - mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme)) - val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams + binding.quoteViewCancelButton.isVisible = false + binding.mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme)) + val quoteViewMainContentContainerLayoutParams = binding.quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams // Since we're not showing the cancel button we can shorten the end margin quoteViewMainContentContainerLayoutParams.marginEnd = resources.getDimension(R.dimen.medium_spacing).roundToInt() - quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams + binding.quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams } } } @@ -73,19 +74,19 @@ class QuoteView : LinearLayout { // region General fun getIntrinsicContentHeight(maxContentWidth: Int): Int { // If we're showing an attachment thumbnail, just constrain to the height of that - if (quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) } + if (binding.quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) } var result = 0 var authorTextViewIntrinsicHeight = 0 - if (quoteViewAuthorTextView.isVisible) { - val author = quoteViewAuthorTextView.text - authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, quoteViewAuthorTextView.paint, maxContentWidth) + if (binding.quoteViewAuthorTextView.isVisible) { + val author = binding.quoteViewAuthorTextView.text + authorTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(author, binding.quoteViewAuthorTextView.paint, maxContentWidth) result += authorTextViewIntrinsicHeight } - val body = quoteViewBodyTextView.text - val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, quoteViewBodyTextView.paint, maxContentWidth) - val staticLayout = TextUtilities.getIntrinsicLayout(body, quoteViewBodyTextView.paint, maxContentWidth) + val body = binding.quoteViewBodyTextView.text + val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, binding.quoteViewBodyTextView.paint, maxContentWidth) + val staticLayout = TextUtilities.getIntrinsicLayout(body, binding.quoteViewBodyTextView.paint, maxContentWidth) result += bodyTextViewIntrinsicHeight - if (!quoteViewAuthorTextView.isVisible) { + if (!binding.quoteViewAuthorTextView.isVisible) { // We want to at least be as high as the cancel button 36DP, and no higher than 3 lines of text. // Height from intrinsic layout is the height of the text before truncation so we shorten // proportionally to our max lines setting. @@ -115,82 +116,90 @@ class QuoteView : LinearLayout { // Reduce the max body text view line count to 2 if this is a group thread because // we'll be showing the author text view and we don't want the overall quote view height // to get too big. - quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3 + binding.quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3 // Author if (thread.isGroupRecipient) { val author = contactDb.getContactWithSessionID(authorPublicKey) val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey - quoteViewAuthorTextView.text = authorDisplayName - quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage)) + binding.quoteViewAuthorTextView.text = authorDisplayName + binding.quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage)) } - quoteViewAuthorTextView.isVisible = thread.isGroupRecipient + binding.quoteViewAuthorTextView.isVisible = thread.isGroupRecipient // Body - quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context); - quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage)) + binding.quoteViewBodyTextView.text = if (isOpenGroupInvitation) resources.getString(R.string.open_group_invitation_view__open_group_invitation) else MentionUtilities.highlightMentions((body ?: "").toSpannable(), threadID, context); + binding.quoteViewBodyTextView.setTextColor(getTextColor(isOutgoingMessage)) // Accent line / attachment preview val hasAttachments = (attachments != null && attachments.asAttachments().isNotEmpty()) && !isOriginalMissing - quoteViewAccentLine.isVisible = !hasAttachments - quoteViewAttachmentPreviewContainer.isVisible = hasAttachments + binding.quoteViewAccentLine.isVisible = !hasAttachments + binding.quoteViewAttachmentPreviewContainer.isVisible = hasAttachments if (!hasAttachments) { - val accentLineLayoutParams = quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams + val accentLineLayoutParams = binding.quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height - quoteViewAccentLine.layoutParams = accentLineLayoutParams - quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage)) + binding.quoteViewAccentLine.layoutParams = accentLineLayoutParams + binding.quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage)) } else if (attachments != null) { - quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme)) + binding.quoteViewAttachmentPreviewImageView.imageTintList = ColorStateList.valueOf(ResourcesCompat.getColor(resources, R.color.white, context.theme)) val backgroundColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.black else R.color.accent val backgroundColor = ResourcesCompat.getColor(resources, backgroundColorID, context.theme) - quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor) - quoteViewAttachmentPreviewImageView.isVisible = false - quoteViewAttachmentThumbnailImageView.isVisible = false - if (attachments.audioSlide != null) { - quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone) - quoteViewAttachmentPreviewImageView.isVisible = true - quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio) - } else if (attachments.documentSlide != null) { - quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light) - quoteViewAttachmentPreviewImageView.isVisible = true - quoteViewBodyTextView.text = resources.getString(R.string.document) - } else if (attachments.thumbnailSlide != null) { - val slide = attachments.thumbnailSlide!! - // This internally fetches the thumbnail - quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources) - quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false) - quoteViewAttachmentThumbnailImageView.isVisible = true - quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image) + binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor) + binding.quoteViewAttachmentPreviewImageView.isVisible = false + binding.quoteViewAttachmentThumbnailImageView.isVisible = false + when { + attachments.audioSlide != null -> { + binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone) + binding.quoteViewAttachmentPreviewImageView.isVisible = true + binding.quoteViewBodyTextView.text = resources.getString(R.string.Slide_audio) + } + attachments.documentSlide != null -> { + binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_document_large_light) + binding.quoteViewAttachmentPreviewImageView.isVisible = true + binding.quoteViewBodyTextView.text = resources.getString(R.string.document) + } + attachments.thumbnailSlide != null -> { + val slide = attachments.thumbnailSlide!! + // This internally fetches the thumbnail + binding.quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources) + binding.quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false) + binding.quoteViewAttachmentThumbnailImageView.isVisible = true + binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image) + } } } - mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth)) - val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams + binding.mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth)) + val quoteViewMainContentContainerLayoutParams = binding.quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams // The start margin is different if we just show the accent line vs if we show an attachment thumbnail quoteViewMainContentContainerLayoutParams.marginStart = if (!hasAttachments) toPx(16, resources) else toPx(48, resources) - quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams + binding.quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams } // endregion // region Convenience @ColorInt private fun getLineColor(isOutgoingMessage: Boolean): Int { val isLightMode = UiModeUtilities.isDayUiMode(context) - if ((mode == Mode.Regular && isLightMode) || (mode == Mode.Draft && isLightMode)) { - return ResourcesCompat.getColor(resources, R.color.black, context.theme) - } else if (mode == Mode.Regular && !isLightMode) { - if (isOutgoingMessage) { - return ResourcesCompat.getColor(resources, R.color.black, context.theme) - } else { - return ResourcesCompat.getColor(resources, R.color.accent, context.theme) + return when { + mode == Mode.Regular && isLightMode || mode == Mode.Draft && isLightMode -> { + ResourcesCompat.getColor(resources, R.color.black, context.theme) + } + mode == Mode.Regular && !isLightMode -> { + if (isOutgoingMessage) { + ResourcesCompat.getColor(resources, R.color.black, context.theme) + } else { + ResourcesCompat.getColor(resources, R.color.accent, context.theme) + } + } + else -> { // Draft & dark mode + ResourcesCompat.getColor(resources, R.color.accent, context.theme) } - } else { // Draft & dark mode - return ResourcesCompat.getColor(resources, R.color.accent, context.theme) } } @ColorInt private fun getTextColor(isOutgoingMessage: Boolean): Int { if (mode == Mode.Draft) { return ResourcesCompat.getColor(resources, R.color.text, context.theme) } val isLightMode = UiModeUtilities.isDayUiMode(context) - if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) { - return ResourcesCompat.getColor(resources, R.color.black, context.theme) + return if ((isOutgoingMessage && !isLightMode) || (!isOutgoingMessage && isLightMode)) { + ResourcesCompat.getColor(resources, R.color.black, context.theme) } else { - return ResourcesCompat.getColor(resources, R.color.white, context.theme) + ResourcesCompat.getColor(resources, R.color.white, context.theme) } } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt index 967e080d9..7e22da091 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt @@ -6,15 +6,15 @@ import android.view.LayoutInflater import android.widget.LinearLayout import androidx.annotation.ColorInt import androidx.core.content.ContextCompat -import kotlinx.android.synthetic.main.view_untrusted_attachment.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewUntrustedAttachmentBinding import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.dialogs.DownloadDialog import org.thoughtcrime.securesms.util.ActivityDispatcher -import java.util.* +import java.util.Locale class UntrustedAttachmentView: LinearLayout { - + private lateinit var binding: ViewUntrustedAttachmentBinding enum class AttachmentType { AUDIO, DOCUMENT, @@ -27,7 +27,7 @@ class UntrustedAttachmentView: LinearLayout { constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_untrusted_attachment, this) + binding = ViewUntrustedAttachmentBinding.inflate(LayoutInflater.from(context), this, true) } // endregion @@ -42,8 +42,8 @@ class UntrustedAttachmentView: LinearLayout { iconDrawable.mutate().setTint(textColor) val text = context.getString(R.string.UntrustedAttachmentView_download_attachment, context.getString(stringRes).toLowerCase(Locale.ROOT)) - untrustedAttachmentIcon.setImageDrawable(iconDrawable) - untrustedAttachmentTitle.text = text + binding.untrustedAttachmentIcon.setImageDrawable(iconDrawable) + binding.untrustedAttachmentTitle.text = text } // endregion 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 f98b3a23d..fd3d47400 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 @@ -23,8 +23,9 @@ import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat import androidx.core.text.getSpans import androidx.core.text.toSpannable -import kotlinx.android.synthetic.main.view_visible_message_content.view.* +import androidx.core.view.children import network.loki.messenger.R +import network.loki.messenger.databinding.ViewVisibleMessageContentBinding import okhttp3.HttpUrl import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ViewUtil @@ -40,14 +41,14 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.util.SearchUtil -import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.util.getColorWithID import org.thoughtcrime.securesms.util.toPx -import java.util.* +import java.util.Locale import kotlin.math.roundToInt class VisibleMessageContentView : LinearLayout { + private lateinit var binding: ViewVisibleMessageContentBinding var onContentClick: ((event: MotionEvent) -> Unit)? = null var onContentDoubleTap: (() -> Unit)? = null var delegate: VisibleMessageContentViewDelegate? = null @@ -59,7 +60,7 @@ class VisibleMessageContentView : LinearLayout { constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_visible_message_content, this) + binding = ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(context), this, true) } // endregion @@ -74,17 +75,17 @@ class VisibleMessageContentView : LinearLayout { background.colorFilter = filter setBackground(background) // Body - mainContainer.removeAllViews() + binding.mainContainer.removeAllViews() onContentClick = null onContentDoubleTap = null if (message.isDeleted) { val deletedMessageView = DeletedMessageView(context) - deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message)) - mainContainer.addView(deletedMessageView) + deletedMessageView.bind(message, getTextColor(context,message)) + binding.mainContainer.addView(deletedMessageView) } else if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) { val linkPreviewView = LinkPreviewView(context) linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery) - mainContainer.addView(linkPreviewView) + binding.mainContainer.addView(linkPreviewView) onContentClick = { event -> linkPreviewView.calculateHit(event) } // Body text view is inside the link preview for layout convenience } else if (message is MmsMessageRecord && message.quote != null) { @@ -102,10 +103,10 @@ class VisibleMessageContentView : LinearLayout { quoteView.bind(quote.author.toString(), quoteText, quote.attachment, thread, message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation, message.threadId, quote.isOriginalMissing, glide) - mainContainer.addView(quoteView) - val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) + binding.mainContainer.addView(quoteView) + val bodyTextView = getBodyTextView(context, message, searchQuery) ViewUtil.setPaddingTop(bodyTextView, 0) - mainContainer.addView(bodyTextView) + binding.mainContainer.addView(bodyTextView) onContentClick = { event -> val r = Rect() quoteView.getGlobalVisibleRect(r) @@ -124,34 +125,34 @@ class VisibleMessageContentView : LinearLayout { voiceMessageView.indexInAdapter = indexInAdapter voiceMessageView.delegate = context as? ConversationActivityV2 voiceMessageView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster) - mainContainer.addView(voiceMessageView) + binding.mainContainer.addView(voiceMessageView) // We have to use onContentClick (rather than a click listener directly on the voice // message view) so as to not interfere with all the other gestures. onContentClick = { voiceMessageView.togglePlayback() } onContentDoubleTap = { voiceMessageView.handleDoubleTap() } } else { val untrustedView = UntrustedAttachmentView(context) - untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message)) - mainContainer.addView(untrustedView) + untrustedView.bind(UntrustedAttachmentView.AttachmentType.AUDIO, getTextColor(context,message)) + binding.mainContainer.addView(untrustedView) onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } } } else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) { // Document attachment if (contactIsTrusted || message.isOutgoing) { val documentView = DocumentView(context) - documentView.bind(message, VisibleMessageContentView.getTextColor(context, message)) - mainContainer.addView(documentView) + documentView.bind(message, getTextColor(context, message)) + binding.mainContainer.addView(documentView) } else { val untrustedView = UntrustedAttachmentView(context) - untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message)) - mainContainer.addView(untrustedView) + untrustedView.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, getTextColor(context,message)) + binding.mainContainer.addView(untrustedView) onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } } } else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) { // Images/Video attachment if (contactIsTrusted || message.isOutgoing) { val albumThumbnailView = AlbumThumbnailView(context) - mainContainer.addView(albumThumbnailView) + binding.mainContainer.addView(albumThumbnailView) // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups // bind after add view because views are inflated and calculated during bind albumThumbnailView.bind( @@ -165,18 +166,18 @@ class VisibleMessageContentView : LinearLayout { } } else { val untrustedView = UntrustedAttachmentView(context) - untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message)) - mainContainer.addView(untrustedView) + untrustedView.bind(UntrustedAttachmentView.AttachmentType.MEDIA, getTextColor(context,message)) + binding.mainContainer.addView(untrustedView) onContentClick = { untrustedView.showTrustDialog(message.individualRecipient) } } } else if (message.isOpenGroupInvitation) { val openGroupInvitationView = OpenGroupInvitationView(context) - openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message)) - mainContainer.addView(openGroupInvitationView) + openGroupInvitationView.bind(message, getTextColor(context, message)) + binding.mainContainer.addView(openGroupInvitationView) onContentClick = { openGroupInvitationView.joinOpenGroup() } } else { - val bodyTextView = VisibleMessageContentView.getBodyTextView(context, message, searchQuery) - mainContainer.addView(bodyTextView) + val bodyTextView = getBodyTextView(context, message, searchQuery) + binding.mainContainer.addView(bodyTextView) onContentClick = { event -> // intersectedModalSpans should only be a list of one item bodyTextView.getIntersectedModalSpans(event).forEach { span -> @@ -188,21 +189,33 @@ class VisibleMessageContentView : LinearLayout { private fun getBackground(isOutgoing: Boolean, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean): Drawable { val isSingleMessage = (isStartOfMessageCluster && isEndOfMessageCluster) - @DrawableRes val backgroundID: Int - if (isSingleMessage) { - backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone - } else if (isStartOfMessageCluster) { - backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start - } else if (isEndOfMessageCluster) { - backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end - } else { - backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle + @DrawableRes val backgroundID = when { + isSingleMessage -> { + if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone + } + isStartOfMessageCluster -> { + if (isOutgoing) R.drawable.message_bubble_background_sent_start else R.drawable.message_bubble_background_received_start + } + isEndOfMessageCluster -> { + if (isOutgoing) R.drawable.message_bubble_background_sent_end else R.drawable.message_bubble_background_received_end + } + else -> { + if (isOutgoing) R.drawable.message_bubble_background_sent_middle else R.drawable.message_bubble_background_received_middle + } } return ResourcesCompat.getDrawable(resources, backgroundID, context.theme)!! } fun recycle() { - mainContainer.removeAllViews() + binding.mainContainer.removeAllViews() + } + + fun playVoiceMessage() { + binding.mainContainer.children.forEach { view -> + if (view is VoiceMessageView) { + return@forEach view.togglePlayback() + } + } } // endregion @@ -227,8 +240,10 @@ class VisibleMessageContentView : LinearLayout { var body = message.body.toSpannable() body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context) - body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { BackgroundColorSpan(Color.WHITE) }, body, searchQuery) - body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery) + body = SearchUtil.getHighlightedSpan(Locale.getDefault(), + { BackgroundColorSpan(Color.WHITE) }, body, searchQuery) + body = SearchUtil.getHighlightedSpan(Locale.getDefault(), + { ForegroundColorSpan(Color.BLACK) }, body, searchQuery) Linkify.addLinks(body, Linkify.WEB_URLS) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 802409cbe..fc858bb25 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -5,12 +5,15 @@ import android.content.res.Resources import android.graphics.Canvas import android.graphics.Rect import android.graphics.drawable.ColorDrawable -import android.os.Build -import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.AttributeSet -import android.view.* +import android.view.Gravity +import android.view.HapticFeedbackConstants +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup import android.widget.LinearLayout import android.widget.RelativeLayout import androidx.appcompat.app.AppCompatActivity @@ -19,25 +22,35 @@ import androidx.core.content.res.ResourcesCompat import androidx.core.os.bundleOf import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint -import kotlinx.android.synthetic.main.view_visible_message.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewVisibleMessageBinding import org.session.libsession.messaging.contacts.Contact.ContactContext import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.utilities.ViewUtil -import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.ThreadUtils import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.database.* +import org.thoughtcrime.securesms.database.LokiThreadDatabase +import org.thoughtcrime.securesms.database.MmsDatabase +import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.SessionContactDatabase +import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.home.UserDetailsBottomSheet import org.thoughtcrime.securesms.mms.GlideRequests -import org.thoughtcrime.securesms.util.* -import java.util.* +import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.disableClipping +import org.thoughtcrime.securesms.util.getColorWithID +import org.thoughtcrime.securesms.util.toDp +import org.thoughtcrime.securesms.util.toPx +import java.util.Date +import java.util.Locale import javax.inject.Inject import kotlin.math.abs import kotlin.math.min import kotlin.math.roundToInt import kotlin.math.sqrt + @AndroidEntryPoint class VisibleMessageView : LinearLayout { @@ -48,6 +61,7 @@ class VisibleMessageView : LinearLayout { @Inject lateinit var smsDb: SmsDatabase @Inject lateinit var mmsDb: MmsDatabase + private lateinit var binding: ViewVisibleMessageBinding private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate() private val swipeToReplyIconRect = Rect() @@ -60,7 +74,11 @@ class VisibleMessageView : LinearLayout { private var onDoubleTap: (() -> Unit)? = null var indexInAdapter: Int = -1 var snIsSelected = false - set(value) { field = value; handleIsSelectedChanged()} + set(value) { + field = value + binding.messageTimestampTextView.isVisible = isSelected + handleIsSelectedChanged() + } var onPress: ((event: MotionEvent) -> Unit)? = null var onSwipeToReply: (() -> Unit)? = null var onLongPress: (() -> Unit)? = null @@ -68,7 +86,7 @@ class VisibleMessageView : LinearLayout { companion object { const val swipeToReplyThreshold = 64.0f // dp - const val longPressMovementTreshold = 10.0f // dp + const val longPressMovementThreshold = 10.0f // dp const val longPressDurationThreshold = 250L // ms const val maxDoubleTapInterval = 200L } @@ -79,12 +97,12 @@ class VisibleMessageView : LinearLayout { constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_visible_message, this) + binding = ViewVisibleMessageBinding.inflate(LayoutInflater.from(context), this, true) layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) isHapticFeedbackEnabled = true setWillNotDraw(false) - expirationTimerViewContainer.disableClipping() - messageContentContainer.disableClipping() + binding.expirationTimerViewContainer.disableClipping() + binding.messageContentContainer.disableClipping() } // endregion @@ -101,47 +119,46 @@ class VisibleMessageView : LinearLayout { // Show profile picture and sender name if this is a group thread AND // the message is incoming if (isGroupThread && !message.isOutgoing) { - profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE - profilePictureView.publicKey = senderSessionID - profilePictureView.glide = glide - profilePictureView.update(message.individualRecipient, threadID) - profilePictureView.setOnClickListener { + binding.profilePictureContainer.visibility = if (isEndOfMessageCluster) View.VISIBLE else View.INVISIBLE + binding.profilePictureView.publicKey = senderSessionID + binding.profilePictureView.glide = glide + binding.profilePictureView.update(message.individualRecipient) + binding.profilePictureView.setOnClickListener { showUserDetails(senderSessionID, threadID) } if (thread.isOpenGroupRecipient) { val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server) - moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE + binding.moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE } else { - moderatorIconImageView.visibility = View.INVISIBLE + binding.moderatorIconImageView.visibility = View.INVISIBLE } - senderNameTextView.isVisible = isStartOfMessageCluster + binding.senderNameTextView.isVisible = isStartOfMessageCluster val context = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR - senderNameTextView.text = contact?.displayName(context) ?: senderSessionID + binding.senderNameTextView.text = contact?.displayName(context) ?: senderSessionID } else { - profilePictureContainer.visibility = View.GONE - senderNameTextView.visibility = View.GONE + binding.profilePictureContainer.visibility = View.GONE + binding.senderNameTextView.visibility = View.GONE } // Date break - dateBreakTextView.showDateBreak(message, previous) + binding.dateBreakTextView.showDateBreak(message, previous) // Timestamp - messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) + binding.messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) // Margins - val startPadding: Int - if (isGroupThread) { - startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() else 0 + val startPadding = if (isGroupThread) { + if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() else 0 } else { - startPadding = if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() - else resources.getDimension(R.dimen.medium_spacing).toInt() + if (message.isOutgoing) resources.getDimension(R.dimen.very_large_spacing).toInt() + else resources.getDimension(R.dimen.medium_spacing).toInt() } val endPadding = if (message.isOutgoing) resources.getDimension(R.dimen.medium_spacing).toInt() else resources.getDimension(R.dimen.very_large_spacing).toInt() - messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0) + binding.messageContentContainer.setPaddingRelative(startPadding, 0, endPadding, 0) // Set inter-message spacing setMessageSpacing(isStartOfMessageCluster, isEndOfMessageCluster) // Gravity val gravity = if (message.isOutgoing) Gravity.END else Gravity.START - mainContainer.gravity = gravity or Gravity.BOTTOM + binding.mainContainer.gravity = gravity or Gravity.BOTTOM // Message status indicator val (iconID, iconColor) = getMessageStatusImage(message) if (iconID != null) { @@ -149,24 +166,24 @@ class VisibleMessageView : LinearLayout { if (iconColor != null) { drawable?.setTint(iconColor) } - messageStatusImageView.setImageDrawable(drawable) + binding.messageStatusImageView.setImageDrawable(drawable) } if (message.isOutgoing) { val lastMessageID = mmsSmsDb.getLastMessageID(message.threadId) - messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID + binding.messageStatusImageView.isVisible = !message.isSent || message.id == lastMessageID } else { - messageStatusImageView.isVisible = false + binding.messageStatusImageView.isVisible = false } // Expiration timer updateExpirationTimer(message) // Calculate max message bubble width var maxWidth = screenWidth - startPadding - endPadding - if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width } + if (binding.profilePictureContainer.visibility != View.GONE) { maxWidth -= binding.profilePictureContainer.width } // Populate content view - messageContentView.indexInAdapter = indexInAdapter - messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false)) - messageContentView.delegate = contentViewDelegate - onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() } + binding.messageContentView.indexInAdapter = indexInAdapter + binding.messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread, searchQuery, isGroupThread || (contact?.isTrusted ?: false)) + binding.messageContentView.delegate = contentViewDelegate + onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() } } private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { @@ -207,7 +224,7 @@ class VisibleMessageView : LinearLayout { } private fun updateExpirationTimer(message: MessageRecord) { - val expirationTimerViewLayoutParams = expirationTimerView.layoutParams as RelativeLayout.LayoutParams + val expirationTimerViewLayoutParams = binding.expirationTimerView.layoutParams as RelativeLayout.LayoutParams val ruleToAdd = if (message.isOutgoing) RelativeLayout.ALIGN_START else RelativeLayout.ALIGN_END val ruleToRemove = if (message.isOutgoing) RelativeLayout.ALIGN_END else RelativeLayout.ALIGN_START expirationTimerViewLayoutParams.removeRule(ruleToRemove) @@ -216,20 +233,20 @@ class VisibleMessageView : LinearLayout { val smallSpacing = resources.getDimension(R.dimen.small_spacing).roundToInt() expirationTimerViewLayoutParams.marginStart = if (message.isOutgoing) -(smallSpacing + expirationTimerViewSize) else 0 expirationTimerViewLayoutParams.marginEnd = if (message.isOutgoing) 0 else -(smallSpacing + expirationTimerViewSize) - expirationTimerView.layoutParams = expirationTimerViewLayoutParams + binding.expirationTimerView.layoutParams = expirationTimerViewLayoutParams if (message.expiresIn > 0 && !message.isPending) { - expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme)) - expirationTimerView.isVisible = true - expirationTimerView.setPercentComplete(0.0f) + binding.expirationTimerView.setColorFilter(ResourcesCompat.getColor(resources, R.color.text, context.theme)) + binding.expirationTimerView.isVisible = true + binding.expirationTimerView.setPercentComplete(0.0f) if (message.expireStarted > 0) { - expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn) - expirationTimerView.startAnimation() + binding.expirationTimerView.setExpirationTime(message.expireStarted, message.expiresIn) + binding.expirationTimerView.startAnimation() if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) { ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule() } } else if (!message.isMediaPending) { - expirationTimerView.setPercentComplete(0.0f) - expirationTimerView.stopAnimation() + binding.expirationTimerView.setPercentComplete(0.0f) + binding.expirationTimerView.stopAnimation() ThreadUtils.queue { val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager val id = message.getId() @@ -238,11 +255,11 @@ class VisibleMessageView : LinearLayout { expirationManager.scheduleDeletion(id, mms, message.expiresIn) } } else { - expirationTimerView.stopAnimation() - expirationTimerView.setPercentComplete(0.0f) + binding.expirationTimerView.stopAnimation() + binding.expirationTimerView.setPercentComplete(0.0f) } } else { - expirationTimerView.isVisible = false + binding.expirationTimerView.isVisible = false } } @@ -255,14 +272,14 @@ class VisibleMessageView : LinearLayout { } override fun onDraw(canvas: Canvas) { - if (translationX < 0 && !expirationTimerView.isVisible) { + if (translationX < 0 && !binding.expirationTimerView.isVisible) { val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) - val threshold = VisibleMessageView.swipeToReplyThreshold + val threshold = swipeToReplyThreshold val iconSize = toPx(24, context.resources) - val bottomVOffset = paddingBottom + messageStatusImageView.height + (messageContentView.height - iconSize) / 2 - swipeToReplyIconRect.left = messageContentContainer.right - messageContentContainer.paddingEnd + spacing + val bottomVOffset = paddingBottom + binding.messageStatusImageView.height + (binding.messageContentView.height - iconSize) / 2 + swipeToReplyIconRect.left = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + spacing swipeToReplyIconRect.top = height - bottomVOffset - iconSize - swipeToReplyIconRect.right = messageContentContainer.right - messageContentContainer.paddingEnd + iconSize + spacing + swipeToReplyIconRect.right = binding.messageContentContainer.right - binding.messageContentContainer.paddingEnd + iconSize + spacing swipeToReplyIconRect.bottom = height - bottomVOffset swipeToReplyIcon.bounds = swipeToReplyIconRect swipeToReplyIcon.alpha = (255.0f * (min(abs(translationX), threshold) / threshold)).roundToInt() @@ -274,8 +291,8 @@ class VisibleMessageView : LinearLayout { } fun recycle() { - profilePictureView.recycle() - messageContentView.recycle() + binding.profilePictureView.recycle() + binding.messageContentView.recycle() } // endregion @@ -296,13 +313,13 @@ class VisibleMessageView : LinearLayout { longPressCallback?.let { gestureHandler.removeCallbacks(it) } val newLongPressCallback = Runnable { onLongPress() } this.longPressCallback = newLongPressCallback - gestureHandler.postDelayed(newLongPressCallback, VisibleMessageView.longPressDurationThreshold) + gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold) onDownTimestamp = Date().time } private fun onMove(event: MotionEvent) { val translationX = toDp(event.rawX + dx, context.resources) - if (abs(translationX) < VisibleMessageView.longPressMovementTreshold || snIsSelected) { + if (abs(translationX) < longPressMovementThreshold || snIsSelected) { return } else { longPressCallback?.let { gestureHandler.removeCallbacks(it) } @@ -313,20 +330,16 @@ class VisibleMessageView : LinearLayout { val sign = -1.0f val x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign this.translationX = x - this.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving + binding.dateBreakTextView.translationX = -x // Bit of a hack to keep the date break text view from moving postInvalidate() // Ensure onDraw(canvas:) is called - if (abs(x) > VisibleMessageView.swipeToReplyThreshold && abs(previousTranslationX) < VisibleMessageView.swipeToReplyThreshold) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) - } else { - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - } + if (abs(x) > swipeToReplyThreshold && abs(previousTranslationX) < swipeToReplyThreshold) { + performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) } previousTranslationX = x } private fun onCancel(event: MotionEvent) { - if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) { + if (abs(translationX) > swipeToReplyThreshold) { onSwipeToReply?.invoke() } longPressCallback?.let { gestureHandler.removeCallbacks(it) } @@ -334,9 +347,9 @@ class VisibleMessageView : LinearLayout { } private fun onUp(event: MotionEvent) { - if (abs(translationX) > VisibleMessageView.swipeToReplyThreshold) { + if (abs(translationX) > swipeToReplyThreshold) { onSwipeToReply?.invoke() - } else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) { + } else if ((Date().time - onDownTimestamp) < longPressDurationThreshold) { longPressCallback?.let { gestureHandler.removeCallbacks(it) } val pressCallback = this.pressCallback if (pressCallback != null) { @@ -363,7 +376,7 @@ class VisibleMessageView : LinearLayout { } .start() // Bit of a hack to keep the date break text view from moving - dateBreakTextView.animate() + binding.dateBreakTextView.animate() .translationX(0.0f) .setDuration(150) .start() @@ -375,7 +388,7 @@ class VisibleMessageView : LinearLayout { } fun onContentClick(event: MotionEvent) { - messageContentView.onContentClick?.invoke(event) + binding.messageContentView.onContentClick?.invoke(event) } private fun onPress(event: MotionEvent) { @@ -393,5 +406,9 @@ class VisibleMessageView : LinearLayout { val activity = context as AppCompatActivity userDetailsBottomSheet.show(activity.supportFragmentManager, userDetailsBottomSheet.tag) } + + fun playVoiceMessage() { + binding.messageContentView.playVoiceMessage() + } // endregion } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt index 85f0bccc5..6fca9ce6b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -9,8 +9,8 @@ import android.widget.LinearLayout import android.widget.RelativeLayout import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint -import kotlinx.android.synthetic.main.view_voice_message.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewVoiceMessageBinding import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.thoughtcrime.securesms.audio.AudioSlidePlayer import org.thoughtcrime.securesms.components.CornerMask @@ -26,6 +26,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { @Inject lateinit var attachmentDb: AttachmentDatabase + private lateinit var binding: ViewVoiceMessageBinding private val cornerMask by lazy { CornerMask(this) } private var isPlaying = false set(value) { @@ -44,8 +45,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_voice_message, this) - voiceMessageViewDurationTextView.text = String.format("%01d:%02d", + binding = ViewVoiceMessageBinding.inflate(LayoutInflater.from(context), this, true) + binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d", TimeUnit.MILLISECONDS.toMinutes(0), TimeUnit.MILLISECONDS.toSeconds(0)) } @@ -54,7 +55,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { // region Updating fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { val audio = message.slideDeck.audioSlide!! - voiceMessageViewLoader.isVisible = audio.isInProgress + binding.voiceMessageViewLoader.isVisible = audio.isInProgress val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) cornerMask.setTopLeftRadius(cornerRadii[0]) cornerMask.setTopRightRadius(cornerRadii[1]) @@ -74,8 +75,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { attachmentDb.getAttachmentAudioExtras(attachment.attachmentId)?.let { audioExtras -> if (audioExtras.durationMs > 0) { duration = audioExtras.durationMs - voiceMessageViewDurationTextView.visibility = View.VISIBLE - voiceMessageViewDurationTextView.text = String.format("%01d:%02d", + binding.voiceMessageViewDurationTextView.visibility = View.VISIBLE + binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d", TimeUnit.MILLISECONDS.toMinutes(audioExtras.durationMs), TimeUnit.MILLISECONDS.toSeconds(audioExtras.durationMs)) } @@ -99,12 +100,12 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { private fun handleProgressChanged(progress: Double) { this.progress = progress - voiceMessageViewDurationTextView.text = String.format("%01d:%02d", + binding.voiceMessageViewDurationTextView.text = String.format("%01d:%02d", TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()), TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong())) - val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams + val layoutParams = binding.progressView.layoutParams as RelativeLayout.LayoutParams layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt() - progressView.layoutParams = layoutParams + binding.progressView.layoutParams = layoutParams } override fun onPlayerStop(player: AudioSlidePlayer) { @@ -118,7 +119,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { private fun renderIcon() { val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play - voiceMessagePlaybackImageView.setImageResource(iconID) + binding.voiceMessagePlaybackImageView.setImageResource(iconID) } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt index da8df0045..afed74b1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt @@ -5,11 +5,12 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout -import kotlinx.android.synthetic.main.view_search_bottom_bar.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewSearchBottomBarBinding class SearchBottomBar : LinearLayout { + private lateinit var binding: ViewSearchBottomBarBinding private var eventListener: EventListener? = null // region Lifecycle @@ -18,10 +19,10 @@ class SearchBottomBar : LinearLayout { constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_search_bottom_bar, this) + binding = ViewSearchBottomBarBinding.inflate(LayoutInflater.from(context), this, true) } - fun setData(position: Int, count: Int) { + fun setData(position: Int, count: Int) = with(binding) { searchProgressWheel.visibility = GONE searchUp.setOnClickListener { v: View? -> if (eventListener != null) { @@ -43,7 +44,7 @@ class SearchBottomBar : LinearLayout { } fun showLoading() { - searchProgressWheel.visibility = VISIBLE + binding.searchProgressWheel.visibility = VISIBLE } private fun setViewEnabled(view: View, enabled: Boolean) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt index 80b233c42..d01d86b6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt @@ -5,6 +5,7 @@ import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri import android.util.AttributeSet +import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import androidx.core.view.isVisible @@ -13,8 +14,8 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions -import kotlinx.android.synthetic.main.thumbnail_view.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ThumbnailViewBinding import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress import org.session.libsession.utilities.Util.equals import org.session.libsignal.utilities.ListenableFuture @@ -22,11 +23,13 @@ import org.session.libsignal.utilities.SettableFuture import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget import org.thoughtcrime.securesms.database.model.MmsMessageRecord -import org.thoughtcrime.securesms.mms.* import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri +import org.thoughtcrime.securesms.mms.GlideRequest +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.mms.Slide open class KThumbnailView: FrameLayout { - + private lateinit var binding: ThumbnailViewBinding companion object { private const val WIDTH = 0 private const val HEIGHT = 1 @@ -37,10 +40,10 @@ open class KThumbnailView: FrameLayout { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) } - private val image by lazy { thumbnail_image } - private val playOverlay by lazy { play_overlay } - val loadIndicator: View by lazy { thumbnail_load_indicator } - val downloadIndicator: View by lazy { thumbnail_download_icon } + private val image by lazy { binding.thumbnailImage } + private val playOverlay by lazy { binding.playOverlay } + val loadIndicator: View by lazy { binding.thumbnailLoadIndicator } + val downloadIndicator: View by lazy { binding.thumbnailDownloadIcon } private val dimensDelegate = ThumbnailDimensDelegate() @@ -48,7 +51,7 @@ open class KThumbnailView: FrameLayout { private var radius: Int = 0 private fun initialize(attrs: AttributeSet?) { - inflate(context, R.layout.thumbnail_view, this) + binding = ThumbnailViewBinding.inflate(LayoutInflater.from(context), this) if (attrs != null) { val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0) diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt new file mode 100644 index 000000000..6f26c6ae3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.dependencies + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.session.libsession.utilities.AppTextSecurePreferences +import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.repository.ConversationRepository +import org.thoughtcrime.securesms.repository.DefaultConversationRepository + +@Module +@InstallIn(SingletonComponent::class) +abstract class AppModule { + + @Binds + abstract fun bindTextSecurePreferences(preferences: AppTextSecurePreferences): TextSecurePreferences + + @Binds + abstract fun bindConversationRepository(repository: DefaultConversationRepository): ConversationRepository + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/dms/CreatePrivateChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/dms/CreatePrivateChatActivity.kt index c1971fa05..2aecad34b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dms/CreatePrivateChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dms/CreatePrivateChatActivity.kt @@ -9,16 +9,18 @@ import android.content.Intent import android.os.Bundle import android.text.InputType import android.util.TypedValue -import android.view.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentPagerAdapter -import kotlinx.android.synthetic.main.activity_create_private_chat.* -import kotlinx.android.synthetic.main.fragment_enter_public_key.* import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityCreatePrivateChatBinding +import network.loki.messenger.databinding.FragmentEnterPublicKeyBinding import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi import org.session.libsession.snode.SnodeAPI @@ -27,13 +29,13 @@ import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.PublicKeyValidation import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity - import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { + private lateinit var binding: ActivityCreatePrivateChatBinding private val adapter = CreatePrivateChatActivityAdapter(this) private var isKeyboardShowing = false set(value) { @@ -47,37 +49,36 @@ class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRC // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) + binding = ActivityCreatePrivateChatBinding.inflate(layoutInflater) // Set content view - setContentView(R.layout.activity_create_private_chat) + setContentView(binding.root) // Set title supportActionBar!!.title = resources.getString(R.string.activity_create_private_chat_title) // Set up view pager - viewPager.adapter = adapter - tabLayout.setupWithViewPager(viewPager) - rootLayout.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { - - override fun onGlobalLayout() { - val diff = rootLayout.rootView.height - rootLayout.height - val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics - val estimatedKeyboardHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics) - this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight) - } - }) + binding.viewPager.adapter = adapter + binding.tabLayout.setupWithViewPager(binding.viewPager) + binding.rootLayout.viewTreeObserver.addOnGlobalLayoutListener { + val diff = binding.rootLayout.rootView.height - binding.rootLayout.height + val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics + val estimatedKeyboardHeight = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics) + this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight) + } } // endregion // region Updating private fun showLoader() { - loader.visibility = View.VISIBLE - loader.animate().setDuration(150).alpha(1.0f).start() + binding.loader.visibility = View.VISIBLE + binding.loader.animate().setDuration(150).alpha(1.0f).start() } private fun hideLoader() { - loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { + binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) - loader.visibility = View.GONE + binding.loader.visibility = View.GONE } }) } @@ -156,6 +157,8 @@ private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatAc // region Enter Public Key Fragment class EnterPublicKeyFragment : Fragment() { + private lateinit var binding: FragmentEnterPublicKeyBinding + var isKeyboardShowing = false set(value) { field = value; handleIsKeyboardShowingChanged() } @@ -165,32 +168,34 @@ class EnterPublicKeyFragment : Fragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment_enter_public_key, container, false) + binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard - publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT) - publicKeyEditText.setOnEditorActionListener { v, actionID, _ -> - if (actionID == EditorInfo.IME_ACTION_DONE) { - val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(v.windowToken, 0) - createPrivateChatIfPossible() - true - } else { - false + with(binding) { + publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard + publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT) + publicKeyEditText.setOnEditorActionListener { v, actionID, _ -> + if (actionID == EditorInfo.IME_ACTION_DONE) { + val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v.windowToken, 0) + createPrivateChatIfPossible() + true + } else { + false + } } + publicKeyTextView.text = hexEncodedPublicKey + copyButton.setOnClickListener { copyPublicKey() } + shareButton.setOnClickListener { sharePublicKey() } + createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() } } - publicKeyTextView.text = hexEncodedPublicKey - copyButton.setOnClickListener { copyPublicKey() } - shareButton.setOnClickListener { sharePublicKey() } - createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() } } private fun handleIsKeyboardShowingChanged() { - val optionalContentContainer = optionalContentContainer ?: return - optionalContentContainer.isVisible = !isKeyboardShowing + binding.optionalContentContainer.isVisible = !isKeyboardShowing } private fun copyPublicKey() { @@ -209,7 +214,7 @@ class EnterPublicKeyFragment : Fragment() { } private fun createPrivateChatIfPossible() { - val hexEncodedPublicKey = publicKeyEditText.text?.trim().toString() + val hexEncodedPublicKey = binding.publicKeyEditText.text?.trim().toString() val activity = requireActivity() as CreatePrivateChatActivity activity.createPrivateChatIfPossible(hexEncodedPublicKey) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupEditingOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupEditingOptionsBottomSheet.kt index f5a75041a..4991d3098 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupEditingOptionsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupEditingOptionsBottomSheet.kt @@ -1,22 +1,23 @@ package org.thoughtcrime.securesms.groups import android.os.Bundle -import com.google.android.material.bottomsheet.BottomSheetDialogFragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import kotlinx.android.synthetic.main.fragment_closed_group_edit_bottom_sheet.* -import network.loki.messenger.R +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import network.loki.messenger.databinding.FragmentClosedGroupEditBottomSheetBinding -public class ClosedGroupEditingOptionsBottomSheet : BottomSheetDialogFragment() { +class ClosedGroupEditingOptionsBottomSheet : BottomSheetDialogFragment() { + private lateinit var binding: FragmentClosedGroupEditBottomSheetBinding var onRemoveTapped: (() -> Unit)? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_closed_group_edit_bottom_sheet, container, false) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentClosedGroupEditBottomSheetBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - removeFromGroup.setOnClickListener { onRemoveTapped?.invoke() } + binding.removeFromGroup.setOnClickListener { onRemoveTapped?.invoke() } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateClosedGroupActivity.kt index 85c0bd3dd..63894344d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateClosedGroupActivity.kt @@ -10,8 +10,8 @@ import android.widget.Toast import androidx.loader.app.LoaderManager import androidx.loader.content.Loader import androidx.recyclerview.widget.LinearLayoutManager -import kotlinx.android.synthetic.main.activity_create_closed_group.* import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityCreateClosedGroupBinding import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi import org.session.libsession.messaging.sending_receiving.MessageSender @@ -28,8 +28,8 @@ import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.util.fadeIn import org.thoughtcrime.securesms.util.fadeOut -//TODO Refactor to avoid using kotlinx.android.synthetic class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks> { + private lateinit var binding: ActivityCreateClosedGroupBinding private var isLoading = false set(newValue) { field = newValue; invalidateOptionsMenu() } private var members = listOf() @@ -50,11 +50,12 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) - setContentView(R.layout.activity_create_closed_group) + binding = ActivityCreateClosedGroupBinding.inflate(layoutInflater) + setContentView(binding.root) supportActionBar!!.title = resources.getString(R.string.activity_create_closed_group_title) - recyclerView.adapter = this.selectContactsAdapter - recyclerView.layoutManager = LinearLayoutManager(this) - createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() } + binding.recyclerView.adapter = this.selectContactsAdapter + binding.recyclerView.layoutManager = LinearLayoutManager(this) + binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() } LoaderManager.getInstance(this).initLoader(0, null, this) } @@ -80,8 +81,8 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM private fun update(members: List) { //if there is a Note to self conversation, it loads self in the list, so we need to remove it here this.members = members.minus(publicKey) - mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE - emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE + binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE + binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE invalidateOptionsMenu() } // endregion @@ -95,12 +96,12 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM } private fun createNewPrivateChat() { - setResult(Companion.closedGroupCreatedResultCode) + setResult(closedGroupCreatedResultCode) finish() } private fun createClosedGroup() { - val name = nameEditText.text.trim() + val name = binding.nameEditText.text.trim() if (name.isEmpty()) { return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show() } @@ -116,9 +117,9 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM } val userPublicKey = TextSecurePreferences.getLocalNumber(this)!! isLoading = true - loaderContainer.fadeIn() + binding.loaderContainer.fadeIn() MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> - loaderContainer.fadeOut() + binding.loaderContainer.fadeOut() isLoading = false val threadID = DatabaseComponent.get(this).threadDatabase().getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) if (!isFinishing) { @@ -126,7 +127,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM finish() } }.failUi { - loaderContainer.fadeOut() + binding.loaderContainer.fadeOut() isLoading = false Toast.makeText(this, it.message, Toast.LENGTH_LONG).show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt index 5ffe42492..62e762316 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt @@ -8,12 +8,14 @@ import android.view.MenuItem import android.view.View import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager -import android.widget.* +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast import androidx.loader.app.LoaderManager import androidx.loader.content.Loader import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.activity_settings.* import network.loki.messenger.R import nl.komponents.kovenant.Promise import nl.komponents.kovenant.task diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt index 5d6d559a8..92802632b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt @@ -13,62 +13,63 @@ import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.view.isVisible -import androidx.fragment.app.* +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentPagerAdapter +import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import com.google.android.material.chip.Chip -import kotlinx.android.synthetic.main.activity_join_public_chat.* -import kotlinx.android.synthetic.main.fragment_enter_chat_url.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityJoinPublicChatBinding +import network.loki.messenger.databinding.FragmentEnterChatUrlBinding import okhttp3.HttpUrl import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup import org.session.libsession.utilities.Address -import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.PublicKeyValidation import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 -import org.thoughtcrime.securesms.groups.DefaultGroupsViewModel -import org.thoughtcrime.securesms.groups.GroupManager -import org.thoughtcrime.securesms.groups.OpenGroupManager +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.State -import java.util.* +import java.util.Locale class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { + private lateinit var binding: ActivityJoinPublicChatBinding private val adapter = JoinPublicChatActivityAdapter(this) // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) + binding = ActivityJoinPublicChatBinding.inflate(layoutInflater) // Set content view - setContentView(R.layout.activity_join_public_chat) + setContentView(binding.root) // Set title supportActionBar!!.title = resources.getString(R.string.activity_join_public_chat_title) // Set up view pager - viewPager.adapter = adapter - tabLayout.setupWithViewPager(viewPager) + binding.viewPager.adapter = adapter + binding.tabLayout.setupWithViewPager(binding.viewPager) } // endregion // region Updating private fun showLoader() { - loader.visibility = View.VISIBLE - loader.animate().setDuration(150).alpha(1.0f).start() + binding.loader.visibility = View.VISIBLE + binding.loader.animate().setDuration(150).alpha(1.0f).start() } private fun hideLoader() { - loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { + binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) - loader.visibility = View.GONE + binding.loader.visibility = View.GONE } }) } @@ -166,26 +167,28 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity // region Enter Chat URL Fragment class EnterChatURLFragment : Fragment() { + private lateinit var binding: FragmentEnterChatUrlBinding private val viewModel by activityViewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment_enter_chat_url, container, false) + binding = FragmentEnterChatUrlBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard - joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } + binding.chatURLEditText.imeOptions = binding.chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard + binding.joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } viewModel.defaultRooms.observe(viewLifecycleOwner) { state -> - defaultRoomsContainer.isVisible = state is State.Success - defaultRoomsLoaderContainer.isVisible = state is State.Loading - defaultRoomsLoader.isVisible = state is State.Loading + binding.defaultRoomsContainer.isVisible = state is State.Success + binding.defaultRoomsLoaderContainer.isVisible = state is State.Loading + binding.defaultRoomsLoader.isVisible = state is State.Loading when (state) { State.Loading -> { - // TODO: Show a loader + // TODO: Show a binding.loader } is State.Error -> { - // TODO: Hide the loader + // TODO: Hide the binding.loader } is State.Success -> { populateDefaultGroups(state.value) @@ -195,10 +198,10 @@ class EnterChatURLFragment : Fragment() { } private fun populateDefaultGroups(groups: List) { - defaultRoomsGridLayout.removeAllViews() - defaultRoomsGridLayout.useDefaultMargins = false + binding.defaultRoomsGridLayout.removeAllViews() + binding.defaultRoomsGridLayout.useDefaultMargins = false groups.forEach { defaultGroup -> - val chip = layoutInflater.inflate(R.layout.default_group_chip, defaultRoomsGridLayout, false) as Chip + val chip = layoutInflater.inflate(R.layout.default_group_chip, binding.defaultRoomsGridLayout, false) as Chip val drawable = defaultGroup.image?.let { bytes -> val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size) RoundedBitmapDrawableFactory.create(resources,bitmap).apply { @@ -210,18 +213,18 @@ class EnterChatURLFragment : Fragment() { chip.setOnClickListener { (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL) } - defaultRoomsGridLayout.addView(chip) + binding.defaultRoomsGridLayout.addView(chip) } if ((groups.size and 1) != 0) { // This checks that the number of rooms is even - layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout) + layoutInflater.inflate(R.layout.grid_layout_filler, binding.defaultRoomsGridLayout) } } // region Convenience private fun joinPublicChatIfPossible() { val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0) - val chatURL = chatURLEditText.text.trim().toString().toLowerCase(Locale.US) + inputMethodManager.hideSoftInputFromWindow(binding.chatURLEditText.windowToken, 0) + val chatURL = binding.chatURLEditText.text.trim().toString().toLowerCase(Locale.US) (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL) } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupGuidelinesActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupGuidelinesActivity.kt index 3b7827679..775c46768 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupGuidelinesActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupGuidelinesActivity.kt @@ -1,16 +1,16 @@ package org.thoughtcrime.securesms.groups import android.os.Bundle -import kotlinx.android.synthetic.main.activity_open_group_guidelines.* -import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityOpenGroupGuidelinesBinding import org.thoughtcrime.securesms.BaseActionBarActivity class OpenGroupGuidelinesActivity : BaseActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_open_group_guidelines) - communityGuidelinesTextView.text = """ + val binding = ActivityOpenGroupGuidelinesBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.communityGuidelinesTextView.text = """ Welcome to Oxen. Oxen believes privacy is an important part of our future. People have been safeguarding the right to privacy since the dawn of humanity, but the digital world has turned privacy into a privilege. Enough is enough. We're taking it back. For you. For us. For everyone. diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt index 40f41d23d..1d5580be3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt @@ -6,13 +6,12 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.* -import network.loki.messenger.R +import network.loki.messenger.databinding.FragmentConversationBottomSheetBinding import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.util.UiModeUtilities -public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener { - +class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener { + private lateinit var binding: FragmentConversationBottomSheetBinding //FIXME AC: Supplying a threadRecord directly into the field from an activity // is not the best idea. It doesn't survive configuration change. // We should be dealing with IDs and all sorts of serializable data instead @@ -29,20 +28,21 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View. var onSetMuteTapped: ((Boolean) -> Unit)? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_conversation_bottom_sheet, container, false) + binding = FragmentConversationBottomSheetBinding.inflate(inflater, container, false) + return binding.root } override fun onClick(v: View?) { when (v) { - detailsTextView -> onViewDetailsTapped?.invoke() - pinTextView -> onPinTapped?.invoke() - unpinTextView -> onUnpinTapped?.invoke() - blockTextView -> onBlockTapped?.invoke() - unblockTextView -> onUnblockTapped?.invoke() - deleteTextView -> onDeleteTapped?.invoke() - notificationsTextView -> onNotificationTapped?.invoke() - unMuteNotificationsTextView -> onSetMuteTapped?.invoke(false) - muteNotificationsTextView -> onSetMuteTapped?.invoke(true) + binding.detailsTextView -> onViewDetailsTapped?.invoke() + binding.pinTextView -> onPinTapped?.invoke() + binding.unpinTextView -> onUnpinTapped?.invoke() + binding.blockTextView -> onBlockTapped?.invoke() + binding.unblockTextView -> onUnblockTapped?.invoke() + binding.deleteTextView -> onDeleteTapped?.invoke() + binding.notificationsTextView -> onNotificationTapped?.invoke() + binding.unMuteNotificationsTextView -> onSetMuteTapped?.invoke(false) + binding.muteNotificationsTextView -> onSetMuteTapped?.invoke(true) } } @@ -51,26 +51,26 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View. if (!this::thread.isInitialized) { return dismiss() } val recipient = thread.recipient if (!recipient.isGroupRecipient && !recipient.isLocalNumber) { - detailsTextView.visibility = View.VISIBLE - unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE - blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE - detailsTextView.setOnClickListener(this) - blockTextView.setOnClickListener(this) - unblockTextView.setOnClickListener(this) + binding.detailsTextView.visibility = View.VISIBLE + binding.unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE + binding.blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE + binding.detailsTextView.setOnClickListener(this) + binding.blockTextView.setOnClickListener(this) + binding.unblockTextView.setOnClickListener(this) } else { - detailsTextView.visibility = View.GONE + binding.detailsTextView.visibility = View.GONE } - unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber - muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber - unMuteNotificationsTextView.setOnClickListener(this) - muteNotificationsTextView.setOnClickListener(this) - notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted - notificationsTextView.setOnClickListener(this) - deleteTextView.setOnClickListener(this) - pinTextView.isVisible = !thread.isPinned - unpinTextView.isVisible = thread.isPinned - pinTextView.setOnClickListener(this) - unpinTextView.setOnClickListener(this) + binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber + binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber + binding.unMuteNotificationsTextView.setOnClickListener(this) + binding.muteNotificationsTextView.setOnClickListener(this) + binding.notificationsTextView.isVisible = recipient.isGroupRecipient && !recipient.isMuted + binding.notificationsTextView.setOnClickListener(this) + binding.deleteTextView.setOnClickListener(this) + binding.pinTextView.isVisible = !thread.isPinned + binding.unpinTextView.isVisible = thread.isPinned + binding.pinTextView.setOnClickListener(this) + binding.unpinTextView.setOnClickListener(this) } override fun onStart() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 679b26d6d..0cd1ad40b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -11,8 +11,8 @@ import android.widget.LinearLayout import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.view_conversation.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewConversationBinding import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions import org.thoughtcrime.securesms.database.RecipientDatabase @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.util.DateUtils import java.util.Locale class ConversationView : LinearLayout { + private lateinit var binding: ViewConversationBinding private val screenWidth = Resources.getSystem().displayMetrics.widthPixels var thread: ThreadRecord? = null @@ -31,7 +32,7 @@ class ConversationView : LinearLayout { constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } private fun initialize() { - LayoutInflater.from(context).inflate(R.layout.view_conversation, this) + binding = ViewConversationBinding.inflate(LayoutInflater.from(context), this, true) layoutParams = RecyclerView.LayoutParams(screenWidth, RecyclerView.LayoutParams.WRAP_CONTENT) } // endregion @@ -39,83 +40,83 @@ class ConversationView : LinearLayout { // region Updating fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) { this.thread = thread - if (thread.isPinned) { - conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0) - background = ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background) + background = if (thread.isPinned) { + binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_pin, 0) + ContextCompat.getDrawable(context, R.drawable.conversation_pinned_background) } else { - conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) - background = ContextCompat.getDrawable(context, R.drawable.conversation_view_background) + binding.conversationViewDisplayNameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) + ContextCompat.getDrawable(context, R.drawable.conversation_view_background) } - profilePictureView.glide = glide + binding.profilePictureView.glide = glide val unreadCount = thread.unreadCount if (thread.recipient.isBlocked) { - accentView.setBackgroundResource(R.color.destructive) - accentView.visibility = View.VISIBLE + binding.accentView.setBackgroundResource(R.color.destructive) + binding.accentView.visibility = View.VISIBLE } else { - accentView.setBackgroundResource(R.color.accent) + binding.accentView.setBackgroundResource(R.color.accent) // Using thread.isRead we can determine if the last message was our own, and display it as 'read' even though previous messages may not be // This would also not trigger the disappearing message timer which may or may not be desirable - accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE + binding.accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE } val formattedUnreadCount = if (thread.isRead) { null } else { if (unreadCount < 100) unreadCount.toString() else "99+" } - unreadCountTextView.text = formattedUnreadCount + binding.unreadCountTextView.text = formattedUnreadCount val textSize = if (unreadCount < 100) 12.0f else 9.0f - unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) - unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL) - unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead) + binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) + binding.unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL) + binding.unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead) val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString() - conversationViewDisplayNameTextView.text = senderDisplayName - timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date) + binding.conversationViewDisplayNameTextView.text = senderDisplayName + binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date) val recipient = thread.recipient - muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL + binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) { R.drawable.ic_outline_notifications_off_24 } else { R.drawable.ic_notifications_mentions } - muteIndicatorImageView.setImageResource(drawableRes) + binding.muteIndicatorImageView.setImageResource(drawableRes) val rawSnippet = thread.getDisplayBody(context) val snippet = highlightMentions(rawSnippet, thread.threadId, context) - snippetTextView.text = snippet - snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT - snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE + binding.snippetTextView.text = snippet + binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT + binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE if (isTyping) { - typingIndicatorView.startAnimation() + binding.typingIndicatorView.startAnimation() } else { - typingIndicatorView.stopAnimation() + binding.typingIndicatorView.stopAnimation() } - typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE - statusIndicatorImageView.visibility = View.VISIBLE + binding.typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE + binding.statusIndicatorImageView.visibility = View.VISIBLE when { - !thread.isOutgoing -> statusIndicatorImageView.visibility = View.GONE + !thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE thread.isFailed -> { val drawable = ContextCompat.getDrawable(context, R.drawable.ic_error)?.mutate() drawable?.setTint(ContextCompat.getColor(context, R.color.destructive)) - statusIndicatorImageView.setImageDrawable(drawable) + binding.statusIndicatorImageView.setImageDrawable(drawable) } - thread.isPending -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot) - thread.isRead -> statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check) - else -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check) + thread.isPending -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot) + thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check) + else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check) } post { - profilePictureView.update(thread.recipient, thread.threadId) + binding.profilePictureView.update(thread.recipient) } } fun recycle() { - profilePictureView.recycle() + binding.profilePictureView.recycle() } private fun getUserDisplayName(recipient: Recipient): String? { - if (recipient.isLocalNumber) { - return context.getString(R.string.note_to_self) + return if (recipient.isLocalNumber) { + context.getString(R.string.note_to_self) } else { - return recipient.name // Internally uses the Contact API + recipient.name // Internally uses the Contact API } } // endregion 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 6373aaca1..a1a4a22ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -10,7 +10,6 @@ import android.os.Bundle import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan -import android.view.View import android.widget.Toast import androidx.core.os.bundleOf import androidx.core.view.isVisible @@ -19,24 +18,22 @@ import androidx.lifecycle.lifecycleScope import androidx.loader.app.LoaderManager import androidx.loader.content.Loader import androidx.localbroadcastmanager.content.LocalBroadcastManager -import androidx.recyclerview.widget.LinearLayoutManager import dagger.hilt.android.AndroidEntryPoint -import kotlinx.android.synthetic.main.activity_home.* -import kotlinx.android.synthetic.main.seed_reminder_stub.* -import kotlinx.android.synthetic.main.seed_reminder_stub.view.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityHomeBinding +import network.loki.messenger.databinding.SeedReminderStubBinding import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.sending_receiving.MessageSender -import org.session.libsession.utilities.* -import org.session.libsession.utilities.Util -import org.session.libsignal.utilities.ThreadUtils +import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.ProfilePictureModifiedEvent +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.MuteDialog @@ -58,13 +55,20 @@ import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.onboarding.SeedActivity import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.preferences.SettingsActivity -import org.thoughtcrime.securesms.util.* +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import org.thoughtcrime.securesms.util.IP2Country +import org.thoughtcrime.securesms.util.disableClipping +import org.thoughtcrime.securesms.util.getColorWithID +import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.util.show import java.io.IOException import javax.inject.Inject @AndroidEntryPoint class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate, LoaderManager.LoaderCallbacks { + + private lateinit var binding: ActivityHomeBinding private lateinit var glide: GlideRequests private var broadcastReceiver: BroadcastReceiver? = null @@ -75,57 +79,57 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis private val publicKey: String get() = TextSecurePreferences.getLocalNumber(this)!! - private val homeAdapter:HomeAdapter by lazy { - HomeAdapter(this, threadDb.conversationList) + private val homeAdapter: HomeAdapter by lazy { + HomeAdapter(context = this, cursor = threadDb.conversationList, listener = this) } // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) // Set content view - setContentView(R.layout.activity_home) + binding = ActivityHomeBinding.inflate(layoutInflater) + setContentView(binding.root) // Set custom toolbar - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) // Set up Glide glide = GlideApp.with(this) // Set up toolbar buttons - profileButton.glide = glide - profileButton.setOnClickListener { openSettings() } - pathStatusViewContainer.disableClipping() - pathStatusViewContainer.setOnClickListener { showPath() } + binding.profileButton.glide = glide + binding.profileButton.setOnClickListener { openSettings() } + binding.pathStatusViewContainer.disableClipping() + binding.pathStatusViewContainer.setOnClickListener { showPath() } // Set up seed reminder view val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) if (!hasViewedSeed) { - seedReminderStub.inflate().apply { - val seedReminderView = this.seedReminderView + binding.seedReminderStub.setOnInflateListener { _, inflated -> + val stubBinding = SeedReminderStubBinding.bind(inflated) val seedReminderViewTitle = SpannableString("You're almost finished! 80%") // Intentionally not yet translated seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - seedReminderView.title = seedReminderViewTitle - seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1) - seedReminderView.setProgress(80, false) - seedReminderView.delegate = this@HomeActivity + stubBinding.seedReminderView.title = seedReminderViewTitle + stubBinding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1) + stubBinding.seedReminderView.setProgress(80, false) + stubBinding.seedReminderView.delegate = this@HomeActivity } + binding.seedReminderStub.inflate() } else { - seedReminderStub.isVisible = false + binding.seedReminderStub.isVisible = false } // Set up recycler view homeAdapter.setHasStableIds(true) homeAdapter.glide = glide - homeAdapter.conversationClickListener = this - recyclerView.adapter = homeAdapter - recyclerView.layoutManager = LinearLayoutManager(this) + binding.recyclerView.adapter = homeAdapter // Set up empty state view - createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() } + binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() } IP2Country.configureIfNeeded(this@HomeActivity) // This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will) LoaderManager.getInstance(this).restartLoader(0, null, this) // Set up new conversation button set - newConversationButtonSet.delegate = this + binding.newConversationButtonSet.delegate = this // Observe blocked contacts changed events val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - recyclerView.adapter!!.notifyDataSetChanged() + binding.recyclerView.adapter!!.notifyDataSetChanged() } } this.broadcastReceiver = broadcastReceiver @@ -138,7 +142,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis // Set up typing observer withContext(Dispatchers.Main) { ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this@HomeActivity, Observer> { threadIDs -> - val adapter = recyclerView.adapter as HomeAdapter + val adapter = binding.recyclerView.adapter as HomeAdapter adapter.typingThreadIDs = threadIDs ?: setOf() }) updateProfileButton() @@ -177,11 +181,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true) if (TextSecurePreferences.getLocalNumber(this) == null) { return; } // This can be the case after a secondary device is auto-cleared IdentityKeyUtil.checkUpdate(this) - profileButton.recycle() // clear cached image before update tje profilePictureView - profileButton.update() + binding.profileButton.recycle() // clear cached image before update tje profilePictureView + binding.profileButton.update() val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this) if (hasViewedSeed) { - seedReminderView?.isVisible = false + binding.seedReminderStub.isVisible = false } if (TextSecurePreferences.getConfigurationMessageSynced(this)) { lifecycleScope.launch(Dispatchers.IO) { @@ -214,8 +218,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis // region Updating private fun updateEmptyState() { - val threadCount = (recyclerView.adapter as HomeAdapter).itemCount - emptyStateContainer.visibility = if (threadCount == 0) View.VISIBLE else View.GONE + val threadCount = (binding.recyclerView.adapter as HomeAdapter).itemCount + binding.emptyStateContainer.isVisible = threadCount == 0 } @Subscribe(threadMode = ThreadMode.MAIN) @@ -226,10 +230,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis } private fun updateProfileButton() { - profileButton.publicKey = publicKey - profileButton.displayName = TextSecurePreferences.getProfileName(this) - profileButton.recycle() - profileButton.update() + binding.profileButton.publicKey = publicKey + binding.profileButton.displayName = TextSecurePreferences.getProfileName(this) + binding.profileButton.recycle() + binding.profileButton.update() } // endregion @@ -239,13 +243,13 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis show(intent) } - override fun onConversationClick(view: ConversationView) { - val thread = view.thread ?: return - openConversation(thread) + override fun onConversationClick(thread: ThreadRecord) { + val intent = Intent(this, ConversationActivityV2::class.java) + intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId) + push(intent) } - override fun onLongConversationClick(view: ConversationView) { - val thread = view.thread ?: return + override fun onLongConversationClick(thread: ThreadRecord) { val bottomSheet = ConversationOptionsBottomSheet() bottomSheet.thread = thread bottomSheet.onViewDetailsTapped = { @@ -286,15 +290,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis } bottomSheet.onPinTapped = { bottomSheet.dismiss() - if (!thread.isPinned) { - pinConversation(thread) - } + setConversationPinned(thread.threadId, true) } bottomSheet.onUnpinTapped = { bottomSheet.dismiss() - if (thread.isPinned) { - unpinConversation(thread) - } + setConversationPinned(thread.threadId, false) } bottomSheet.show(supportFragmentManager, bottomSheet.tag) } @@ -305,10 +305,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis .setMessage(R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.RecipientPreferenceActivity_block) { dialog, _ -> - ThreadUtils.queue { + lifecycleScope.launch(Dispatchers.IO) { recipientDatabase.setBlocked(thread.recipient, true) - Util.runOnMain { - recyclerView.adapter!!.notifyDataSetChanged() + withContext(Dispatchers.Main) { + binding.recyclerView.adapter!!.notifyDataSetChanged() dialog.dismiss() } } @@ -321,10 +321,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis .setMessage(R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(R.string.RecipientPreferenceActivity_unblock) { dialog, _ -> - ThreadUtils.queue { + lifecycleScope.launch(Dispatchers.IO) { recipientDatabase.setBlocked(thread.recipient, false) - Util.runOnMain { - recyclerView.adapter!!.notifyDataSetChanged() + withContext(Dispatchers.Main) { + binding.recyclerView.adapter!!.notifyDataSetChanged() dialog.dismiss() } } @@ -333,18 +333,18 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis private fun setConversationMuted(thread: ThreadRecord, isMuted: Boolean) { if (!isMuted) { - ThreadUtils.queue { + lifecycleScope.launch(Dispatchers.IO) { recipientDatabase.setMuted(thread.recipient, 0) - Util.runOnMain { - recyclerView.adapter!!.notifyDataSetChanged() + withContext(Dispatchers.Main) { + binding.recyclerView.adapter!!.notifyDataSetChanged() } } } else { MuteDialog.show(this) { until: Long -> - ThreadUtils.queue { + lifecycleScope.launch(Dispatchers.IO) { recipientDatabase.setMuted(thread.recipient, until) - Util.runOnMain { - recyclerView.adapter!!.notifyDataSetChanged() + withContext(Dispatchers.Main) { + binding.recyclerView.adapter!!.notifyDataSetChanged() } } } @@ -352,28 +352,19 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis } private fun setNotifyType(thread: ThreadRecord, newNotifyType: Int) { - ThreadUtils.queue { + lifecycleScope.launch(Dispatchers.IO) { recipientDatabase.setNotifyType(thread.recipient, newNotifyType) - Util.runOnMain { - recyclerView.adapter!!.notifyDataSetChanged() + withContext(Dispatchers.Main) { + binding.recyclerView.adapter!!.notifyDataSetChanged() } } } - private fun pinConversation(thread: ThreadRecord) { - ThreadUtils.queue { - threadDb.setPinned(thread.threadId, true) - Util.runOnMain { - LoaderManager.getInstance(this).restartLoader(0, null, this) - } - } - } - - private fun unpinConversation(thread: ThreadRecord) { - ThreadUtils.queue { - threadDb.setPinned(thread.threadId, false) - Util.runOnMain { - LoaderManager.getInstance(this).restartLoader(0, null, this) + private fun setConversationPinned(threadId: Long, pinned: Boolean) { + lifecycleScope.launch(Dispatchers.IO) { + threadDb.setPinned(threadId, pinned) + withContext(Dispatchers.Main) { + LoaderManager.getInstance(this@HomeActivity).restartLoader(0, null, this@HomeActivity) } } } @@ -381,16 +372,15 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis private fun deleteConversation(thread: ThreadRecord) { val threadID = thread.threadId val recipient = thread.recipient - val message: String - if (recipient.isGroupRecipient) { + val message = if (recipient.isGroupRecipient) { val group = groupDatabase.getGroup(recipient.address.toString()).orNull() if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) { - message = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." + "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." } else { - message = resources.getString(R.string.activity_home_leave_group_dialog_message) + resources.getString(R.string.activity_home_leave_group_dialog_message) } } else { - message = resources.getString(R.string.activity_home_delete_conversation_dialog_message) + resources.getString(R.string.activity_home_delete_conversation_dialog_message) } val dialog = AlertDialog.Builder(this) dialog.setMessage(message) @@ -419,7 +409,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis if (v2OpenGroup != null) { OpenGroupManager.delete(v2OpenGroup.server, v2OpenGroup.room, this@HomeActivity) } else { - ThreadUtils.queue { + lifecycleScope.launch(Dispatchers.IO) { threadDb.deleteConversation(threadID) } } @@ -436,12 +426,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis dialog.create().show() } - private fun openConversation(thread: ThreadRecord) { - val intent = Intent(this, ConversationActivityV2::class.java) - intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId) - push(intent) - } - private fun openSettings() { val intent = Intent(this, SettingsActivity::class.java) show(intent, isForResult = true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt index 3adf5f7b0..c75564d07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt @@ -9,20 +9,23 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.GlideRequests -class HomeAdapter(context: Context, cursor: Cursor?) : CursorRecyclerViewAdapter(context, cursor) { +class HomeAdapter( + context: Context, + cursor: Cursor?, + val listener: ConversationClickListener +) : CursorRecyclerViewAdapter(context, cursor) { private val threadDatabase = DatabaseComponent.get(context).threadDatabase() lateinit var glide: GlideRequests var typingThreadIDs = setOf() set(value) { field = value; notifyDataSetChanged() } - var conversationClickListener: ConversationClickListener? = null class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view) override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = ConversationView(context) - view.setOnClickListener { conversationClickListener?.onConversationClick(view) } + view.setOnClickListener { view.thread?.let { listener.onConversationClick(it) } } view.setOnLongClickListener { - conversationClickListener?.onLongConversationClick(view) + view.thread?.let { listener.onLongConversationClick(it) } true } return ViewHolder(view) @@ -45,6 +48,6 @@ class HomeAdapter(context: Context, cursor: Cursor?) : CursorRecyclerViewAdapter } interface ConversationClickListener { - fun onConversationClick(view: ConversationView) - fun onLongConversationClick(view: ConversationView) + fun onConversationClick(thread: ThreadRecord) + fun onLongConversationClick(thread: ThreadRecord) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt index b812d5c0d..c76668c41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt @@ -17,26 +17,33 @@ import android.widget.TextView import android.widget.Toast import androidx.annotation.ColorRes import androidx.localbroadcastmanager.content.LocalBroadcastManager -import kotlinx.android.synthetic.main.activity_path.* import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityPathBinding import org.session.libsession.snode.OnionRequestAPI import org.session.libsignal.utilities.Snode import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.util.* import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.IP2Country import org.thoughtcrime.securesms.util.PathDotView +import org.thoughtcrime.securesms.util.UiModeUtilities +import org.thoughtcrime.securesms.util.animateSizeChange +import org.thoughtcrime.securesms.util.disableClipping +import org.thoughtcrime.securesms.util.fadeIn +import org.thoughtcrime.securesms.util.fadeOut +import org.thoughtcrime.securesms.util.getColorWithID class PathActivity : PassphraseRequiredActionBarActivity() { + private lateinit var binding: ActivityPathBinding private val broadcastReceivers = mutableListOf() // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) - setContentView(R.layout.activity_path) + binding = ActivityPathBinding.inflate(layoutInflater) + setContentView(binding.root) supportActionBar!!.title = resources.getString(R.string.activity_path_title) - pathRowsContainer.disableClipping() - learnMoreButton.setOnClickListener { learnMore() } + binding.pathRowsContainer.disableClipping() + binding.learnMoreButton.setOnClickListener { learnMore() } update(false) registerObservers() } @@ -82,7 +89,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() { private fun handleOnionRequestPathCountriesLoaded() { update(false) } private fun update(isAnimated: Boolean) { - pathRowsContainer.removeAllViews() + binding.pathRowsContainer.removeAllViews() if (OnionRequestAPI.paths.isNotEmpty()) { val path = OnionRequestAPI.paths.firstOrNull() ?: return finish() val dotAnimationRepeatInterval = path.count().toLong() * 1000 + 1000 @@ -94,18 +101,18 @@ class PathActivity : PassphraseRequiredActionBarActivity() { val destinationRow = getPathRow(resources.getString(R.string.activity_path_destination_row_title), null, LineView.Location.Bottom, path.count().toLong() * 1000 + 2000, dotAnimationRepeatInterval) val rows = listOf( youRow ) + pathRows + listOf( destinationRow ) for (row in rows) { - pathRowsContainer.addView(row) + binding.pathRowsContainer.addView(row) } if (isAnimated) { - spinner.fadeOut() + binding.spinner.fadeOut() } else { - spinner.alpha = 0.0f + binding.spinner.alpha = 0.0f } } else { if (isAnimated) { - spinner.fadeIn() + binding.spinner.fadeIn() } else { - spinner.alpha = 1.0f + binding.spinner.alpha = 1.0f } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt index 35785492d..0ac74c084 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt @@ -14,10 +14,9 @@ import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.core.view.isVisible import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import dagger.hilt.EntryPoint import dagger.hilt.android.AndroidEntryPoint -import kotlinx.android.synthetic.main.fragment_user_details_bottom_sheet.* import network.loki.messenger.R +import network.loki.messenger.databinding.FragmentUserDetailsBottomSheetBinding import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.Address @@ -34,13 +33,15 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() { @Inject lateinit var threadDb: ThreadDatabase + private lateinit var binding: FragmentUserDetailsBottomSheetBinding companion object { const val ARGUMENT_PUBLIC_KEY = "publicKey" const val ARGUMENT_THREAD_ID = "threadId" } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_user_details_bottom_sheet, container, false) + binding = FragmentUserDetailsBottomSheetBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -49,58 +50,62 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() { val threadID = arguments?.getLong(ARGUMENT_THREAD_ID) ?: return dismiss() val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false) val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss() - profilePictureView.publicKey = publicKey - profilePictureView.glide = GlideApp.with(this) - profilePictureView.isLarge = true - profilePictureView.update(recipient, -1) - nameTextViewContainer.visibility = View.VISIBLE - nameTextViewContainer.setOnClickListener { - nameTextViewContainer.visibility = View.INVISIBLE - nameEditTextContainer.visibility = View.VISIBLE - nicknameEditText.text = null - nicknameEditText.requestFocus() - showSoftKeyboard() - } - cancelNicknameEditingButton.setOnClickListener { - nicknameEditText.clearFocus() - hideSoftKeyboard() + with(binding) { + profilePictureView.publicKey = publicKey + profilePictureView.glide = GlideApp.with(this@UserDetailsBottomSheet) + profilePictureView.isLarge = true + profilePictureView.update(recipient) nameTextViewContainer.visibility = View.VISIBLE - nameEditTextContainer.visibility = View.INVISIBLE - } - saveNicknameButton.setOnClickListener { - saveNickName(recipient) - } - nicknameEditText.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - saveNickName(recipient) - return@setOnEditorActionListener true - } - else -> return@setOnEditorActionListener false + nameTextViewContainer.setOnClickListener { + nameTextViewContainer.visibility = View.INVISIBLE + nameEditTextContainer.visibility = View.VISIBLE + nicknameEditText.text = null + nicknameEditText.requestFocus() + showSoftKeyboard() } - } - nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally + cancelNicknameEditingButton.setOnClickListener { + nicknameEditText.clearFocus() + hideSoftKeyboard() + nameTextViewContainer.visibility = View.VISIBLE + nameEditTextContainer.visibility = View.INVISIBLE + } + saveNicknameButton.setOnClickListener { + saveNickName(recipient) + } + nicknameEditText.setOnEditorActionListener { _, actionId, _ -> + when (actionId) { + EditorInfo.IME_ACTION_DONE -> { + saveNickName(recipient) + return@setOnEditorActionListener true + } + else -> return@setOnEditorActionListener false + } + } + nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally - publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient - messageButton.isVisible = !threadRecipient.isOpenGroupRecipient - publicKeyTextView.text = publicKey - publicKeyTextView.setOnLongClickListener { - val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Session ID", publicKey) - clipboard.setPrimaryClip(clip) - Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() - true - } - messageButton.setOnClickListener { - val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient) - val intent = Intent( - context, - ConversationActivityV2::class.java - ) - intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) - intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1) - startActivity(intent) - dismiss() + publicKeyTextView.isVisible = !threadRecipient.isOpenGroupRecipient + messageButton.isVisible = !threadRecipient.isOpenGroupRecipient + publicKeyTextView.text = publicKey + publicKeyTextView.setOnLongClickListener { + val clipboard = + requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Session ID", publicKey) + clipboard.setPrimaryClip(clip) + Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT) + .show() + true + } + messageButton.setOnClickListener { + val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient) + val intent = Intent( + context, + ConversationActivityV2::class.java + ) + intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) + intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1) + startActivity(intent) + dismiss() + } } } @@ -111,7 +116,7 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() { window.setDimAmount(if (isLightMode) 0.1f else 0.75f) } - fun saveNickName(recipient: Recipient) { + fun saveNickName(recipient: Recipient) = with(binding) { nicknameEditText.clearFocus() hideSoftKeyboard() nameTextViewContainer.visibility = View.VISIBLE @@ -131,11 +136,11 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() { @SuppressLint("ServiceCast") fun showSoftKeyboard() { val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.showSoftInput(nicknameEditText, 0) + imm?.showSoftInput(binding.nicknameEditText, 0) } fun hideSoftKeyboard() { val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager - imm?.hideSoftInputFromWindow(nicknameEditText.windowToken, 0) + imm?.hideSoftInputFromWindow(binding.nicknameEditText.windowToken, 0) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java index ee11c12bc..23f9d33e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java @@ -1,7 +1,7 @@ package org.thoughtcrime.securesms.mediasend; import android.annotation.SuppressLint; -import androidx.lifecycle.ViewModelProviders; +import androidx.lifecycle.ViewModelProvider; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Matrix; @@ -80,7 +80,7 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText controller = (Controller) getActivity(); camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), displaySize.x, displaySize.y, this); orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE); - viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); + viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); } @Nullable diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java index 5f9e4ed09..0fe41d2a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java @@ -1,7 +1,7 @@ package org.thoughtcrime.securesms.mediasend; import androidx.appcompat.app.ActionBar; -import androidx.lifecycle.ViewModelProviders; +import androidx.lifecycle.ViewModelProvider; import android.content.Context; import android.content.res.Configuration; import android.graphics.Point; @@ -66,7 +66,7 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem bucketId = getArguments().getString(KEY_BUCKET_ID); folderTitle = getArguments().getString(KEY_FOLDER_TITLE); maxSelection = getArguments().getInt(KEY_MAX_SELECTION); - viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); + viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java index ce1ccb2bb..319f00997 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java @@ -1,7 +1,7 @@ package org.thoughtcrime.securesms.mediasend; import android.annotation.SuppressLint; -import androidx.lifecycle.ViewModelProviders; +import androidx.lifecycle.ViewModelProvider; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Rect; @@ -313,7 +313,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl } private void initViewModel() { - viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); + viewModel = new ViewModelProvider(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class); viewModel.getSelectedMedia().observe(this, media -> { if (Util.isEmpty(media)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt index 885878c5f..c0699e3eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt @@ -7,8 +7,8 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.TextView.OnEditorActionListener import android.widget.Toast -import kotlinx.android.synthetic.main.activity_display_name.* import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityDisplayNameBinding import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.util.push @@ -16,28 +16,32 @@ import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo import org.session.libsession.utilities.TextSecurePreferences class DisplayNameActivity : BaseActionBarActivity() { + private lateinit var binding: ActivityDisplayNameBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setUpActionBarSessionLogo() - setContentView(R.layout.activity_display_name) - displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard - displayNameEditText.setOnEditorActionListener( - OnEditorActionListener { _, actionID, event -> - if (actionID == EditorInfo.IME_ACTION_SEARCH || - actionID == EditorInfo.IME_ACTION_DONE || - (event.action == KeyEvent.ACTION_DOWN && - event.keyCode == KeyEvent.KEYCODE_ENTER)) { - this.register() - return@OnEditorActionListener true - } - false - }) - registerButton.setOnClickListener { register() } + binding = ActivityDisplayNameBinding.inflate(layoutInflater) + setContentView(binding.root) + with(binding) { + displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard + displayNameEditText.setOnEditorActionListener( + OnEditorActionListener { _, actionID, event -> + if (actionID == EditorInfo.IME_ACTION_SEARCH || + actionID == EditorInfo.IME_ACTION_DONE || + (event.action == KeyEvent.ACTION_DOWN && + event.keyCode == KeyEvent.KEYCODE_ENTER)) { + register() + return@OnEditorActionListener true + } + false + }) + registerButton.setOnClickListener { register() } + } } private fun register() { - val displayName = displayNameEditText.text.toString().trim() + val displayName = binding.displayNameEditText.text.toString().trim() if (displayName.isEmpty()) { return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show() } @@ -45,7 +49,7 @@ class DisplayNameActivity : BaseActionBarActivity() { return Toast.makeText(this, R.string.activity_display_name_display_name_too_long_error, Toast.LENGTH_SHORT).show() } val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0) + inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0) TextSecurePreferences.setProfileName(this, displayName) val intent = Intent(this, PNModeActivity::class.java) push(intent) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt index 68c4edd59..dcd4d783e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt @@ -3,19 +3,17 @@ package org.thoughtcrime.securesms.onboarding import android.animation.FloatEvaluator import android.animation.ValueAnimator import android.content.Context -import android.content.Context.LAYOUT_INFLATER_SERVICE import android.os.Handler import android.util.AttributeSet import android.view.LayoutInflater import android.view.View -import android.widget.LinearLayout import android.widget.ScrollView -import kotlinx.android.synthetic.main.view_fake_chat.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.ViewFakeChatBinding import org.thoughtcrime.securesms.util.disableClipping class FakeChatView : ScrollView { - + private lateinit var binding: ViewFakeChatBinding // region Settings private val spacing = context.resources.getDimension(R.dimen.medium_spacing) private val startDelay: Long = 1000 @@ -41,17 +39,15 @@ class FakeChatView : ScrollView { } private fun setUpViewHierarchy() { - val inflater = context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater - val contentView = inflater.inflate(R.layout.view_fake_chat, null) as LinearLayout - contentView.disableClipping() - addView(contentView) + binding = ViewFakeChatBinding.inflate(LayoutInflater.from(context), this, true) + binding.root.disableClipping() isVerticalScrollBarEnabled = false } // endregion // region Animation fun startAnimating() { - listOf( bubble1, bubble2, bubble3, bubble4, bubble5 ).forEach { it.alpha = 0.0f } + listOf( binding.bubble1, binding.bubble2, binding.bubble3, binding.bubble4, binding.bubble5 ).forEach { it.alpha = 0.0f } fun show(bubble: View) { val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) animation.duration = animationDuration @@ -61,18 +57,18 @@ class FakeChatView : ScrollView { animation.start() } Handler().postDelayed({ - show(bubble1) + show(binding.bubble1) Handler().postDelayed({ - show(bubble2) + show(binding.bubble2) Handler().postDelayed({ - show(bubble3) - smoothScrollTo(0, (bubble1.height + spacing).toInt()) + show(binding.bubble3) + smoothScrollTo(0, (binding.bubble1.height + spacing).toInt()) Handler().postDelayed({ - show(bubble4) - smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt()) + show(binding.bubble4) + smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt()) Handler().postDelayed({ - show(bubble5) - smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt() + (bubble3.height + spacing).toInt()) + show(binding.bubble5) + smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt() + (binding.bubble3.height + spacing).toInt()) }, delayBetweenMessages) }, delayBetweenMessages) }, delayBetweenMessages) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt index 4fbd0f688..a87b769e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt @@ -2,25 +2,27 @@ package org.thoughtcrime.securesms.onboarding import android.content.Intent import android.os.Bundle -import android.view.View -import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityLandingBinding import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo -import org.thoughtcrime.securesms.service.KeyCachingService class LandingActivity : BaseActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_landing) + val binding = ActivityLandingBinding.inflate(layoutInflater) + setContentView(binding.root) setUpActionBarSessionLogo(true) - findViewById(R.id.fakeChatView).startAnimating() - findViewById(R.id.registerButton).setOnClickListener { register() } - findViewById(R.id.restoreButton).setOnClickListener { restore() } - findViewById(R.id.linkButton).setOnClickListener { link() } + with(binding) { + fakeChatView.startAnimating() + registerButton.setOnClickListener { register() } + restoreButton.setOnClickListener { restore() } + linkButton.setOnClickListener { link() } + } IdentityKeyUtil.generateIdentityKeyPair(this) TextSecurePreferences.setPasswordDisabled(this, true) // AC: This is a temporary workaround to trick the old code that the screen is unlocked. diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt index 24701e461..ee1631a00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt @@ -4,7 +4,9 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.text.InputType -import android.view.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast @@ -13,14 +15,13 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentPagerAdapter import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.activity_link_device.* -import kotlinx.android.synthetic.main.fragment_recovery_phrase.* import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityLinkDeviceBinding +import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.utilities.Hex @@ -30,13 +31,14 @@ import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.crypto.MnemonicUtilities import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate -import org.thoughtcrime.securesms.crypto.MnemonicUtilities import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { + private lateinit var binding: ActivityLinkDeviceBinding private val adapter = LinkDeviceActivityAdapter(this) private var restoreJob: Job? = null @@ -55,9 +57,10 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis()) setLastProfileUpdateTime(this@LinkDeviceActivity, 0) } - setContentView(R.layout.activity_link_device) - viewPager.adapter = adapter - tabLayout.setupWithViewPager(viewPager) + binding = ActivityLinkDeviceBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.viewPager.adapter = adapter + binding.tabLayout.setupWithViewPager(binding.viewPager) } // endregion @@ -107,8 +110,8 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis()) TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true) - loader.isVisible = true - val snackBar = Snackbar.make(containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE) + binding.loader.isVisible = true + val snackBar = Snackbar.make(binding.containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE) .setAction(R.string.registration_activity__skip) { register(true) } val skipJob = launch { @@ -127,13 +130,13 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel register(false) } - loader.isVisible = false + binding.loader.isVisible = false } } private fun register(skipped: Boolean) { restoreJob?.cancel() - loader.isVisible = false + binding.loader.isVisible = false TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis()) val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK @@ -175,30 +178,34 @@ private class LinkDeviceActivityAdapter(private val activity: LinkDeviceActivity // region Recovery Phrase Fragment class RecoveryPhraseFragment : Fragment() { + private lateinit var binding: FragmentRecoveryPhraseBinding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment_recovery_phrase, container, false) + binding = FragmentRecoveryPhraseBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard - mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT) - mnemonicEditText.setOnEditorActionListener { v, actionID, _ -> - if (actionID == EditorInfo.IME_ACTION_DONE) { - val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(v.windowToken, 0) - handleContinueButtonTapped() - true - } else { - false + with(binding) { + mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard + mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT) + mnemonicEditText.setOnEditorActionListener { v, actionID, _ -> + if (actionID == EditorInfo.IME_ACTION_DONE) { + val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v.windowToken, 0) + handleContinueButtonTapped() + true + } else { + false + } } + continueButton.setOnClickListener { handleContinueButtonTapped() } } - continueButton.setOnClickListener { handleContinueButtonTapped() } } private fun handleContinueButtonTapped() { - val mnemonic = mnemonicEditText.text?.trim().toString() + val mnemonic = binding.mnemonicEditText.text?.trim().toString() (requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt index cbfd5bcc9..c081e8fa9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt @@ -13,9 +13,8 @@ import android.view.View import android.widget.Toast import androidx.annotation.ColorRes import androidx.annotation.DrawableRes -import kotlinx.android.synthetic.main.activity_display_name.registerButton -import kotlinx.android.synthetic.main.activity_pn_mode.* import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityPnModeBinding import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity @@ -28,6 +27,7 @@ import org.thoughtcrime.securesms.util.GlowViewUtilities import org.thoughtcrime.securesms.util.PNModeView class PNModeActivity : BaseActionBarActivity() { + private lateinit var binding: ActivityPnModeBinding private var selectedOptionView: PNModeView? = null // region Lifecycle @@ -35,15 +35,18 @@ class PNModeActivity : BaseActionBarActivity() { super.onCreate(savedInstanceState) setUpActionBarSessionLogo(true) TextSecurePreferences.setHasSeenWelcomeScreen(this, true) - setContentView(R.layout.activity_pn_mode) - contentView.disableClipping() - fcmOptionView.setOnClickListener { toggleFCM() } - fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme) - fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme) - backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() } - backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme) - backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme) - registerButton.setOnClickListener { register() } + binding = ActivityPnModeBinding.inflate(layoutInflater) + setContentView(binding.root) + with(binding) { + contentView.disableClipping() + fcmOptionView.setOnClickListener { toggleFCM() } + fcmOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme) + fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme) + backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() } + backgroundPollingOptionView.mainColor = resources.getColorWithID(R.color.pn_option_background, theme) + backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme) + registerButton.setOnClickListener { register() } + } toggleFCM() } @@ -63,8 +66,7 @@ class PNModeActivity : BaseActionBarActivity() { // region Interaction override fun onOptionsItemSelected(item: MenuItem): Boolean { - val id = item.itemId - when(id) { + when(item.itemId) { R.id.learnMoreButton -> learnMore() else -> { /* Do nothing */ } } @@ -81,52 +83,52 @@ class PNModeActivity : BaseActionBarActivity() { } } - private fun toggleFCM() { + private fun toggleFCM() = with(binding) { when (selectedOptionView) { null -> { performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.transparent, R.color.accent) + GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.transparent, R.color.accent) animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent) selectedOptionView = fcmOptionView } fcmOptionView -> { performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.accent, R.color.transparent) + GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.accent, R.color.transparent) animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border) selectedOptionView = null } backgroundPollingOptionView -> { performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.transparent, R.color.accent) + GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.transparent, R.color.accent) animateStrokeColorChange(fcmOptionView, R.color.pn_option_border, R.color.accent) performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.accent, R.color.transparent) + GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.accent, R.color.transparent) animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border) selectedOptionView = fcmOptionView } } } - private fun toggleBackgroundPolling() { + private fun toggleBackgroundPolling() = with(binding) { when (selectedOptionView) { null -> { performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.transparent, R.color.accent) + GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.transparent, R.color.accent) animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent) selectedOptionView = backgroundPollingOptionView } backgroundPollingOptionView -> { performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.accent, R.color.transparent) + GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.accent, R.color.transparent) animateStrokeColorChange(backgroundPollingOptionView, R.color.accent, R.color.pn_option_border) selectedOptionView = null } fcmOptionView -> { performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(this, backgroundPollingOptionView, R.color.transparent, R.color.accent) + GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, backgroundPollingOptionView, R.color.transparent, R.color.accent) animateStrokeColorChange(backgroundPollingOptionView, R.color.pn_option_border, R.color.accent) performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(this, fcmOptionView, R.color.accent, R.color.transparent) + GlowViewUtilities.animateShadowColorChange(this@PNModeActivity, fcmOptionView, R.color.accent, R.color.transparent) animateStrokeColorChange(fcmOptionView, R.color.accent, R.color.pn_option_border) selectedOptionView = backgroundPollingOptionView } @@ -153,7 +155,7 @@ class PNModeActivity : BaseActionBarActivity() { dialog.create().show() return } - TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == fcmOptionView)) + TextSecurePreferences.setIsUsingFCM(this, (selectedOptionView == binding.fcmOptionView)) val application = ApplicationContext.getInstance(this) application.startPollingIfNeeded() application.registerForFCMIfNeeded(true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt index 968b3d0dd..6a1c785ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt @@ -11,8 +11,8 @@ import android.text.style.ClickableSpan import android.text.style.StyleSpan import android.view.View import android.widget.Toast -import kotlinx.android.synthetic.main.activity_recovery_phrase_restore.* import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.utilities.Hex @@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo class RecoveryPhraseRestoreActivity : BaseActionBarActivity() { - + private lateinit var binding: ActivityRecoveryPhraseRestoreBinding // region Lifecycle override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -36,9 +36,10 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() { setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis()) setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis()) } - setContentView(R.layout.activity_recovery_phrase_restore) - mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard - restoreButton.setOnClickListener { restore() } + binding = ActivityRecoveryPhraseRestoreBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.mnemonicEditText.imeOptions = binding.mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard + binding.restoreButton.setOnClickListener { restore() } val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy") termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) termsExplanation.setSpan(object : ClickableSpan() { @@ -54,14 +55,14 @@ class RecoveryPhraseRestoreActivity : BaseActionBarActivity() { openURL("https://getsession.org/privacy-policy/") } }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsTextView.movementMethod = LinkMovementMethod.getInstance() - termsTextView.text = termsExplanation + binding.termsTextView.movementMethod = LinkMovementMethod.getInstance() + binding.termsTextView.text = termsExplanation } // endregion // region Interaction private fun restore() { - val mnemonic = mnemonicEditText.text.toString() + val mnemonic = binding.mnemonicEditText.text.toString() try { val loadFileContents: (String) -> String = { fileName -> MnemonicUtilities.loadFileContents(this, fileName) diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt index 9dac8875d..0105fbedf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt @@ -16,8 +16,8 @@ import android.text.style.StyleSpan import android.view.View import android.widget.Toast import com.goterl.lazysodium.utils.KeyPair -import kotlinx.android.synthetic.main.activity_register.* import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityRegisterBinding import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.utilities.KeyHelper @@ -26,9 +26,9 @@ import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo -import java.util.* class RegisterActivity : BaseActionBarActivity() { + private lateinit var binding: ActivityRegisterBinding private var seed: ByteArray? = null private var ed25519KeyPair: KeyPair? = null private var x25519KeyPair: ECKeyPair? = null @@ -37,7 +37,8 @@ class RegisterActivity : BaseActionBarActivity() { // region Lifecycle override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_register) + binding = ActivityRegisterBinding.inflate(layoutInflater) + setContentView(binding.root) setUpActionBarSessionLogo() TextSecurePreferences.apply { setHasViewedSeed(this@RegisterActivity, false) @@ -45,8 +46,8 @@ class RegisterActivity : BaseActionBarActivity() { setRestorationTime(this@RegisterActivity, 0) setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis()) } - registerButton.setOnClickListener { register() } - copyButton.setOnClickListener { copyPublicKey() } + binding.registerButton.setOnClickListener { register() } + binding.copyButton.setOnClickListener { copyPublicKey() } val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy") termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) termsExplanation.setSpan(object : ClickableSpan() { @@ -62,8 +63,8 @@ class RegisterActivity : BaseActionBarActivity() { openURL("https://getsession.org/privacy-policy/") } }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsTextView.movementMethod = LinkMovementMethod.getInstance() - termsTextView.text = termsExplanation + binding.termsTextView.movementMethod = LinkMovementMethod.getInstance() + binding.termsTextView.text = termsExplanation updateKeyPair() } // endregion @@ -94,12 +95,12 @@ class RegisterActivity : BaseActionBarActivity() { } count += 1 if (count < limit) { - publicKeyTextView.text = mangledHexEncodedPublicKey + binding.publicKeyTextView.text = mangledHexEncodedPublicKey Handler().postDelayed({ animate() }, 32) } else { - publicKeyTextView.text = hexEncodedPublicKey + binding.publicKeyTextView.text = hexEncodedPublicKey } } animate() diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt index ad8d3a29c..b179ce852 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt @@ -9,8 +9,8 @@ import android.text.SpannableString import android.text.style.ForegroundColorSpan import android.widget.LinearLayout import android.widget.Toast -import kotlinx.android.synthetic.main.activity_seed.* import network.loki.messenger.R +import network.loki.messenger.databinding.ActivitySeedBinding import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.utilities.hexEncodedPrivateKey @@ -21,6 +21,8 @@ import org.thoughtcrime.securesms.util.getColorWithID class SeedActivity : BaseActionBarActivity() { + private lateinit var binding: ActivitySeedBinding + private val seed by lazy { var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED) if (hexEncodedSeed == null) { @@ -35,27 +37,30 @@ class SeedActivity : BaseActionBarActivity() { // region Lifecycle override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_seed) + binding = ActivitySeedBinding.inflate(layoutInflater) + setContentView(binding.root) supportActionBar!!.title = resources.getString(R.string.activity_seed_title) val seedReminderViewTitle = SpannableString("You're almost finished! 90%") // Intentionally not yet translated seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - seedReminderView.title = seedReminderViewTitle - seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2) - seedReminderView.setProgress(90, false) - seedReminderView.hideContinueButton() - var redactedSeed = seed - var index = 0 - for (character in seed) { - if (character.isLetter()) { - redactedSeed = redactedSeed.replaceRange(index, index + 1, "â–†") + with(binding) { + seedReminderView.title = seedReminderViewTitle + seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2) + seedReminderView.setProgress(90, false) + seedReminderView.hideContinueButton() + var redactedSeed = seed + var index = 0 + for (character in seed) { + if (character.isLetter()) { + redactedSeed = redactedSeed.replaceRange(index, index + 1, "â–†") + } + index += 1 } - index += 1 + seedTextView.setTextColor(resources.getColorWithID(R.color.accent, theme)) + seedTextView.text = redactedSeed + seedTextView.setOnLongClickListener { revealSeed(); true } + revealButton.setOnLongClickListener { revealSeed(); true } + copyButton.setOnClickListener { copySeed() } } - seedTextView.setTextColor(resources.getColorWithID(R.color.accent, theme)) - seedTextView.text = redactedSeed - seedTextView.setOnLongClickListener { revealSeed(); true } - revealButton.setOnLongClickListener { revealSeed(); true } - copyButton.setOnClickListener { copySeed() } } // endregion @@ -63,14 +68,16 @@ class SeedActivity : BaseActionBarActivity() { private fun revealSeed() { val seedReminderViewTitle = SpannableString("Account secured! 100%") // Intentionally not yet translated seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 17, 21, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - seedReminderView.title = seedReminderViewTitle - seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3) - seedReminderView.setProgress(100, true) - val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams - seedTextViewLayoutParams.height = seedTextView.height - seedTextView.layoutParams = seedTextViewLayoutParams - seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme)) - seedTextView.text = seed + with(binding) { + seedReminderView.title = seedReminderViewTitle + seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3) + seedReminderView.setProgress(100, true) + val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams + seedTextViewLayoutParams.height = seedTextView.height + seedTextView.layoutParams = seedTextViewLayoutParams + seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme)) + seedTextView.text = seed + } TextSecurePreferences.setHasViewedSeed(this, true) } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt index 199ed7a5a..28611985f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt @@ -6,16 +6,17 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout -import kotlinx.android.synthetic.main.view_seed_reminder.view.* -import network.loki.messenger.R +import network.loki.messenger.databinding.ViewSeedReminderBinding class SeedReminderView : FrameLayout { + private lateinit var binding: ViewSeedReminderBinding + var title: CharSequence - get() = titleTextView.text - set(value) { titleTextView.text = value } + get() = binding.titleTextView.text + set(value) { binding.titleTextView.text = value } var subtitle: CharSequence - get() = subtitleTextView.text - set(value) { subtitleTextView.text = value } + get() = binding.subtitleTextView.text + set(value) { binding.subtitleTextView.text = value } var delegate: SeedReminderViewDelegate? = null constructor(context: Context) : super(context) { @@ -35,22 +36,20 @@ class SeedReminderView : FrameLayout { } private fun setUpViewHierarchy() { - val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - val contentView = inflater.inflate(R.layout.view_seed_reminder, null) - addView(contentView) - button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() } + binding = ViewSeedReminderBinding.inflate(LayoutInflater.from(context), this, true) + binding.button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() } } fun setProgress(progress: Int, isAnimated: Boolean) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - progressBar.setProgress(progress, isAnimated) + binding.progressBar.setProgress(progress, isAnimated) } else { - progressBar.progress = progress + binding.progressBar.progress = progress } } fun hideContinueButton() { - button.visibility = View.GONE + binding.button.visibility = View.GONE } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt index a974d427a..d4d30d864 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt @@ -4,10 +4,12 @@ import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope -import kotlinx.android.synthetic.main.dialog_clear_all_data.* -import kotlinx.android.synthetic.main.dialog_clear_all_data.view.* -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import network.loki.messenger.R +import network.loki.messenger.databinding.DialogClearAllDataBinding import org.session.libsession.snode.SnodeAPI import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.ApplicationContext @@ -15,6 +17,7 @@ import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog class ClearAllDataDialog : BaseDialog() { + private lateinit var binding: DialogClearAllDataBinding enum class Steps { INFO_PROMPT, @@ -34,15 +37,15 @@ class ClearAllDataDialog : BaseDialog() { } override fun setContentView(builder: AlertDialog.Builder) { - val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_clear_all_data, null) - contentView.cancelButton.setOnClickListener { + binding = DialogClearAllDataBinding.inflate(LayoutInflater.from(requireContext())) + binding.cancelButton.setOnClickListener { if (step == Steps.NETWORK_PROMPT) { clearAllData(false) } else if (step != Steps.DELETING) { dismiss() } } - contentView.clearAllDataButton.setOnClickListener { + binding.clearAllDataButton.setOnClickListener { when(step) { Steps.INFO_PROMPT -> step = Steps.NETWORK_PROMPT Steps.NETWORK_PROMPT -> { @@ -51,36 +54,33 @@ class ClearAllDataDialog : BaseDialog() { Steps.DELETING -> { /* do nothing intentionally */ } } } - builder.setView(contentView) + builder.setView(binding.root) builder.setCancelable(false) } private fun updateUI() { - - dialog?.let { view -> - + dialog?.let { val isLoading = step == Steps.DELETING when (step) { Steps.INFO_PROMPT -> { - view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation) - view.cancelButton.setText(R.string.cancel) - view.clearAllDataButton.setText(R.string.delete) + binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_explanation) + binding.cancelButton.setText(R.string.cancel) + binding.clearAllDataButton.setText(R.string.delete) } else -> { - view.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation) - view.cancelButton.setText(R.string.dialog_clear_all_data_local_only) - view.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network) + binding.dialogDescriptionText.setText(R.string.dialog_clear_all_data_network_explanation) + binding.cancelButton.setText(R.string.dialog_clear_all_data_local_only) + binding.clearAllDataButton.setText(R.string.dialog_clear_all_data_clear_network) } } - view.cancelButton.isVisible = !isLoading - view.clearAllDataButton.isVisible = !isLoading - view.progressBar.isVisible = isLoading + binding.cancelButton.isVisible = !isLoading + binding.clearAllDataButton.isVisible = !isLoading + binding.progressBar.isVisible = isLoading - view.setCanceledOnTouchOutside(!isLoading) + it.setCanceledOnTouchOutside(!isLoading) isCancelable = !isLoading - } } 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 c21470598..20f67cd29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt @@ -10,9 +10,9 @@ import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentPagerAdapter -import kotlinx.android.synthetic.main.activity_qr_code.* -import kotlinx.android.synthetic.main.fragment_view_my_qr_code.* import network.loki.messenger.R +import network.loki.messenger.databinding.ActivityQrCodeBinding +import network.loki.messenger.databinding.FragmentViewMyQrCodeBinding import org.session.libsession.utilities.Address import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient @@ -20,23 +20,29 @@ import org.session.libsignal.utilities.PublicKeyValidation import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.util.* +import org.thoughtcrime.securesms.util.FileProviderUtil +import org.thoughtcrime.securesms.util.QRCodeUtilities +import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment +import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate +import org.thoughtcrime.securesms.util.toPx import java.io.File import java.io.FileOutputStream class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { + private lateinit var binding: ActivityQrCodeBinding private val adapter = QRCodeActivityAdapter(this) // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) + binding = ActivityQrCodeBinding.inflate(layoutInflater) // Set content view - setContentView(R.layout.activity_qr_code) + setContentView(binding.root) // Set title supportActionBar!!.title = resources.getString(R.string.activity_qr_code_title) // Set up view pager - viewPager.adapter = adapter - tabLayout.setupWithViewPager(viewPager) + binding.viewPager.adapter = adapter + binding.tabLayout.setupWithViewPager(binding.viewPager) } // endregion @@ -91,6 +97,7 @@ private class QRCodeActivityAdapter(val activity: QRCodeActivity) : FragmentPage // region View My QR Code Fragment class ViewMyQRCodeFragment : Fragment() { + private lateinit var binding: FragmentViewMyQrCodeBinding private val hexEncodedPublicKey: String get() { @@ -98,18 +105,19 @@ class ViewMyQRCodeFragment : Fragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment_view_my_qr_code, container, false) + binding = FragmentViewMyQrCodeBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val size = toPx(280, resources) val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false) - qrCodeImageView.setImageBitmap(qrCode) + binding.qrCodeImageView.setImageBitmap(qrCode) // val explanation = SpannableStringBuilder("This is your unique public QR code. Other users can scan this to start a conversation with you.") // explanation.setSpan(StyleSpan(Typeface.BOLD), 8, 34, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation) - shareButton.setOnClickListener { shareQRCode() } + binding.explanationTextView.text = resources.getString(R.string.fragment_view_my_qr_code_explanation) + binding.shareButton.setOnClickListener { shareQRCode() } } private fun shareQRCode() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt index 19fffc343..32624129d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt @@ -6,8 +6,8 @@ import android.content.Context import android.view.LayoutInflater import android.widget.Toast import androidx.appcompat.app.AlertDialog -import kotlinx.android.synthetic.main.dialog_seed.view.* import network.loki.messenger.R +import network.loki.messenger.databinding.DialogSeedBinding import org.session.libsignal.crypto.MnemonicCodec import org.session.libsignal.utilities.hexEncodedPrivateKey import org.thoughtcrime.securesms.crypto.IdentityKeyUtil @@ -28,11 +28,11 @@ class SeedDialog : BaseDialog() { } override fun setContentView(builder: AlertDialog.Builder) { - val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_seed, null) - contentView.seedTextView.text = seed - contentView.cancelButton.setOnClickListener { dismiss() } - contentView.copyButton.setOnClickListener { copySeed() } - builder.setView(contentView) + val binding = DialogSeedBinding.inflate(LayoutInflater.from(requireContext())) + binding.seedTextView.text = seed + binding.cancelButton.setOnClickListener { dismiss() } + binding.copyButton.setOnClickListener { copySeed() } + builder.setView(binding.root) } private fun copySeed() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index c0159d317..fb9d13f0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -7,7 +7,11 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.net.Uri -import android.os.* +import android.os.AsyncTask +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.ActionMode import android.view.Menu import android.view.MenuItem @@ -15,9 +19,9 @@ import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.core.view.isVisible -import kotlinx.android.synthetic.main.activity_settings.* import network.loki.messenger.BuildConfig import network.loki.messenger.R +import network.loki.messenger.databinding.ActivitySettingsBinding import nl.komponents.kovenant.Promise import nl.komponents.kovenant.all import nl.komponents.kovenant.ui.alwaysUi @@ -34,12 +38,17 @@ import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints -import org.thoughtcrime.securesms.util.* +import org.thoughtcrime.securesms.util.BitmapDecodingException +import org.thoughtcrime.securesms.util.BitmapUtil +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import org.thoughtcrime.securesms.util.UiModeUtilities +import org.thoughtcrime.securesms.util.push import java.io.File import java.security.SecureRandom -import java.util.* +import java.util.Date class SettingsActivity : PassphraseRequiredActionBarActivity() { + private lateinit var binding: ActivitySettingsBinding private var displayNameEditActionMode: ActionMode? = null set(value) { field = value; handleDisplayNameEditActionModeChanged() } private lateinit var glide: GlideRequests @@ -59,33 +68,36 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { super.onCreate(savedInstanceState, isReady) - setContentView(R.layout.activity_settings) + binding = ActivitySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) val displayName = TextSecurePreferences.getProfileName(this) ?: hexEncodedPublicKey glide = GlideApp.with(this) - profilePictureView.glide = glide - profilePictureView.publicKey = hexEncodedPublicKey - profilePictureView.displayName = displayName - profilePictureView.isLarge = true - profilePictureView.update() - profilePictureView.setOnClickListener { showEditProfilePictureUI() } - ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) } - btnGroupNameDisplay.text = displayName - publicKeyTextView.text = hexEncodedPublicKey - copyButton.setOnClickListener { copyPublicKey() } - shareButton.setOnClickListener { sharePublicKey() } - privacyButton.setOnClickListener { showPrivacySettings() } - notificationsButton.setOnClickListener { showNotificationSettings() } - chatsButton.setOnClickListener { showChatSettings() } - sendInvitationButton.setOnClickListener { sendInvitation() } - faqButton.setOnClickListener { showFAQ() } - surveyButton.setOnClickListener { showSurvey() } - helpTranslateButton.setOnClickListener { helpTranslate() } - seedButton.setOnClickListener { showSeed() } - clearAllDataButton.setOnClickListener { clearAllData() } - debugLogButton.setOnClickListener { shareLogs() } - val isLightMode = UiModeUtilities.isDayUiMode(this) - oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode) - versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") + with(binding) { + profilePictureView.glide = glide + profilePictureView.publicKey = hexEncodedPublicKey + profilePictureView.displayName = displayName + profilePictureView.isLarge = true + profilePictureView.update() + profilePictureView.setOnClickListener { showEditProfilePictureUI() } + ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) } + btnGroupNameDisplay.text = displayName + publicKeyTextView.text = hexEncodedPublicKey + copyButton.setOnClickListener { copyPublicKey() } + shareButton.setOnClickListener { sharePublicKey() } + privacyButton.setOnClickListener { showPrivacySettings() } + notificationsButton.setOnClickListener { showNotificationSettings() } + chatsButton.setOnClickListener { showChatSettings() } + sendInvitationButton.setOnClickListener { sendInvitation() } + faqButton.setOnClickListener { showFAQ() } + surveyButton.setOnClickListener { showSurvey() } + helpTranslateButton.setOnClickListener { helpTranslate() } + seedButton.setOnClickListener { showSeed() } + clearAllDataButton.setOnClickListener { clearAllData() } + debugLogButton.setOnClickListener { shareLogs() } + val isLightMode = UiModeUtilities.isDayUiMode(this@SettingsActivity) + oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode) + versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -152,22 +164,22 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { private fun handleDisplayNameEditActionModeChanged() { val isEditingDisplayName = this.displayNameEditActionMode !== null - btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE - displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE + binding.btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE + binding.displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager if (isEditingDisplayName) { - displayNameEditText.setText(btnGroupNameDisplay.text) - displayNameEditText.selectAll() - displayNameEditText.requestFocus() - inputMethodManager.showSoftInput(displayNameEditText, 0) + binding.displayNameEditText.setText(binding.btnGroupNameDisplay.text) + binding.displayNameEditText.selectAll() + binding.displayNameEditText.requestFocus() + inputMethodManager.showSoftInput(binding.displayNameEditText, 0) } else { - inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0) + inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0) } } private fun updateProfile(isUpdatingProfilePicture: Boolean) { - loader.isVisible = true + binding.loader.isVisible = true val promises = mutableListOf>() val displayName = displayNameToBeUploaded if (displayName != null) { @@ -192,15 +204,15 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } compoundPromise.alwaysUi { if (displayName != null) { - btnGroupNameDisplay.text = displayName + binding.btnGroupNameDisplay.text = displayName } if (isUpdatingProfilePicture && profilePicture != null) { - profilePictureView.recycle() // Clear the cached image before updating - profilePictureView.update() + binding.profilePictureView.recycle() // Clear the cached image before updating + binding.profilePictureView.update() } displayNameToBeUploaded = null profilePictureToBeUploaded = null - loader.isVisible = false + binding.loader.isVisible = false } } // endregion @@ -211,7 +223,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { * @return true if the update was successful. */ private fun saveDisplayName(): Boolean { - val displayName = displayNameEditText.text.toString().trim() + val displayName = binding.displayNameEditText.text.toString().trim() if (displayName.isEmpty()) { Toast.makeText(this, R.string.activity_settings_display_name_missing_error, Toast.LENGTH_SHORT).show() return false diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt index 76d6f6a1c..0421f08ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt @@ -13,12 +13,12 @@ import android.webkit.MimeTypeMap import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope -import kotlinx.android.synthetic.main.dialog_share_logs.view.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import network.loki.messenger.BuildConfig import network.loki.messenger.R +import network.loki.messenger.databinding.DialogShareLogsBinding import org.session.libsignal.utilities.ExternalStorageUtil import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog @@ -26,7 +26,7 @@ import org.thoughtcrime.securesms.util.StreamUtil import java.io.File import java.io.FileOutputStream import java.io.IOException -import java.util.* +import java.util.Objects import java.util.concurrent.TimeUnit class ShareLogsDialog : BaseDialog() { @@ -34,16 +34,15 @@ class ShareLogsDialog : BaseDialog() { private var shareJob: Job? = null override fun setContentView(builder: AlertDialog.Builder) { - val contentView = - LayoutInflater.from(requireContext()).inflate(R.layout.dialog_share_logs, null) - contentView.cancelButton.setOnClickListener { + val binding = DialogShareLogsBinding.inflate(LayoutInflater.from(requireContext())) + binding.cancelButton.setOnClickListener { dismiss() } - contentView.shareButton.setOnClickListener { + binding.shareButton.setOnClickListener { // start the export and share shareLogs() } - builder.setView(contentView) + builder.setView(binding.root) builder.setCancelable(false) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt new file mode 100644 index 000000000..4225dabf4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -0,0 +1,229 @@ +package org.thoughtcrime.securesms.repository + +import org.session.libsession.database.MessageDataProvider +import org.session.libsession.messaging.messages.control.UnsendRequest +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage +import org.session.libsession.messaging.messages.visible.OpenGroupInvitation +import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 +import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.utilities.toHexString +import org.thoughtcrime.securesms.database.DraftDatabase +import org.thoughtcrime.securesms.database.LokiMessageDatabase +import org.thoughtcrime.securesms.database.LokiThreadDatabase +import org.thoughtcrime.securesms.database.MmsDatabase +import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.model.MessageRecord +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +interface ConversationRepository { + fun isOxenHostedOpenGroup(threadId: Long): Boolean + fun getRecipientForThreadId(threadId: Long): Recipient + fun saveDraft(threadId: Long, text: String) + fun getDraft(threadId: Long): String? + fun inviteContacts(threadId: Long, contacts: List) + fun unblock(recipient: Recipient) + fun deleteLocally(recipient: Recipient, message: MessageRecord) + + suspend fun deleteForEveryone( + threadId: Long, + recipient: Recipient, + message: MessageRecord + ): ResultOf + + fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? + + suspend fun deleteMessageWithoutUnsendRequest( + threadId: Long, + messages: Set + ): ResultOf + + suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf + + suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf +} + +class DefaultConversationRepository @Inject constructor( + private val textSecurePreferences: TextSecurePreferences, + private val messageDataProvider: MessageDataProvider, + private val threadDb: ThreadDatabase, + private val draftDb: DraftDatabase, + private val lokiThreadDb: LokiThreadDatabase, + private val smsDb: SmsDatabase, + private val mmsDb: MmsDatabase, + private val recipientDb: RecipientDatabase, + private val lokiMessageDb: LokiMessageDatabase +) : ConversationRepository { + + override fun isOxenHostedOpenGroup(threadId: Long): Boolean { + val openGroup = lokiThreadDb.getOpenGroupChat(threadId) + return openGroup?.room == "session" || openGroup?.room == "oxen" + || openGroup?.room == "lokinet" || openGroup?.room == "crypto" + } + + override fun getRecipientForThreadId(threadId: Long): Recipient { + return threadDb.getRecipientForThreadId(threadId)!! + } + + override fun saveDraft(threadId: Long, text: String) { + if (text.isEmpty()) return + val drafts = DraftDatabase.Drafts() + drafts.add(DraftDatabase.Draft(DraftDatabase.Draft.TEXT, text)) + draftDb.insertDrafts(threadId, drafts) + } + + override fun getDraft(threadId: Long): String? { + val drafts = draftDb.getDrafts(threadId) + draftDb.clearDrafts(threadId) + return drafts.find { it.type == DraftDatabase.Draft.TEXT }?.value + } + + override fun inviteContacts(threadId: Long, contacts: List) { + val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return + for (contact in contacts) { + val message = VisibleMessage() + message.sentTimestamp = System.currentTimeMillis() + val openGroupInvitation = OpenGroupInvitation() + openGroupInvitation.name = openGroup.name + openGroupInvitation.url = openGroup.joinURL + message.openGroupInvitation = openGroupInvitation + val outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation( + openGroupInvitation, + contact, + message.sentTimestamp + ) + smsDb.insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!) + MessageSender.send(message, contact.address) + } + } + + override fun unblock(recipient: Recipient) { + recipientDb.setBlocked(recipient, false) + } + + override fun deleteLocally(recipient: Recipient, message: MessageRecord) { + buildUnsendRequest(recipient, message)?.let { unsendRequest -> + textSecurePreferences.getLocalNumber()?.let { + MessageSender.send(unsendRequest, Address.fromSerialized(it)) + } + } + messageDataProvider.deleteMessage(message.id, !message.isMms) + } + + override suspend fun deleteForEveryone( + threadId: Long, + recipient: Recipient, + message: MessageRecord + ): ResultOf = suspendCoroutine { continuation -> + buildUnsendRequest(recipient, message)?.let { unsendRequest -> + MessageSender.send(unsendRequest, recipient.address) + } + val openGroup = lokiThreadDb.getOpenGroupChat(threadId) + if (openGroup != null) { + lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID -> + OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server) + .success { + messageDataProvider.deleteMessage(message.id, !message.isMms) + continuation.resume(ResultOf.Success(Unit)) + }.fail { error -> + continuation.resumeWithException(error) + } + } + } else { + messageDataProvider.deleteMessage(message.id, !message.isMms) + messageDataProvider.getServerHashForMessage(message.id)?.let { serverHash -> + var publicKey = recipient.address.serialize() + if (recipient.isClosedGroupRecipient) { + publicKey = GroupUtil.doubleDecodeGroupID(publicKey).toHexString() + } + SnodeAPI.deleteMessage(publicKey, listOf(serverHash)) + .success { + continuation.resume(ResultOf.Success(Unit)) + }.fail { error -> + continuation.resumeWithException(error) + } + } + } + } + + override fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest? { + if (recipient.isOpenGroupRecipient) return null + messageDataProvider.getServerHashForMessage(message.id) ?: return null + val unsendRequest = UnsendRequest() + if (message.isOutgoing) { + unsendRequest.author = textSecurePreferences.getLocalNumber() + } else { + unsendRequest.author = message.individualRecipient.address.contactIdentifier() + } + unsendRequest.timestamp = message.timestamp + + return unsendRequest + } + + override suspend fun deleteMessageWithoutUnsendRequest( + threadId: Long, + messages: Set + ): ResultOf = suspendCoroutine { continuation -> + val openGroup = lokiThreadDb.getOpenGroupChat(threadId) + if (openGroup != null) { + val messageServerIDs = mutableMapOf() + for (message in messages) { + val messageServerID = + lokiMessageDb.getServerID(message.id, !message.isMms) ?: continue + messageServerIDs[messageServerID] = message + } + for ((messageServerID, message) in messageServerIDs) { + OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server) + .success { + messageDataProvider.deleteMessage(message.id, !message.isMms) + }.fail { error -> + continuation.resumeWithException(error) + } + } + } else { + for (message in messages) { + if (message.isMms) { + mmsDb.deleteMessage(message.id) + } else { + smsDb.deleteMessage(message.id) + } + } + } + continuation.resume(ResultOf.Success(Unit)) + } + + override suspend fun banUser(threadId: Long, recipient: Recipient): ResultOf = + suspendCoroutine { continuation -> + val sessionID = recipient.address.toString() + val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!! + OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server) + .success { + continuation.resume(ResultOf.Success(Unit)) + }.fail { error -> + continuation.resumeWithException(error) + } + } + + override suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): ResultOf = + suspendCoroutine { continuation -> + val sessionID = recipient.address.toString() + val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!! + OpenGroupAPIV2.banAndDeleteAll(sessionID, openGroup.room, openGroup.server) + .success { + continuation.resume(ResultOf.Success(Unit)) + }.fail { error -> + continuation.resumeWithException(error) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ResultOf.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ResultOf.kt new file mode 100644 index 000000000..96ae97e51 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ResultOf.kt @@ -0,0 +1,43 @@ +package org.thoughtcrime.securesms.repository + +import kotlinx.coroutines.CancellationException + +sealed class ResultOf { + + data class Success(val value: R) : ResultOf() + + data class Failure(val throwable: Throwable) : ResultOf() + + inline fun onFailure(block: (throwable: Throwable) -> Unit) = this.also { + if (this is Failure) { + block(throwable) + } + } + + inline fun onSuccess(block: (value: T) -> Unit) = this.also { + if (this is Success) { + block(value) + } + } + + inline fun flatMap(mapper: (T) -> R): ResultOf = when (this) { + is Success -> wrap { mapper(value) } + is Failure -> Failure(throwable) + } + + fun getOrThrow(): T = when (this) { + is Success -> value + is Failure -> throw throwable + } + + companion object { + inline fun wrap(block: () -> T): ResultOf = + try { + Success(block()) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Failure(e) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeFragment.kt index 9eecc15b7..e060a8282 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeFragment.kt @@ -2,39 +2,40 @@ package org.thoughtcrime.securesms.util import android.content.res.Configuration import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout -import kotlinx.android.synthetic.main.fragment_scan_qr_code.* -import network.loki.messenger.R +import androidx.fragment.app.Fragment +import network.loki.messenger.databinding.FragmentScanQrCodeBinding import org.thoughtcrime.securesms.qr.ScanListener import org.thoughtcrime.securesms.qr.ScanningThread class ScanQRCodeFragment : Fragment() { + private lateinit var binding: FragmentScanQrCodeBinding private val scanningThread = ScanningThread() var scanListener: ScanListener? = null set(value) { field = value; scanningThread.setScanListener(scanListener) } var message: CharSequence = "" - override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? { - return layoutInflater.inflate(R.layout.fragment_scan_qr_code, viewGroup, false) + override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View { + binding = FragmentScanQrCodeBinding.inflate(layoutInflater, viewGroup, false) + return binding.root } override fun onViewCreated(view: View, bundle: Bundle?) { super.onViewCreated(view, bundle) when (resources.configuration.orientation) { - Configuration.ORIENTATION_LANDSCAPE -> overlayView.orientation = LinearLayout.HORIZONTAL - else -> overlayView.orientation = LinearLayout.VERTICAL + Configuration.ORIENTATION_LANDSCAPE -> binding.overlayView.orientation = LinearLayout.HORIZONTAL + else -> binding.overlayView.orientation = LinearLayout.VERTICAL } - messageTextView.text = message + binding.messageTextView.text = message } override fun onResume() { super.onResume() - cameraView.onResume() - cameraView.setPreviewCallback(scanningThread) + binding.cameraView.onResume() + binding.cameraView.setPreviewCallback(scanningThread) try { scanningThread.start() } catch (exception: Exception) { @@ -45,18 +46,18 @@ class ScanQRCodeFragment : Fragment() { override fun onConfigurationChanged(newConfiguration: Configuration) { super.onConfigurationChanged(newConfiguration) - this.cameraView.onPause() + binding.cameraView.onPause() when (newConfiguration.orientation) { - Configuration.ORIENTATION_LANDSCAPE -> overlayView.orientation = LinearLayout.HORIZONTAL - else -> overlayView.orientation = LinearLayout.VERTICAL + Configuration.ORIENTATION_LANDSCAPE -> binding.overlayView.orientation = LinearLayout.HORIZONTAL + else -> binding.overlayView.orientation = LinearLayout.VERTICAL } - cameraView.onResume() - cameraView.setPreviewCallback(scanningThread) + binding.cameraView.onResume() + binding.cameraView.setPreviewCallback(scanningThread) } override fun onPause() { super.onPause() - this.cameraView.onPause() + this.binding.cameraView.onPause() this.scanningThread.stopScanning() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodePlaceholderFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodePlaceholderFragment.kt index 5b36de7ae..7e14b7234 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodePlaceholderFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodePlaceholderFragment.kt @@ -1,23 +1,24 @@ package org.thoughtcrime.securesms.util import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import kotlinx.android.synthetic.main.fragment_scan_qr_code_placeholder.* -import network.loki.messenger.R +import androidx.fragment.app.Fragment +import network.loki.messenger.databinding.FragmentScanQrCodePlaceholderBinding class ScanQRCodePlaceholderFragment: Fragment() { + private lateinit var binding: FragmentScanQrCodePlaceholderBinding var delegate: ScanQRCodePlaceholderFragmentDelegate? = null - override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? { - return layoutInflater.inflate(R.layout.fragment_scan_qr_code_placeholder, viewGroup, false) + override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View { + binding = FragmentScanQrCodePlaceholderBinding.inflate(layoutInflater, viewGroup, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - grantCameraAccessButton.setOnClickListener { delegate?.requestCameraAccess() } + binding.grantCameraAccessButton.setOnClickListener { delegate?.requestCameraAccess() } } } diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index 87bd87eb2..ad1e42d30 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -1,6 +1,5 @@ - + android:layout_height="wrap_content" /> @@ -91,7 +90,9 @@ android:layout_height="match_parent" android:paddingBottom="172dp" android:clipToPadding="false" - tools:listitem="@layout/view_conversation"/> + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:itemCount="6" + tools:listitem="@layout/view_conversation" /> + android:paddingBottom="32dp" + android:visibility="gone"> + android:textSize="@dimen/medium_font_size" />