Browse Source

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
pull/827/head
ceokot 11 months ago committed by GitHub
parent
commit
c113a447cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 49
      app/build.gradle
  2. 14
      app/src/main/java/org/thoughtcrime/securesms/components/LabeledSeparatorView.kt
  3. 37
      app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
  4. 27
      app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.kt
  5. 45
      app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt
  6. 21
      app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt
  7. 34
      app/src/main/java/org/thoughtcrime/securesms/contacts/UserView.kt
  8. 720
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
  9. 15
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
  10. 10
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRecyclerView.kt
  11. 130
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
  12. 24
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt
  13. 25
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt
  14. 24
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt
  15. 40
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt
  16. 24
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/LinkPreviewDraftView.kt
  17. 2
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateSelectionView.kt
  18. 19
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt
  19. 16
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/OpenGroupGuidelinesView.kt
  20. 10
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/TypingIndicatorViewContainer.kt
  21. 14
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt
  22. 14
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/DownloadDialog.kt
  23. 14
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt
  24. 11
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/LinkPreviewDialog.kt
  25. 11
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt
  26. 69
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt
  27. 4
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt
  28. 62
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt
  29. 20
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt
  30. 3
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidatesView.kt
  31. 25
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
  32. 24
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt
  33. 16
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt
  34. 14
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt
  35. 17
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt
  36. 20
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/OpenGroupInvitationView.kt
  37. 131
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt
  38. 12
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/UntrustedAttachmentView.kt
  39. 89
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt
  40. 165
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
  41. 21
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt
  42. 9
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt
  43. 19
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt
  44. 22
      app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt
  45. 81
      app/src/main/java/org/thoughtcrime/securesms/dms/CreatePrivateChatActivity.kt
  46. 15
      app/src/main/java/org/thoughtcrime/securesms/groups/ClosedGroupEditingOptionsBottomSheet.kt
  47. 27
      app/src/main/java/org/thoughtcrime/securesms/groups/CreateClosedGroupActivity.kt
  48. 6
      app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt
  49. 65
      app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt
  50. 8
      app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupGuidelinesActivity.kt
  51. 64
      app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
  52. 75
      app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
  53. 170
      app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
  54. 15
      app/src/main/java/org/thoughtcrime/securesms/home/HomeAdapter.kt
  55. 29
      app/src/main/java/org/thoughtcrime/securesms/home/PathActivity.kt
  56. 113
      app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
  57. 4
      app/src/main/java/org/thoughtcrime/securesms/mediasend/Camera1Fragment.java
  58. 4
      app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaPickerItemFragment.java
  59. 4
      app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
  60. 38
      app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt
  61. 30
      app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt
  62. 18
      app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt
  63. 57
      app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt
  64. 50
      app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt
  65. 17
      app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt
  66. 19
      app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt
  67. 57
      app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt
  68. 25
      app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt
  69. 42
      app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt
  70. 28
      app/src/main/java/org/thoughtcrime/securesms/preferences/QRCodeActivity.kt
  71. 12
      app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt
  72. 96
      app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt
  73. 13
      app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt
  74. 229
      app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
  75. 43
      app/src/main/java/org/thoughtcrime/securesms/repository/ResultOf.kt
  76. 33
      app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodeFragment.kt
  77. 13
      app/src/main/java/org/thoughtcrime/securesms/util/ScanQRCodePlaceholderFragment.kt
  78. 20
      app/src/main/res/layout/activity_home.xml
  79. 108
      app/src/main/res/layout/message_audio_view.xml
  80. 15
      app/src/sharedTest/java/org/thoughtcrime/securesms/BaseCoroutineTest.kt
  81. 50
      app/src/sharedTest/java/org/thoughtcrime/securesms/CoroutineTestRule.kt
  82. 60
      app/src/sharedTest/java/org/thoughtcrime/securesms/LiveDataTestUtil.kt
  83. 11
      app/src/test/java/org/thoughtcrime/securesms/BaseViewModelTest.kt
  84. 173
      app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt
  85. 18
      app/src/test/java/org/thoughtcrime/securesms/jobs/FastJobStorageTest.java
  86. 55
      app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientExporterTest.java
  87. 61
      app/src/test/java/org/thoughtcrime/securesms/service/VerificationCodeParserTest.java
  88. 56
      app/src/test/java/org/thoughtcrime/securesms/util/DelimiterUtilTest.java
  89. 50
      app/src/test/java/org/thoughtcrime/securesms/util/DelimiterUtilTest.kt
  90. 67
      app/src/test/java/org/thoughtcrime/securesms/util/PhoneNumberFormatterTest.java
  91. 1
      app/src/test/java/org/thoughtcrime/securesms/util/dynamiclanguage/LanguageStringTest.java
  92. 2
      app/src/test/java/org/thoughtcrime/securesms/util/dynamiclanguage/LocaleParserTest.java
  93. 4
      build.gradle
  94. 10
      gradle.properties
  95. 5
      gradle/wrapper/gradle-wrapper.properties
  96. 7
      libsession/build.gradle
  97. 24
      libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt
  98. 1557
      libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt

49
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
}
}

14
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)

37
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
}
}

27
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<RecyclerView.ViewHolder>() {
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)
}
}

45
app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListFragment.kt

@ -1,23 +1,21 @@
package org.thoughtcrime.securesms.contacts
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.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<List<ContactSelectionListItem>>, ContactClickListener {
private lateinit var binding: ContactSelectionListFragmentBinding
private var cursorFilter: String? = null
var onContactSelectedListener: OnContactSelectedListener? = null
@ -46,20 +44,21 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
fun onContactDeselected(number: String?)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recyclerView.layoutManager = LinearLayoutManager(activity)
recyclerView.adapter = listAdapter
swipeRefreshLayout.isEnabled = requireActivity().intent.getBooleanExtra(REFRESHABLE, true)
}
override fun onStart() {
super.onStart()
LoaderManager.getInstance(this).initLoader(0, null, this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.contact_selection_list_fragment, container, false)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = ContactSelectionListFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.layoutManager = LinearLayoutManager(activity)
binding.recyclerView.adapter = listAdapter
binding.swipeRefreshLayout.isEnabled = requireActivity().intent.getBooleanExtra(REFRESHABLE, true)
}
override fun onStop() {
@ -74,15 +73,15 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
fun resetQueryFilter() {
setQueryFilter(null)
swipeRefreshLayout.isRefreshing = false
binding.swipeRefreshLayout.isRefreshing = false
}
fun setRefreshing(refreshing: Boolean) {
swipeRefreshLayout.isRefreshing = refreshing
binding.swipeRefreshLayout.isRefreshing = refreshing
}
fun setOnRefreshListener(onRefreshListener: OnRefreshListener?) {
swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
binding.swipeRefreshLayout.setOnRefreshListener(onRefreshListener)
}
override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ContactSelectionListItem>> {
@ -107,8 +106,8 @@ class ContactSelectionListFragment : Fragment(), LoaderManager.LoaderCallbacks<L
return
}
listAdapter.items = items
mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
binding.mainContentContainer.visibility = if (items.isEmpty()) View.GONE else View.VISIBLE
binding.emptyStateContainer.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE
}
override fun onContactClick(contact: Recipient) {

21
app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt

@ -9,16 +9,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
import android.view.Menu
import android.view.MenuItem
import android.view.View
import kotlinx.android.synthetic.main.activity_create_closed_group.emptyStateContainer
import kotlinx.android.synthetic.main.activity_create_closed_group.mainContentContainer
import kotlinx.android.synthetic.main.activity_select_contacts.*
import kotlinx.android.synthetic.main.activity_select_contacts.recyclerView
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySelectContactsBinding
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.mms.GlideApp
//TODO Refactor to avoid using kotlinx.android.synthetic
class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
private lateinit var binding: ActivitySelectContactsBinding
private var members = listOf<String>()
set(value) { field = value; selectContactsAdapter.members = value }
private lateinit var usersToExclude: Set<String>
@ -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<String>) {
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

34
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() {

720
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt

File diff suppressed because it is too large Load Diff

15
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) {

10
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)
}
}

130
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<ConversationUiState> = _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<Recipient>) {
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<MessageRecord>) = 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 <T : ViewModel> create(modelClass: Class<T>): 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<UiMessage> = emptyList()
)

24
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() {

25
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
}
}
}

24
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()
}
}
}

40
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<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
binding.albumCellContainer.findViewById<ViewGroup>(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<TextView>(R.id.album_cell_overflow_text)?.let { overflowText ->
binding.albumCellContainer.findViewById<TextView>(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<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
1 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
2 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
3 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
4 -> albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
0 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
1 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
2 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)
3 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_4)
4 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_5)
else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position")
}

24
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() {

2
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

19
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