mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 02:55:23 +00:00
[SES-557] Update MessageDetailActivity with Compose
This commit is contained in:
commit
d39cf2754c
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@ -13,6 +14,11 @@ buildscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id 'kotlin-kapt'
|
||||||
|
id 'com.google.dagger.hilt.android'
|
||||||
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'witness'
|
apply plugin: 'witness'
|
||||||
@ -22,11 +28,16 @@ apply plugin: 'com.google.gms.google-services'
|
|||||||
apply plugin: 'kotlinx-serialization'
|
apply plugin: 'kotlinx-serialization'
|
||||||
apply plugin: 'dagger.hilt.android.plugin'
|
apply plugin: 'dagger.hilt.android.plugin'
|
||||||
|
|
||||||
|
|
||||||
configurations.all {
|
configurations.all {
|
||||||
exclude module: "commons-logging"
|
exclude module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
implementation("com.google.dagger:hilt-android:2.46.1")
|
||||||
|
kapt("com.google.dagger:hilt-android-compiler:2.44")
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
implementation "com.google.android.material:material:$materialVersion"
|
implementation "com.google.android.material:material:$materialVersion"
|
||||||
@ -39,7 +50,6 @@ dependencies {
|
|||||||
implementation 'androidx.exifinterface:exifinterface:1.3.4'
|
implementation 'androidx.exifinterface:exifinterface:1.3.4'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
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-runtime-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
|
||||||
@ -154,6 +164,16 @@ dependencies {
|
|||||||
|
|
||||||
testImplementation 'org.robolectric:robolectric:4.4'
|
testImplementation 'org.robolectric:robolectric:4.4'
|
||||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||||
|
|
||||||
|
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.1'
|
||||||
|
implementation 'androidx.compose.ui:ui:1.4.3'
|
||||||
|
implementation 'androidx.compose.ui:ui-tooling:1.4.3'
|
||||||
|
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.31.5-beta"
|
||||||
|
implementation "com.google.accompanist:accompanist-pager-indicators:0.31.5-beta"
|
||||||
|
implementation "androidx.compose.runtime:runtime-livedata:1.4.3"
|
||||||
|
|
||||||
|
implementation 'androidx.compose.foundation:foundation-layout:1.5.0-alpha02'
|
||||||
|
implementation 'androidx.compose.material:material:1.5.0-alpha02'
|
||||||
}
|
}
|
||||||
|
|
||||||
def canonicalVersionCode = 354
|
def canonicalVersionCode = 354
|
||||||
@ -199,6 +219,13 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose true
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion '1.4.7'
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionCode canonicalVersionCode * postFixSize
|
versionCode canonicalVersionCode * postFixSize
|
||||||
versionName canonicalVersionName
|
versionName canonicalVersionName
|
||||||
@ -305,3 +332,8 @@ def autoResConfig() {
|
|||||||
.collect { matcher -> matcher.group(1) }
|
.collect { matcher -> matcher.group(1) }
|
||||||
.sort()
|
.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow references to generated code
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes = true
|
||||||
|
}
|
||||||
|
@ -147,6 +147,10 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static Intent getPreviewIntent(Context context, MediaPreviewArgs args) {
|
||||||
|
return getPreviewIntent(context, args.getSlide(), args.getMmsRecord(), args.getThread());
|
||||||
|
}
|
||||||
|
|
||||||
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
|
public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms, Recipient threadRecipient) {
|
||||||
Intent previewIntent = null;
|
Intent previewIntent = null;
|
||||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||||
@ -524,7 +528,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
|||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
|
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
@SuppressWarnings("ConstantConditions")
|
|
||||||
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
|
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
|
||||||
mediaPager.setAdapter(adapter);
|
mediaPager.setAdapter(adapter);
|
||||||
adapter.setActive(true);
|
adapter.setActive(true);
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
package org.thoughtcrime.securesms
|
||||||
|
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
|
|
||||||
|
data class MediaPreviewArgs(
|
||||||
|
val slide: Slide,
|
||||||
|
val mmsRecord: MmsMessageRecord?,
|
||||||
|
val thread: Recipient?,
|
||||||
|
)
|
@ -249,18 +249,12 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
viewModel.callState.collect { state ->
|
viewModel.callState.collect { state ->
|
||||||
Log.d("Loki", "Consuming view model state $state")
|
Log.d("Loki", "Consuming view model state $state")
|
||||||
when (state) {
|
when (state) {
|
||||||
CALL_RINGING -> {
|
CALL_RINGING -> if (wantsToAnswer) {
|
||||||
if (wantsToAnswer) {
|
|
||||||
answerCall()
|
answerCall()
|
||||||
wantsToAnswer = false
|
wantsToAnswer = false
|
||||||
}
|
}
|
||||||
}
|
CALL_CONNECTED -> wantsToAnswer = false
|
||||||
CALL_OUTGOING -> {
|
else -> {}
|
||||||
}
|
|
||||||
CALL_CONNECTED -> {
|
|
||||||
wantsToAnswer = false
|
|
||||||
}
|
|
||||||
else -> { /* do nothing */ }
|
|
||||||
}
|
}
|
||||||
updateControls(state)
|
updateControls(state)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
@ -9,6 +10,7 @@ import androidx.annotation.DimenRes
|
|||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
import network.loki.messenger.databinding.ViewProfilePictureBinding
|
||||||
|
import network.loki.messenger.databinding.ViewUserBinding
|
||||||
import org.session.libsession.avatars.ContactColors
|
import org.session.libsession.avatars.ContactColors
|
||||||
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
||||||
import org.session.libsession.avatars.ProfileContactPhoto
|
import org.session.libsession.avatars.ProfileContactPhoto
|
||||||
@ -18,13 +20,14 @@ import org.session.libsession.utilities.Address
|
|||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
class ProfilePictureView @JvmOverloads constructor(
|
class ProfilePictureView @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null
|
context: Context, attrs: AttributeSet? = null
|
||||||
) : RelativeLayout(context, attrs) {
|
) : RelativeLayout(context, attrs) {
|
||||||
private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) }
|
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
|
||||||
lateinit var glide: GlideRequests
|
private val glide: GlideRequests = GlideApp.with(this)
|
||||||
var publicKey: String? = null
|
var publicKey: String? = null
|
||||||
var displayName: String? = null
|
var displayName: String? = null
|
||||||
var additionalPublicKey: String? = null
|
var additionalPublicKey: String? = null
|
||||||
@ -37,8 +40,13 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
|
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
|
||||||
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
|
||||||
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
constructor(context: Context, sender: Recipient): this(context) {
|
||||||
|
update(sender)
|
||||||
|
}
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
fun update(recipient: Recipient) {
|
fun update(recipient: Recipient) {
|
||||||
fun getUserDisplayName(publicKey: String): String {
|
fun getUserDisplayName(publicKey: String): String {
|
||||||
@ -80,7 +88,6 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun update() {
|
fun update() {
|
||||||
if (!this::glide.isInitialized) return
|
|
||||||
val publicKey = publicKey ?: return
|
val publicKey = publicKey ?: return
|
||||||
val additionalPublicKey = additionalPublicKey
|
val additionalPublicKey = additionalPublicKey
|
||||||
if (additionalPublicKey != null) {
|
if (additionalPublicKey != null) {
|
||||||
|
@ -55,8 +55,7 @@ class UserView : LinearLayout {
|
|||||||
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
|
||||||
}
|
}
|
||||||
val address = user.address.serialize()
|
val address = user.address.serialize()
|
||||||
binding.profilePictureView.root.glide = glide
|
binding.profilePictureView.update(user)
|
||||||
binding.profilePictureView.root.update(user)
|
|
||||||
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
|
||||||
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
|
||||||
when (actionIndicator) {
|
when (actionIndicator) {
|
||||||
@ -88,7 +87,7 @@ class UserView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun unbind() {
|
fun unbind() {
|
||||||
binding.profilePictureView.root.recycle()
|
binding.profilePictureView.recycle()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
@ -32,14 +32,13 @@ class ContactListAdapter(
|
|||||||
|
|
||||||
class ContactViewHolder(private val binding: ViewContactBinding) : RecyclerView.ViewHolder(binding.root) {
|
class ContactViewHolder(private val binding: ViewContactBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(contact: ContactListItem.Contact, glide: GlideRequests, listener: (Recipient) -> Unit) {
|
fun bind(contact: ContactListItem.Contact, glide: GlideRequests, listener: (Recipient) -> Unit) {
|
||||||
binding.profilePictureView.root.glide = glide
|
binding.profilePictureView.update(contact.recipient)
|
||||||
binding.profilePictureView.root.update(contact.recipient)
|
|
||||||
binding.nameTextView.text = contact.displayName
|
binding.nameTextView.text = contact.displayName
|
||||||
binding.root.setOnClickListener { listener(contact.recipient) }
|
binding.root.setOnClickListener { listener(contact.recipient) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unbind() {
|
fun unbind() {
|
||||||
binding.profilePictureView.root.recycle()
|
binding.profilePictureView.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,8 @@ import android.view.WindowManager
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.DimenRes
|
import androidx.annotation.DimenRes
|
||||||
import androidx.core.text.set
|
import androidx.core.text.set
|
||||||
@ -106,6 +108,10 @@ import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.sele
|
|||||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
|
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
|
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener
|
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_REPLY
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_DELETE
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
|
||||||
import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog
|
import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog
|
||||||
@ -582,10 +588,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
R.dimen.small_profile_picture_size
|
R.dimen.small_profile_picture_size
|
||||||
}
|
}
|
||||||
val size = resources.getDimension(sizeID).roundToInt()
|
val size = resources.getDimension(sizeID).roundToInt()
|
||||||
binding.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
|
binding.toolbarContent.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
|
||||||
binding.toolbarContent.profilePictureView.root.glide = glide
|
|
||||||
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
|
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
|
||||||
val profilePictureView = binding.toolbarContent.profilePictureView.root
|
val profilePictureView = binding.toolbarContent.profilePictureView
|
||||||
viewModel.recipient?.let(profilePictureView::update)
|
viewModel.recipient?.let(profilePictureView::update)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -795,7 +800,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
updateSendAfterApprovalText()
|
updateSendAfterApprovalText()
|
||||||
showOrHideInputIfNeeded()
|
showOrHideInputIfNeeded()
|
||||||
|
|
||||||
binding?.toolbarContent?.profilePictureView?.root?.update(threadRecipient)
|
binding?.toolbarContent?.profilePictureView?.update(threadRecipient)
|
||||||
binding?.toolbarContent?.conversationTitleView?.text = when {
|
binding?.toolbarContent?.conversationTitleView?.text = when {
|
||||||
threadRecipient.isLocalNumber -> getString(R.string.note_to_self)
|
threadRecipient.isLocalNumber -> getString(R.string.note_to_self)
|
||||||
else -> threadRecipient.toShortString()
|
else -> threadRecipient.toShortString()
|
||||||
@ -1921,10 +1926,24 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
endActionMode()
|
endActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val handleMessageDetail = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||||
|
val message = result.data?.extras?.getLong(MESSAGE_TIMESTAMP)
|
||||||
|
?.let(mmsSmsDb::getMessageForTimestamp)
|
||||||
|
|
||||||
|
val set = setOfNotNull(message)
|
||||||
|
|
||||||
|
when (result.resultCode) {
|
||||||
|
ON_REPLY -> reply(set)
|
||||||
|
ON_RESEND -> resendMessage(set)
|
||||||
|
ON_DELETE -> deleteMessages(set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun showMessageDetail(messages: Set<MessageRecord>) {
|
override fun showMessageDetail(messages: Set<MessageRecord>) {
|
||||||
val intent = Intent(this, MessageDetailActivity::class.java)
|
Intent(this, MessageDetailActivity::class.java)
|
||||||
intent.putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, messages.first().timestamp)
|
.apply { putExtra(MESSAGE_TIMESTAMP, messages.first().timestamp) }
|
||||||
push(intent)
|
.let { handleMessageDetail.launch(it) }
|
||||||
|
|
||||||
endActionMode()
|
endActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1963,7 +1982,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
override fun reply(messages: Set<MessageRecord>) {
|
override fun reply(messages: Set<MessageRecord>) {
|
||||||
val recipient = viewModel.recipient ?: return
|
val recipient = viewModel.recipient ?: return
|
||||||
binding?.inputBar?.draftQuote(recipient, messages.first(), glide)
|
messages.firstOrNull()?.let { binding?.inputBar?.draftQuote(recipient, it, glide) }
|
||||||
endActionMode()
|
endActionMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -695,9 +695,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||||||
items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.conversation_context__menu_ban_and_delete_all), () -> handleActionItemClicked(Action.BAN_AND_DELETE_ALL)));
|
items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.conversation_context__menu_ban_and_delete_all), () -> handleActionItemClicked(Action.BAN_AND_DELETE_ALL)));
|
||||||
}
|
}
|
||||||
// Message detail
|
// Message detail
|
||||||
if (message.isFailed()) {
|
|
||||||
items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
|
items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO)));
|
||||||
}
|
|
||||||
// Resend
|
// Resend
|
||||||
if (message.isFailed()) {
|
if (message.isFailed()) {
|
||||||
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
|
items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resend_message), () -> handleActionItemClicked(Action.RESEND)));
|
||||||
|
@ -1,99 +1,401 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.v2
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.LayoutInflater
|
||||||
import androidx.core.view.isVisible
|
import android.view.MotionEvent.ACTION_UP
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.LocalTextStyle
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||||
|
import com.bumptech.glide.integration.compose.GlideImage
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityMessageDetailBinding
|
import network.loki.messenger.databinding.ViewVisibleMessageContentBinding
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
|
||||||
import org.session.libsession.messaging.utilities.SessionId
|
|
||||||
import org.session.libsession.messaging.utilities.SodiumUtilities
|
|
||||||
import org.session.libsession.snode.SnodeAPI
|
|
||||||
import org.session.libsession.utilities.Address
|
|
||||||
import org.session.libsession.utilities.ExpirationUtil
|
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
|
||||||
import org.session.libsignal.utilities.IdPrefix
|
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
|
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.ui.Avatar
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.ui.CarouselNextButton
|
||||||
import java.text.SimpleDateFormat
|
import org.thoughtcrime.securesms.ui.CarouselPrevButton
|
||||||
import java.util.*
|
import org.thoughtcrime.securesms.ui.Cell
|
||||||
|
import org.thoughtcrime.securesms.ui.CellNoMargin
|
||||||
|
import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin
|
||||||
|
import org.thoughtcrime.securesms.ui.Divider
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator
|
||||||
|
import org.thoughtcrime.securesms.ui.ItemButton
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.TitledText
|
||||||
|
import org.thoughtcrime.securesms.ui.blackAlpha40
|
||||||
|
import org.thoughtcrime.securesms.ui.colorDestructive
|
||||||
|
import org.thoughtcrime.securesms.ui.destructiveButtonColors
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
|
||||||
private lateinit var binding: ActivityMessageDetailBinding
|
|
||||||
var messageRecord: MessageRecord? = null
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var storage: Storage
|
lateinit var storage: Storage
|
||||||
|
|
||||||
// region Settings
|
private val viewModel: MessageDetailsViewModel by viewModels()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Extras
|
// Extras
|
||||||
const val MESSAGE_TIMESTAMP = "message_timestamp"
|
const val MESSAGE_TIMESTAMP = "message_timestamp"
|
||||||
|
|
||||||
|
const val ON_REPLY = 1
|
||||||
|
const val ON_RESEND = 2
|
||||||
|
const val ON_DELETE = 3
|
||||||
}
|
}
|
||||||
// endregion
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
super.onCreate(savedInstanceState, ready)
|
super.onCreate(savedInstanceState, ready)
|
||||||
binding = ActivityMessageDetailBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
title = resources.getString(R.string.conversation_context__menu_message_details)
|
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,
|
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
|
||||||
// so the author of the messages must be the current user.
|
|
||||||
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
|
ComposeView(this)
|
||||||
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) ?: run {
|
.apply { setContent { MessageDetailsScreen() } }
|
||||||
finish()
|
.let(::setContentView)
|
||||||
return
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.eventFlow.collect {
|
||||||
|
when (it) {
|
||||||
|
Event.Finish -> finish()
|
||||||
|
is Event.StartMediaPreview -> startActivity(
|
||||||
|
getPreviewIntent(this@MessageDetailActivity, it.args)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val threadId = messageRecord!!.threadId
|
|
||||||
val openGroup = storage.getOpenGroup(threadId)
|
|
||||||
val blindedKey = openGroup?.let { group ->
|
|
||||||
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return@let null
|
|
||||||
val blindingEnabled = storage.getServerCapabilities(group.server).contains(OpenGroupApi.Capability.BLIND.name.lowercase())
|
|
||||||
if (blindingEnabled) {
|
|
||||||
SodiumUtilities.blindedKeyPair(group.publicKey, userEdKeyPair)?.publicKey?.asBytes
|
|
||||||
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
|
|
||||||
} else null
|
|
||||||
}
|
}
|
||||||
updateContent()
|
}
|
||||||
binding.resendButton.setOnClickListener {
|
}
|
||||||
ResendMessageUtilities.resend(this, messageRecord!!, blindedKey)
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageDetailsScreen() {
|
||||||
|
val state by viewModel.stateFlow.collectAsState()
|
||||||
|
AppTheme {
|
||||||
|
MessageDetails(
|
||||||
|
state = state,
|
||||||
|
onReply = { setResultAndFinish(ON_REPLY) },
|
||||||
|
onResend = state.error?.let { { setResultAndFinish(ON_RESEND) } },
|
||||||
|
onDelete = { setResultAndFinish(ON_DELETE) },
|
||||||
|
onClickImage = { viewModel.onClickImage(it) },
|
||||||
|
onAttachmentNeedsDownload = viewModel::onAttachmentNeedsDownload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setResultAndFinish(code: Int) {
|
||||||
|
Bundle().apply { putLong(MESSAGE_TIMESTAMP, viewModel.timestamp) }
|
||||||
|
.let(Intent()::putExtras)
|
||||||
|
.let { setResult(code, it) }
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateContent() {
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
val dateLocale = Locale.getDefault()
|
@Composable
|
||||||
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
|
fun MessageDetails(
|
||||||
binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
|
state: MessageDetailsState,
|
||||||
|
onReply: () -> Unit = {},
|
||||||
|
onResend: (() -> Unit)? = null,
|
||||||
|
onDelete: () -> Unit = {},
|
||||||
|
onClickImage: (Int) -> Unit = {},
|
||||||
|
onAttachmentNeedsDownload: (Long, Long) -> Unit = { _, _ -> }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
state.record?.let { message ->
|
||||||
|
AndroidView(
|
||||||
|
modifier = Modifier.padding(horizontal = 32.dp),
|
||||||
|
factory = {
|
||||||
|
ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(it)).mainContainerConstraint.apply {
|
||||||
|
bind(
|
||||||
|
message,
|
||||||
|
thread = state.thread!!,
|
||||||
|
onAttachmentNeedsDownload = onAttachmentNeedsDownload,
|
||||||
|
suppressThumbnails = true
|
||||||
|
)
|
||||||
|
|
||||||
val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId())
|
setOnTouchListener { _, event ->
|
||||||
if (errorMessage != null) {
|
if (event.actionMasked == ACTION_UP) onContentClick(event)
|
||||||
binding.errorMessage.text = errorMessage
|
true
|
||||||
binding.resendContainer.isVisible = true
|
}
|
||||||
binding.errorContainer.isVisible = true
|
}
|
||||||
} else {
|
}
|
||||||
binding.errorContainer.isVisible = false
|
)
|
||||||
binding.resendContainer.isVisible = false
|
}
|
||||||
|
Carousel(state.imageAttachments) { onClickImage(it) }
|
||||||
|
state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
|
||||||
|
CellMetadata(state)
|
||||||
|
CellButtons(
|
||||||
|
onReply,
|
||||||
|
onResend,
|
||||||
|
onDelete,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
|
@Composable
|
||||||
binding.expiresContainer.visibility = View.GONE
|
fun CellMetadata(
|
||||||
} else {
|
state: MessageDetailsState,
|
||||||
binding.expiresContainer.visibility = View.VISIBLE
|
) {
|
||||||
val elapsed = SnodeAPI.nowWithOffset - messageRecord!!.expireStarted
|
state.apply {
|
||||||
val remaining = messageRecord!!.expiresIn - elapsed
|
if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return
|
||||||
|
CellWithPaddingAndMargin {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
TitledText(sent)
|
||||||
|
TitledText(received)
|
||||||
|
TitledErrorText(error)
|
||||||
|
senderInfo?.let {
|
||||||
|
TitledView(state.fromTitle) {
|
||||||
|
Row {
|
||||||
|
sender?.let { Avatar(it) }
|
||||||
|
TitledMonospaceText(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
|
@Composable
|
||||||
binding.expiresIn.text = duration
|
fun CellButtons(
|
||||||
|
onReply: () -> Unit = {},
|
||||||
|
onResend: (() -> Unit)? = null,
|
||||||
|
onDelete: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
Cell {
|
||||||
|
Column {
|
||||||
|
ItemButton(
|
||||||
|
stringResource(R.string.reply),
|
||||||
|
R.drawable.ic_message_details__reply,
|
||||||
|
onClick = onReply
|
||||||
|
)
|
||||||
|
Divider()
|
||||||
|
onResend?.let {
|
||||||
|
ItemButton(
|
||||||
|
stringResource(R.string.resend),
|
||||||
|
R.drawable.ic_message_details__refresh,
|
||||||
|
onClick = it
|
||||||
|
)
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
ItemButton(
|
||||||
|
stringResource(R.string.delete),
|
||||||
|
R.drawable.ic_message_details__trash,
|
||||||
|
colors = destructiveButtonColors(),
|
||||||
|
onClick = onDelete
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun Carousel(attachments: List<Attachment>, onClick: (Int) -> Unit) {
|
||||||
|
if (attachments.isEmpty()) return
|
||||||
|
|
||||||
|
val pagerState = rememberPagerState { attachments.size }
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Row {
|
||||||
|
CarouselPrevButton(pagerState)
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
CellCarousel(pagerState, attachments, onClick)
|
||||||
|
HorizontalPagerIndicator(pagerState)
|
||||||
|
ExpandButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(8.dp)
|
||||||
|
) { onClick(pagerState.currentPage) }
|
||||||
|
}
|
||||||
|
CarouselNextButton(pagerState)
|
||||||
|
}
|
||||||
|
attachments.getOrNull(pagerState.currentPage)?.fileDetails?.let { FileDetails(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(
|
||||||
|
ExperimentalFoundationApi::class,
|
||||||
|
ExperimentalGlideComposeApi::class
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
private fun CellCarousel(
|
||||||
|
pagerState: PagerState,
|
||||||
|
attachments: List<Attachment>,
|
||||||
|
onClick: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
CellNoMargin {
|
||||||
|
HorizontalPager(state = pagerState) { i ->
|
||||||
|
GlideImage(
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.clickable { onClick(i) },
|
||||||
|
model = attachments[i].uri,
|
||||||
|
contentDescription = attachments[i].fileName ?: stringResource(id = R.string.image)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||||
|
Surface(
|
||||||
|
shape = CircleShape,
|
||||||
|
color = blackAlpha40,
|
||||||
|
modifier = modifier,
|
||||||
|
contentColor = Color.White,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_expand),
|
||||||
|
contentDescription = stringResource(id = R.string.expand),
|
||||||
|
modifier = Modifier.clickable { onClick() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun PreviewMessageDetails(
|
||||||
|
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
||||||
|
) {
|
||||||
|
PreviewTheme(themeResId) {
|
||||||
|
MessageDetails(
|
||||||
|
state = MessageDetailsState(
|
||||||
|
nonImageAttachmentFileDetails = listOf(
|
||||||
|
TitledText(R.string.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"),
|
||||||
|
TitledText(R.string.message_details_header__file_type, "image/png"),
|
||||||
|
TitledText(R.string.message_details_header__file_size, "195.6kB"),
|
||||||
|
TitledText(R.string.message_details_header__resolution, "342x312"),
|
||||||
|
),
|
||||||
|
sent = TitledText(R.string.message_details_header__sent, "6:12 AM Tue, 09/08/2022"),
|
||||||
|
received = TitledText(R.string.message_details_header__received, "6:12 AM Tue, 09/08/2022"),
|
||||||
|
error = TitledText(R.string.message_details_header__error, "Message failed to send"),
|
||||||
|
senderInfo = TitledText("Connor", "d4f1g54sdf5g1d5f4g65ds4564df65f4g65d54"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FileDetails(fileDetails: List<TitledText>) {
|
||||||
|
if (fileDetails.isEmpty()) return
|
||||||
|
|
||||||
|
CellWithPaddingAndMargin(padding = 0.dp) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(vertical = 24.dp, horizontal = 12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
fileDetails.forEach {
|
||||||
|
BoxWithConstraints {
|
||||||
|
TitledText(
|
||||||
|
it,
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(min = maxWidth.div(2))
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
.width(IntrinsicSize.Max)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TitledErrorText(titledText: TitledText?) {
|
||||||
|
TitledText(
|
||||||
|
titledText,
|
||||||
|
valueStyle = LocalTextStyle.current.copy(color = colorDestructive)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TitledMonospaceText(titledText: TitledText?) {
|
||||||
|
TitledText(
|
||||||
|
titledText,
|
||||||
|
valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TitledText(
|
||||||
|
titledText: TitledText?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
valueStyle: TextStyle = LocalTextStyle.current,
|
||||||
|
) {
|
||||||
|
titledText?.apply {
|
||||||
|
TitledView(title, modifier) {
|
||||||
|
Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TitledView(title: GetString, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
||||||
|
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
Title(title)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Title(title: GetString) {
|
||||||
|
Text(title.string(), fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.messaging.jobs.AttachmentDownloadJob
|
||||||
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
|
||||||
|
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||||
|
import org.session.libsession.utilities.Util
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.MediaPreviewArgs
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
|
import org.thoughtcrime.securesms.mms.ImageSlide
|
||||||
|
import org.thoughtcrime.securesms.mms.Slide
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import org.thoughtcrime.securesms.ui.TitledText
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class MessageDetailsViewModel @Inject constructor(
|
||||||
|
private val attachmentDb: AttachmentDatabase,
|
||||||
|
private val lokiMessageDatabase: LokiMessageDatabase,
|
||||||
|
private val mmsSmsDatabase: MmsSmsDatabase,
|
||||||
|
private val threadDb: ThreadDatabase,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val state = MutableStateFlow(MessageDetailsState())
|
||||||
|
val stateFlow = state.asStateFlow()
|
||||||
|
|
||||||
|
private val event = Channel<Event>()
|
||||||
|
val eventFlow = event.receiveAsFlow()
|
||||||
|
|
||||||
|
var timestamp: Long = 0L
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
val record = mmsSmsDatabase.getMessageForTimestamp(timestamp)
|
||||||
|
|
||||||
|
if (record == null) {
|
||||||
|
viewModelScope.launch { event.send(Event.Finish) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val mmsRecord = record as? MmsMessageRecord
|
||||||
|
|
||||||
|
state.value = record.run {
|
||||||
|
val slides = mmsRecord?.slideDeck?.slides ?: emptyList()
|
||||||
|
|
||||||
|
MessageDetailsState(
|
||||||
|
attachments = slides.map(::Attachment),
|
||||||
|
record = record,
|
||||||
|
sent = dateSent.let(::Date).toString().let { TitledText(R.string.message_details_header__sent, it) },
|
||||||
|
received = dateReceived.let(::Date).toString().let { TitledText(R.string.message_details_header__received, it) },
|
||||||
|
error = lokiMessageDatabase.getErrorMessage(id)?.let { TitledText(R.string.message_details_header__error, it) },
|
||||||
|
senderInfo = individualRecipient.run { name?.let { TitledText(it, address.serialize()) } },
|
||||||
|
sender = individualRecipient,
|
||||||
|
thread = threadDb.getRecipientForThreadId(threadId)!!,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Slide.details: List<TitledText>
|
||||||
|
get() = listOfNotNull(
|
||||||
|
fileName.orNull()?.let { TitledText(R.string.message_details_header__file_id, it) },
|
||||||
|
TitledText(R.string.message_details_header__file_type, asAttachment().contentType),
|
||||||
|
TitledText(R.string.message_details_header__file_size, Util.getPrettyFileSize(fileSize)),
|
||||||
|
takeIf { it is ImageSlide }
|
||||||
|
?.let(Slide::asAttachment)
|
||||||
|
?.run { "${width}x$height" }
|
||||||
|
?.let { TitledText(R.string.message_details_header__resolution, it) },
|
||||||
|
attachmentDb.duration(this)?.let { TitledText(R.string.message_details_header__duration, it) },
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun AttachmentDatabase.duration(slide: Slide): String? =
|
||||||
|
slide.takeIf { it.hasAudio() }
|
||||||
|
?.run { asAttachment() as? DatabaseAttachment }
|
||||||
|
?.run { getAttachmentAudioExtras(attachmentId)?.durationMs }
|
||||||
|
?.takeIf { it > 0 }
|
||||||
|
?.let {
|
||||||
|
String.format(
|
||||||
|
"%01d:%02d",
|
||||||
|
TimeUnit.MILLISECONDS.toMinutes(it),
|
||||||
|
TimeUnit.MILLISECONDS.toSeconds(it) % 60
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Attachment(slide: Slide): Attachment =
|
||||||
|
Attachment(slide.details, slide.fileName.orNull(), slide.uri, slide is ImageSlide)
|
||||||
|
|
||||||
|
fun onClickImage(index: Int) {
|
||||||
|
val state = state.value ?: return
|
||||||
|
val mmsRecord = state.mmsRecord ?: return
|
||||||
|
val slide = mmsRecord.slideDeck.slides[index] ?: return
|
||||||
|
// only open to downloaded images
|
||||||
|
if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) {
|
||||||
|
// Restart download here (on IO thread)
|
||||||
|
(slide.asAttachment() as? DatabaseAttachment)?.let { attachment ->
|
||||||
|
onAttachmentNeedsDownload(attachment.attachmentId.rowId, state.mmsRecord.getId())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slide.isInProgress) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
MediaPreviewArgs(slide, state.mmsRecord, state.thread)
|
||||||
|
.let(Event::StartMediaPreview)
|
||||||
|
.let { event.send(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAttachmentNeedsDownload(attachmentId: Long, mmsId: Long) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
JobQueue.shared.add(AttachmentDownloadJob(attachmentId, mmsId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MessageDetailsState(
|
||||||
|
val attachments: List<Attachment> = emptyList(),
|
||||||
|
val imageAttachments: List<Attachment> = attachments.filter { it.hasImage },
|
||||||
|
val nonImageAttachmentFileDetails: List<TitledText>? = attachments.firstOrNull { !it.hasImage }?.fileDetails,
|
||||||
|
val record: MessageRecord? = null,
|
||||||
|
val mmsRecord: MmsMessageRecord? = record as? MmsMessageRecord,
|
||||||
|
val sent: TitledText? = null,
|
||||||
|
val received: TitledText? = null,
|
||||||
|
val error: TitledText? = null,
|
||||||
|
val senderInfo: TitledText? = null,
|
||||||
|
val sender: Recipient? = null,
|
||||||
|
val thread: Recipient? = null,
|
||||||
|
) {
|
||||||
|
val fromTitle = GetString(R.string.message_details_header__from)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Attachment(
|
||||||
|
val fileDetails: List<TitledText>,
|
||||||
|
val fileName: String?,
|
||||||
|
val uri: Uri?,
|
||||||
|
val hasImage: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class Event {
|
||||||
|
object Finish: Event()
|
||||||
|
data class StartMediaPreview(val args: MediaPreviewArgs): Event()
|
||||||
|
}
|
@ -28,11 +28,10 @@ class MentionCandidateView : LinearLayout {
|
|||||||
|
|
||||||
private fun update() = with(binding) {
|
private fun update() = with(binding) {
|
||||||
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
mentionCandidateNameTextView.text = mentionCandidate.displayName
|
||||||
profilePictureView.root.publicKey = mentionCandidate.publicKey
|
profilePictureView.publicKey = mentionCandidate.publicKey
|
||||||
profilePictureView.root.displayName = mentionCandidate.displayName
|
profilePictureView.displayName = mentionCandidate.displayName
|
||||||
profilePictureView.root.additionalPublicKey = null
|
profilePictureView.additionalPublicKey = null
|
||||||
profilePictureView.root.glide = glide!!
|
profilePictureView.update()
|
||||||
profilePictureView.root.update()
|
|
||||||
if (openGroupServer != null && openGroupRoom != null) {
|
if (openGroupServer != null && openGroupRoom != null) {
|
||||||
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey)
|
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey)
|
||||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||||
|
@ -28,11 +28,10 @@ class MentionCandidateView : RelativeLayout {
|
|||||||
|
|
||||||
private fun update() = with(binding) {
|
private fun update() = with(binding) {
|
||||||
mentionCandidateNameTextView.text = candidate.displayName
|
mentionCandidateNameTextView.text = candidate.displayName
|
||||||
profilePictureView.root.publicKey = candidate.publicKey
|
profilePictureView.publicKey = candidate.publicKey
|
||||||
profilePictureView.root.displayName = candidate.displayName
|
profilePictureView.displayName = candidate.displayName
|
||||||
profilePictureView.root.additionalPublicKey = null
|
profilePictureView.additionalPublicKey = null
|
||||||
profilePictureView.root.glide = glide!!
|
profilePictureView.update()
|
||||||
profilePictureView.root.update()
|
|
||||||
if (openGroupServer != null && openGroupRoom != null) {
|
if (openGroupServer != null && openGroupRoom != null) {
|
||||||
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", candidate.publicKey)
|
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", candidate.publicKey)
|
||||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||||
|
@ -67,7 +67,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
|
||||||
(thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
|
(thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
|
||||||
// Message detail
|
// Message detail
|
||||||
menu.findItem(R.id.menu_message_details).isVisible = (selectedItems.size == 1 && firstMessage.isOutgoing)
|
menu.findItem(R.id.menu_message_details).isVisible = selectedItems.size == 1
|
||||||
// Resend
|
// Resend
|
||||||
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
|
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
|
||||||
// Resync
|
// Resync
|
||||||
|
@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getInt
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
@ -45,7 +46,6 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
class VisibleMessageContentView : ConstraintLayout {
|
class VisibleMessageContentView : ConstraintLayout {
|
||||||
private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) }
|
private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) }
|
||||||
var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
|
|
||||||
var onContentDoubleTap: (() -> Unit)? = null
|
var onContentDoubleTap: (() -> Unit)? = null
|
||||||
var delegate: VisibleMessageViewDelegate? = null
|
var delegate: VisibleMessageViewDelegate? = null
|
||||||
var indexInAdapter: Int = -1
|
var indexInAdapter: Int = -1
|
||||||
@ -59,13 +59,14 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(
|
fun bind(
|
||||||
message: MessageRecord,
|
message: MessageRecord,
|
||||||
isStartOfMessageCluster: Boolean,
|
isStartOfMessageCluster: Boolean = true,
|
||||||
isEndOfMessageCluster: Boolean,
|
isEndOfMessageCluster: Boolean = true,
|
||||||
glide: GlideRequests,
|
glide: GlideRequests = GlideApp.with(this),
|
||||||
thread: Recipient,
|
thread: Recipient,
|
||||||
searchQuery: String?,
|
searchQuery: String? = null,
|
||||||
contactIsTrusted: Boolean,
|
contactIsTrusted: Boolean = true,
|
||||||
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
onAttachmentNeedsDownload: (Long, Long) -> Unit,
|
||||||
|
suppressThumbnails: Boolean = false
|
||||||
) {
|
) {
|
||||||
// Background
|
// Background
|
||||||
val color = if (message.isOutgoing) context.getAccentColor()
|
val color = if (message.isOutgoing) context.getAccentColor()
|
||||||
@ -184,7 +185,7 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||||||
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty() -> {
|
message is MmsMessageRecord && !suppressThumbnails && message.slideDeck.asAttachments().isNotEmpty() -> {
|
||||||
/*
|
/*
|
||||||
* Images / Video attachment
|
* Images / Video attachment
|
||||||
*/
|
*/
|
||||||
@ -237,6 +238,12 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||||||
binding.contentParent.layoutParams = layoutParams
|
binding.contentParent.layoutParams = layoutParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
|
||||||
|
|
||||||
|
fun onContentClick(event: MotionEvent) {
|
||||||
|
onContentClick.forEach { clickHandler -> clickHandler.invoke(event) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
|
||||||
listOf<View>(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }
|
listOf<View>(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Resources
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
@ -48,6 +47,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase
|
|||||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||||
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
@ -72,7 +72,6 @@ class VisibleMessageView : LinearLayout {
|
|||||||
@Inject lateinit var mmsDb: MmsDatabase
|
@Inject lateinit var mmsDb: MmsDatabase
|
||||||
|
|
||||||
private val binding by lazy { ViewVisibleMessageBinding.bind(this) }
|
private val binding by lazy { ViewVisibleMessageBinding.bind(this) }
|
||||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
|
||||||
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
private val swipeToReplyIcon = ContextCompat.getDrawable(context, R.drawable.ic_baseline_reply_24)!!.mutate()
|
||||||
private val swipeToReplyIconRect = Rect()
|
private val swipeToReplyIconRect = Rect()
|
||||||
private var dx = 0.0f
|
private var dx = 0.0f
|
||||||
@ -124,14 +123,14 @@ class VisibleMessageView : LinearLayout {
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(
|
fun bind(
|
||||||
message: MessageRecord,
|
message: MessageRecord,
|
||||||
previous: MessageRecord?,
|
previous: MessageRecord? = null,
|
||||||
next: MessageRecord?,
|
next: MessageRecord? = null,
|
||||||
glide: GlideRequests,
|
glide: GlideRequests = GlideApp.with(this),
|
||||||
searchQuery: String?,
|
searchQuery: String? = null,
|
||||||
contact: Contact?,
|
contact: Contact? = null,
|
||||||
senderSessionID: String,
|
senderSessionID: String,
|
||||||
lastSeen: Long,
|
lastSeen: Long,
|
||||||
delegate: VisibleMessageViewDelegate?,
|
delegate: VisibleMessageViewDelegate? = null,
|
||||||
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
onAttachmentNeedsDownload: (Long, Long) -> Unit
|
||||||
) {
|
) {
|
||||||
val threadID = message.threadId
|
val threadID = message.threadId
|
||||||
@ -142,7 +141,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
// Show profile picture and sender name if this is a group thread AND
|
// Show profile picture and sender name if this is a group thread AND
|
||||||
// the message is incoming
|
// the message is incoming
|
||||||
binding.moderatorIconImageView.isVisible = false
|
binding.moderatorIconImageView.isVisible = false
|
||||||
binding.profilePictureView.root.visibility = when {
|
binding.profilePictureView.visibility = when {
|
||||||
thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE
|
thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE
|
||||||
thread.isGroupRecipient -> View.INVISIBLE
|
thread.isGroupRecipient -> View.INVISIBLE
|
||||||
else -> View.GONE
|
else -> View.GONE
|
||||||
@ -151,22 +150,21 @@ class VisibleMessageView : LinearLayout {
|
|||||||
val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing)
|
val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing)
|
||||||
else ViewUtil.dpToPx(context,2)
|
else ViewUtil.dpToPx(context,2)
|
||||||
|
|
||||||
if (binding.profilePictureView.root.visibility == View.GONE) {
|
if (binding.profilePictureView.visibility == View.GONE) {
|
||||||
val expirationParams = binding.messageInnerContainer.layoutParams as MarginLayoutParams
|
val expirationParams = binding.messageInnerContainer.layoutParams as MarginLayoutParams
|
||||||
expirationParams.bottomMargin = bottomMargin
|
expirationParams.bottomMargin = bottomMargin
|
||||||
binding.messageInnerContainer.layoutParams = expirationParams
|
binding.messageInnerContainer.layoutParams = expirationParams
|
||||||
} else {
|
} else {
|
||||||
val avatarLayoutParams = binding.profilePictureView.root.layoutParams as MarginLayoutParams
|
val avatarLayoutParams = binding.profilePictureView.layoutParams as MarginLayoutParams
|
||||||
avatarLayoutParams.bottomMargin = bottomMargin
|
avatarLayoutParams.bottomMargin = bottomMargin
|
||||||
binding.profilePictureView.root.layoutParams = avatarLayoutParams
|
binding.profilePictureView.layoutParams = avatarLayoutParams
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGroupThread && !message.isOutgoing) {
|
if (isGroupThread && !message.isOutgoing) {
|
||||||
if (isEndOfMessageCluster) {
|
if (isEndOfMessageCluster) {
|
||||||
binding.profilePictureView.root.publicKey = senderSessionID
|
binding.profilePictureView.publicKey = senderSessionID
|
||||||
binding.profilePictureView.root.glide = glide
|
binding.profilePictureView.update(message.individualRecipient)
|
||||||
binding.profilePictureView.root.update(message.individualRecipient)
|
binding.profilePictureView.setOnClickListener {
|
||||||
binding.profilePictureView.root.setOnClickListener {
|
|
||||||
if (thread.isOpenGroupRecipient) {
|
if (thread.isOpenGroupRecipient) {
|
||||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
|
||||||
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED && openGroup?.canWrite == true) {
|
||||||
@ -398,7 +396,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
|
||||||
val iconSize = toPx(24, context.resources)
|
val iconSize = toPx(24, context.resources)
|
||||||
val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing
|
val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing
|
||||||
val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2)
|
val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.marginBottom - (iconSize / 2)
|
||||||
val right = left + iconSize
|
val right = left + iconSize
|
||||||
val bottom = top + iconSize
|
val bottom = top + iconSize
|
||||||
swipeToReplyIconRect.left = left
|
swipeToReplyIconRect.left = left
|
||||||
@ -418,7 +416,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
binding.profilePictureView.root.recycle()
|
binding.profilePictureView.recycle()
|
||||||
binding.messageContentView.root.recycle()
|
binding.messageContentView.root.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,7 +517,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onContentClick(event: MotionEvent) {
|
fun onContentClick(event: MotionEvent) {
|
||||||
binding.messageContentView.root.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
|
binding.messageContentView.root.onContentClick(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPress(event: MotionEvent) {
|
private fun onPress(event: MotionEvent) {
|
||||||
|
@ -65,7 +65,6 @@ class ConversationView : LinearLayout {
|
|||||||
} else {
|
} else {
|
||||||
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
|
||||||
}
|
}
|
||||||
binding.profilePictureView.root.glide = glide
|
|
||||||
val unreadCount = thread.unreadCount
|
val unreadCount = thread.unreadCount
|
||||||
if (thread.recipient.isBlocked) {
|
if (thread.recipient.isBlocked) {
|
||||||
binding.accentView.setBackgroundResource(R.color.destructive)
|
binding.accentView.setBackgroundResource(R.color.destructive)
|
||||||
@ -125,11 +124,11 @@ class ConversationView : LinearLayout {
|
|||||||
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||||
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
||||||
}
|
}
|
||||||
binding.profilePictureView.root.update(thread.recipient)
|
binding.profilePictureView.update(thread.recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
binding.profilePictureView.root.recycle()
|
binding.profilePictureView.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
private fun getUserDisplayName(recipient: Recipient): String? {
|
||||||
|
@ -168,8 +168,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// Set up Glide
|
// Set up Glide
|
||||||
glide = GlideApp.with(this)
|
glide = GlideApp.with(this)
|
||||||
// Set up toolbar buttons
|
// Set up toolbar buttons
|
||||||
binding.profileButton.root.glide = glide
|
binding.profileButton.setOnClickListener { openSettings() }
|
||||||
binding.profileButton.root.setOnClickListener { openSettings() }
|
|
||||||
binding.searchViewContainer.setOnClickListener {
|
binding.searchViewContainer.setOnClickListener {
|
||||||
binding.globalSearchInputLayout.requestFocus()
|
binding.globalSearchInputLayout.requestFocus()
|
||||||
}
|
}
|
||||||
@ -364,8 +363,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
|
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
|
||||||
if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared
|
if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared
|
||||||
IdentityKeyUtil.checkUpdate(this)
|
IdentityKeyUtil.checkUpdate(this)
|
||||||
binding.profileButton.root.recycle() // clear cached image before update tje profilePictureView
|
binding.profileButton.recycle() // clear cached image before update tje profilePictureView
|
||||||
binding.profileButton.root.update()
|
binding.profileButton.update()
|
||||||
if (textSecurePreferences.getHasViewedSeed()) {
|
if (textSecurePreferences.getHasViewedSeed()) {
|
||||||
binding.seedReminderView.isVisible = false
|
binding.seedReminderView.isVisible = false
|
||||||
}
|
}
|
||||||
@ -440,10 +439,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateProfileButton() {
|
private fun updateProfileButton() {
|
||||||
binding.profileButton.root.publicKey = publicKey
|
binding.profileButton.publicKey = publicKey
|
||||||
binding.profileButton.root.displayName = textSecurePreferences.getProfileName()
|
binding.profileButton.displayName = textSecurePreferences.getProfileName()
|
||||||
binding.profileButton.root.recycle()
|
binding.profileButton.recycle()
|
||||||
binding.profileButton.root.update()
|
binding.profileButton.update()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
@ -53,10 +53,9 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
|
|||||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
|
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
|
||||||
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
|
||||||
with(binding) {
|
with(binding) {
|
||||||
profilePictureView.root.publicKey = publicKey
|
profilePictureView.publicKey = publicKey
|
||||||
profilePictureView.root.glide = GlideApp.with(this@UserDetailsBottomSheet)
|
profilePictureView.isLarge = true
|
||||||
profilePictureView.root.isLarge = true
|
profilePictureView.update(recipient)
|
||||||
profilePictureView.root.update(recipient)
|
|
||||||
nameTextViewContainer.visibility = View.VISIBLE
|
nameTextViewContainer.visibility = View.VISIBLE
|
||||||
nameTextViewContainer.setOnClickListener {
|
nameTextViewContainer.setOnClickListener {
|
||||||
if (recipient.isOpenGroupInboxRecipient || recipient.isOpenGroupOutboxRecipient) return@setOnClickListener
|
if (recipient.isOpenGroupInboxRecipient || recipient.isOpenGroupOutboxRecipient) return@setOnClickListener
|
||||||
|
@ -83,22 +83,20 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
|
|
||||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||||
if (holder is ContentView) {
|
if (holder is ContentView) {
|
||||||
holder.binding.searchResultProfilePicture.root.recycle()
|
holder.binding.searchResultProfilePicture.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
|
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
val binding = ViewGlobalSearchResultBinding.bind(view).apply {
|
val binding = ViewGlobalSearchResultBinding.bind(view)
|
||||||
searchResultProfilePicture.root.glide = GlideApp.with(root)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bindPayload(newQuery: String, model: Model) {
|
fun bindPayload(newQuery: String, model: Model) {
|
||||||
bindQuery(newQuery, model)
|
bindQuery(newQuery, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(query: String, model: Model) {
|
fun bind(query: String, model: Model) {
|
||||||
binding.searchResultProfilePicture.root.recycle()
|
binding.searchResultProfilePicture.recycle()
|
||||||
when (model) {
|
when (model) {
|
||||||
is Model.GroupConversation -> bindModel(query, model)
|
is Model.GroupConversation -> bindModel(query, model)
|
||||||
is Model.Contact -> bindModel(query, model)
|
is Model.Contact -> bindModel(query, model)
|
||||||
|
@ -87,12 +87,12 @@ private fun getHighlight(query: String?, toSearch: String): Spannable? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
||||||
binding.searchResultProfilePicture.root.isVisible = true
|
binding.searchResultProfilePicture.isVisible = true
|
||||||
binding.searchResultSavedMessages.isVisible = false
|
binding.searchResultSavedMessages.isVisible = false
|
||||||
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
|
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
|
||||||
binding.searchResultTimestamp.isVisible = false
|
binding.searchResultTimestamp.isVisible = false
|
||||||
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false)
|
val threadRecipient = Recipient.from(binding.root.context, Address.fromSerialized(model.groupRecord.encodedId), false)
|
||||||
binding.searchResultProfilePicture.root.update(threadRecipient)
|
binding.searchResultProfilePicture.update(threadRecipient)
|
||||||
val nameString = model.groupRecord.title
|
val nameString = model.groupRecord.title
|
||||||
binding.searchResultTitle.text = getHighlight(query, nameString)
|
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||||
|
|
||||||
@ -108,14 +108,14 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ContentView.bindModel(query: String?, model: ContactModel) {
|
fun ContentView.bindModel(query: String?, model: ContactModel) {
|
||||||
binding.searchResultProfilePicture.root.isVisible = true
|
binding.searchResultProfilePicture.isVisible = true
|
||||||
binding.searchResultSavedMessages.isVisible = false
|
binding.searchResultSavedMessages.isVisible = false
|
||||||
binding.searchResultSubtitle.isVisible = false
|
binding.searchResultSubtitle.isVisible = false
|
||||||
binding.searchResultTimestamp.isVisible = false
|
binding.searchResultTimestamp.isVisible = false
|
||||||
binding.searchResultSubtitle.text = null
|
binding.searchResultSubtitle.text = null
|
||||||
val recipient =
|
val recipient =
|
||||||
Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false)
|
Recipient.from(binding.root.context, Address.fromSerialized(model.contact.sessionID), false)
|
||||||
binding.searchResultProfilePicture.root.update(recipient)
|
binding.searchResultProfilePicture.update(recipient)
|
||||||
val nameString = model.contact.getSearchName()
|
val nameString = model.contact.getSearchName()
|
||||||
binding.searchResultTitle.text = getHighlight(query, nameString)
|
binding.searchResultTitle.text = getHighlight(query, nameString)
|
||||||
}
|
}
|
||||||
@ -124,12 +124,12 @@ fun ContentView.bindModel(model: SavedMessages) {
|
|||||||
binding.searchResultSubtitle.isVisible = false
|
binding.searchResultSubtitle.isVisible = false
|
||||||
binding.searchResultTimestamp.isVisible = false
|
binding.searchResultTimestamp.isVisible = false
|
||||||
binding.searchResultTitle.setText(R.string.note_to_self)
|
binding.searchResultTitle.setText(R.string.note_to_self)
|
||||||
binding.searchResultProfilePicture.root.isVisible = false
|
binding.searchResultProfilePicture.isVisible = false
|
||||||
binding.searchResultSavedMessages.isVisible = true
|
binding.searchResultSavedMessages.isVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ContentView.bindModel(query: String?, model: Message) {
|
fun ContentView.bindModel(query: String?, model: Message) {
|
||||||
binding.searchResultProfilePicture.root.isVisible = true
|
binding.searchResultProfilePicture.isVisible = true
|
||||||
binding.searchResultSavedMessages.isVisible = false
|
binding.searchResultSavedMessages.isVisible = false
|
||||||
binding.searchResultTimestamp.isVisible = true
|
binding.searchResultTimestamp.isVisible = true
|
||||||
// val hasUnreads = model.unread > 0
|
// val hasUnreads = model.unread > 0
|
||||||
@ -138,7 +138,7 @@ fun ContentView.bindModel(query: String?, model: Message) {
|
|||||||
// binding.unreadCountTextView.text = model.unread.toString()
|
// binding.unreadCountTextView.text = model.unread.toString()
|
||||||
// }
|
// }
|
||||||
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
|
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
|
||||||
binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient)
|
binding.searchResultProfilePicture.update(model.messageResult.conversationRecipient)
|
||||||
val textSpannable = SpannableStringBuilder()
|
val textSpannable = SpannableStringBuilder()
|
||||||
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
|
||||||
// group chat, bind
|
// group chat, bind
|
||||||
|
@ -34,7 +34,6 @@ class MessageRequestView : LinearLayout {
|
|||||||
// region Updating
|
// region Updating
|
||||||
fun bind(thread: ThreadRecord, glide: GlideRequests) {
|
fun bind(thread: ThreadRecord, glide: GlideRequests) {
|
||||||
this.thread = thread
|
this.thread = thread
|
||||||
binding.profilePictureView.root.glide = glide
|
|
||||||
val senderDisplayName = getUserDisplayName(thread.recipient)
|
val senderDisplayName = getUserDisplayName(thread.recipient)
|
||||||
?: thread.recipient.address.toString()
|
?: thread.recipient.address.toString()
|
||||||
binding.displayNameTextView.text = senderDisplayName
|
binding.displayNameTextView.text = senderDisplayName
|
||||||
@ -44,12 +43,12 @@ class MessageRequestView : LinearLayout {
|
|||||||
binding.snippetTextView.text = snippet
|
binding.snippetTextView.text = snippet
|
||||||
|
|
||||||
post {
|
post {
|
||||||
binding.profilePictureView.root.update(thread.recipient)
|
binding.profilePictureView.update(thread.recipient)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recycle() {
|
fun recycle() {
|
||||||
binding.profilePictureView.root.recycle()
|
binding.profilePictureView.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUserDisplayName(recipient: Recipient): String? {
|
private fun getUserDisplayName(recipient: Recipient): String? {
|
||||||
|
@ -38,7 +38,7 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap
|
|||||||
|
|
||||||
override fun onViewRecycled(holder: ViewHolder) {
|
override fun onViewRecycled(holder: ViewHolder) {
|
||||||
super.onViewRecycled(holder)
|
super.onViewRecycled(holder)
|
||||||
holder.binding.profilePictureView.root.recycle()
|
holder.binding.profilePictureView.recycle()
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
|
||||||
@ -48,8 +48,7 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap
|
|||||||
|
|
||||||
fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) {
|
fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) {
|
||||||
binding.recipientName.text = selectable.item.name
|
binding.recipientName.text = selectable.item.name
|
||||||
with (binding.profilePictureView.root) {
|
with (binding.profilePictureView) {
|
||||||
glide = this@ViewHolder.glide
|
|
||||||
update(selectable.item)
|
update(selectable.item)
|
||||||
}
|
}
|
||||||
binding.root.setOnClickListener { toggle(selectable) }
|
binding.root.setOnClickListener { toggle(selectable) }
|
||||||
|
@ -88,10 +88,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
val displayName = getDisplayName()
|
val displayName = getDisplayName()
|
||||||
glide = GlideApp.with(this)
|
glide = GlideApp.with(this)
|
||||||
with(binding) {
|
with(binding) {
|
||||||
setupProfilePictureView(profilePictureView.root)
|
setupProfilePictureView(profilePictureView)
|
||||||
profilePictureView.root.setOnClickListener {
|
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
||||||
showEditProfilePictureUI()
|
|
||||||
}
|
|
||||||
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
||||||
btnGroupNameDisplay.text = displayName
|
btnGroupNameDisplay.text = displayName
|
||||||
publicKeyTextView.text = hexEncodedPublicKey
|
publicKeyTextView.text = hexEncodedPublicKey
|
||||||
@ -116,7 +114,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
|
TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
|
||||||
|
|
||||||
private fun setupProfilePictureView(view: ProfilePictureView) {
|
private fun setupProfilePictureView(view: ProfilePictureView) {
|
||||||
view.glide = glide
|
|
||||||
view.apply {
|
view.apply {
|
||||||
publicKey = hexEncodedPublicKey
|
publicKey = hexEncodedPublicKey
|
||||||
displayName = getDisplayName()
|
displayName = getDisplayName()
|
||||||
@ -255,8 +252,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
binding.btnGroupNameDisplay.text = displayName
|
binding.btnGroupNameDisplay.text = displayName
|
||||||
}
|
}
|
||||||
if (isUpdatingProfilePicture) {
|
if (isUpdatingProfilePicture) {
|
||||||
binding.profilePictureView.root.recycle() // Clear the cached image before updating
|
binding.profilePictureView.recycle() // Clear the cached image before updating
|
||||||
binding.profilePictureView.root.update()
|
binding.profilePictureView.update()
|
||||||
}
|
}
|
||||||
binding.loader.isVisible = false
|
binding.loader.isVisible = false
|
||||||
}
|
}
|
||||||
|
@ -144,7 +144,6 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
|
|||||||
super(itemView);
|
super(itemView);
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
avatar = itemView.findViewById(R.id.reactions_bottom_view_avatar);
|
avatar = itemView.findViewById(R.id.reactions_bottom_view_avatar);
|
||||||
avatar.glide = GlideApp.with(itemView);
|
|
||||||
recipient = itemView.findViewById(R.id.reactions_bottom_view_recipient_name);
|
recipient = itemView.findViewById(R.id.reactions_bottom_view_recipient_name);
|
||||||
remove = itemView.findViewById(R.id.reactions_bottom_view_recipient_remove);
|
remove = itemView.findViewById(R.id.reactions_bottom_view_recipient_remove);
|
||||||
}
|
}
|
||||||
|
63
app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt
Normal file
63
app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package org.thoughtcrime.securesms.ui
|
||||||
|
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material.Colors
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val colorDestructive = Color(0xffFF453A)
|
||||||
|
|
||||||
|
const val classicDark0 = 0xff111111
|
||||||
|
const val classicDark1 = 0xff1B1B1B
|
||||||
|
const val classicDark2 = 0xff2D2D2D
|
||||||
|
const val classicDark3 = 0xff414141
|
||||||
|
const val classicDark4 = 0xff767676
|
||||||
|
const val classicDark5 = 0xffA1A2A1
|
||||||
|
const val classicDark6 = 0xffFFFFFF
|
||||||
|
|
||||||
|
const val classicLight0 = 0xff000000
|
||||||
|
const val classicLight1 = 0xff6D6D6D
|
||||||
|
const val classicLight2 = 0xffA1A2A1
|
||||||
|
const val classicLight3 = 0xffDFDFDF
|
||||||
|
const val classicLight4 = 0xffF0F0F0
|
||||||
|
const val classicLight5 = 0xffF9F9F9
|
||||||
|
const val classicLight6 = 0xffFFFFFF
|
||||||
|
|
||||||
|
const val oceanDark0 = 0xff000000
|
||||||
|
const val oceanDark1 = 0xff1A1C28
|
||||||
|
const val oceanDark2 = 0xff252735
|
||||||
|
const val oceanDark3 = 0xff2B2D40
|
||||||
|
const val oceanDark4 = 0xff3D4A5D
|
||||||
|
const val oceanDark5 = 0xffA6A9CE
|
||||||
|
const val oceanDark6 = 0xff5CAACC
|
||||||
|
const val oceanDark7 = 0xffFFFFFF
|
||||||
|
|
||||||
|
const val oceanLight0 = 0xff000000
|
||||||
|
const val oceanLight1 = 0xff19345D
|
||||||
|
const val oceanLight2 = 0xff6A6E90
|
||||||
|
const val oceanLight3 = 0xff5CAACC
|
||||||
|
const val oceanLight4 = 0xffB3EDF2
|
||||||
|
const val oceanLight5 = 0xffE7F3F4
|
||||||
|
const val oceanLight6 = 0xffECFAFB
|
||||||
|
const val oceanLight7 = 0xffFCFFFF
|
||||||
|
|
||||||
|
val ocean_accent = Color(0xff57C9FA)
|
||||||
|
|
||||||
|
val oceanLights = arrayOf(oceanLight0, oceanLight1, oceanLight2, oceanLight3, oceanLight4, oceanLight5, oceanLight6, oceanLight7)
|
||||||
|
val oceanDarks = arrayOf(oceanDark0, oceanDark1, oceanDark2, oceanDark3, oceanDark4, oceanDark5, oceanDark6, oceanDark7)
|
||||||
|
val classicLights = arrayOf(classicLight0, classicLight1, classicLight2, classicLight3, classicLight4, classicLight5, classicLight6)
|
||||||
|
val classicDarks = arrayOf(classicDark0, classicDark1, classicDark2, classicDark3, classicDark4, classicDark5, classicDark6)
|
||||||
|
|
||||||
|
val oceanLightColors = oceanLights.map(::Color)
|
||||||
|
val oceanDarkColors = oceanDarks.map(::Color)
|
||||||
|
val classicLightColors = classicLights.map(::Color)
|
||||||
|
val classicDarkColors = classicDarks.map(::Color)
|
||||||
|
|
||||||
|
val blackAlpha40 = Color.Black.copy(alpha = 0.4f)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun transparentButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun destructiveButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = colorDestructive)
|
182
app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
Normal file
182
app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package org.thoughtcrime.securesms.ui
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ButtonColors
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.Colors
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import com.google.accompanist.pager.HorizontalPagerIndicator
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ItemButton(
|
||||||
|
text: String,
|
||||||
|
@DrawableRes icon: Int,
|
||||||
|
colors: ButtonColors = transparentButtonColors(),
|
||||||
|
contentDescription: String = text,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp),
|
||||||
|
colors = colors,
|
||||||
|
onClick = onClick,
|
||||||
|
shape = RectangleShape,
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.width(80.dp)
|
||||||
|
.fillMaxHeight()) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = icon),
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(text, modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Cell(content: @Composable () -> Unit) {
|
||||||
|
CellWithPaddingAndMargin(padding = 0.dp) { content() }
|
||||||
|
}
|
||||||
|
@Composable
|
||||||
|
fun CellNoMargin(content: @Composable () -> Unit) {
|
||||||
|
CellWithPaddingAndMargin(padding = 0.dp, margin = 0.dp) { content() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CellWithPaddingAndMargin(
|
||||||
|
padding: Dp = 24.dp,
|
||||||
|
margin: Dp = 32.dp,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
backgroundColor = MaterialTheme.colors.cellColor,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
elevation = 0.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentHeight()
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = margin),
|
||||||
|
) {
|
||||||
|
Box(Modifier.padding(padding)) { content() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Colors.cellColor: Color
|
||||||
|
@Composable
|
||||||
|
get() = LocalExtraColors.current.settingsBackground
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) {
|
||||||
|
if (pagerState.pageCount >= 2) Card(
|
||||||
|
shape = RoundedCornerShape(50.dp),
|
||||||
|
backgroundColor = Color.Black.copy(alpha = 0.4f),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.padding(8.dp)) {
|
||||||
|
HorizontalPagerIndicator(
|
||||||
|
pagerState = pagerState,
|
||||||
|
pageCount = pagerState.pageCount,
|
||||||
|
activeColor = Color.White,
|
||||||
|
inactiveColor = classicDarkColors[5])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun RowScope.CarouselPrevButton(pagerState: PagerState) {
|
||||||
|
CarouselButton(pagerState, pagerState.canScrollBackward, R.drawable.ic_prev, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun RowScope.CarouselNextButton(pagerState: PagerState) {
|
||||||
|
CarouselButton(pagerState, pagerState.canScrollForward, R.drawable.ic_next, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun RowScope.CarouselButton(
|
||||||
|
pagerState: PagerState,
|
||||||
|
enabled: Boolean,
|
||||||
|
@DrawableRes id: Int,
|
||||||
|
delta: Int
|
||||||
|
) {
|
||||||
|
if (pagerState.pageCount <= 1) Spacer(modifier = Modifier.width(32.dp))
|
||||||
|
else {
|
||||||
|
val animationScope = rememberCoroutineScope()
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(40.dp)
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
enabled = enabled,
|
||||||
|
onClick = { animationScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + delta) } }) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = id),
|
||||||
|
contentDescription = "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Divider() {
|
||||||
|
androidx.compose.material.Divider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RowScope.Avatar(recipient: Recipient) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(60.dp)
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = {
|
||||||
|
ProfilePictureView(it).apply { update(recipient) }
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.width(46.dp)
|
||||||
|
.height(46.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
34
app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt
Normal file
34
app/src/main/java/org/thoughtcrime/securesms/ui/Data.kt
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package org.thoughtcrime.securesms.ui
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibility class to allow ViewModels to use strings and string resources interchangeably.
|
||||||
|
*/
|
||||||
|
sealed class GetString {
|
||||||
|
@Composable
|
||||||
|
abstract fun string(): String
|
||||||
|
data class FromString(val string: String): GetString() {
|
||||||
|
@Composable
|
||||||
|
override fun string(): String = string
|
||||||
|
}
|
||||||
|
data class FromResId(@StringRes val resId: Int): GetString() {
|
||||||
|
@Composable
|
||||||
|
override fun string(): String = stringResource(resId)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun GetString(@StringRes resId: Int) = GetString.FromResId(resId)
|
||||||
|
fun GetString(string: String) = GetString.FromString(string)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents some text with an associated title.
|
||||||
|
*/
|
||||||
|
data class TitledText(val title: GetString, val text: String) {
|
||||||
|
constructor(title: String, text: String): this(GetString(title), text)
|
||||||
|
constructor(@StringRes title: Int, text: String): this(GetString(title), text)
|
||||||
|
}
|
76
app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt
Normal file
76
app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package org.thoughtcrime.securesms.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import network.loki.messenger.R
|
||||||
|
|
||||||
|
val LocalExtraColors = staticCompositionLocalOf<ExtraColors> { error("No Custom Attribute value provided") }
|
||||||
|
|
||||||
|
|
||||||
|
data class ExtraColors(
|
||||||
|
val settingsBackground: Color,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts current Theme to Compose Theme.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun AppTheme(
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val extraColors = LocalContext.current.run {
|
||||||
|
ExtraColors(
|
||||||
|
settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalExtraColors provides extraColors) {
|
||||||
|
AppCompatTheme {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getColorFromTheme(@AttrRes attr: Int, defaultValue: Int = 0x0): Color =
|
||||||
|
MaterialColors.getColor(this, attr, defaultValue).let(::Color)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the theme and a background for Compose Previews.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun PreviewTheme(
|
||||||
|
themeResId: Int,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalContext provides ContextThemeWrapper(LocalContext.current, themeResId)
|
||||||
|
) {
|
||||||
|
AppTheme {
|
||||||
|
Box(modifier = Modifier.background(color = MaterialTheme.colors.background)) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThemeResPreviewParameterProvider : PreviewParameterProvider<Int> {
|
||||||
|
override val values = sequenceOf(
|
||||||
|
R.style.Classic_Dark,
|
||||||
|
R.style.Classic_Light,
|
||||||
|
R.style.Ocean_Dark,
|
||||||
|
R.style.Ocean_Light,
|
||||||
|
)
|
||||||
|
}
|
5
app/src/main/res/drawable/ic_expand.xml
Normal file
5
app/src/main/res/drawable/ic_expand.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:autoMirrored="true" android:height="27dp"
|
||||||
|
android:viewportHeight="27" android:viewportWidth="26"
|
||||||
|
android:width="26dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#ffffff" android:pathData="M7.38,12.265C7.792,12.265 8.093,11.962 8.093,11.55V11.079L7.957,9.008L9.494,10.629L11.394,12.542C11.528,12.682 11.703,12.746 11.893,12.746C12.336,12.746 12.654,12.448 12.654,12.009C12.654,11.807 12.58,11.627 12.441,11.489L10.533,9.588L8.911,8.052L10.995,8.188H11.497C11.909,8.188 12.217,7.892 12.217,7.476C12.217,7.058 11.915,6.758 11.497,6.758H7.849C7.097,6.758 6.662,7.193 6.662,7.944V11.55C6.662,11.957 6.969,12.265 7.38,12.265ZM14.497,19.444H18.146C18.897,19.444 19.338,19.009 19.338,18.257V14.65C19.338,14.245 19.031,13.937 18.614,13.937C18.208,13.937 17.901,14.24 17.901,14.65V15.123L18.043,17.193L16.5,15.572L14.605,13.66C14.472,13.52 14.291,13.456 14.101,13.456C13.664,13.456 13.34,13.754 13.34,14.191C13.34,14.394 13.42,14.574 13.559,14.712L15.461,16.613L17.089,18.149L15.005,18.013H14.497C14.086,18.013 13.777,18.309 13.777,18.726C13.777,19.144 14.086,19.444 14.497,19.444Z"/>
|
||||||
|
</vector>
|
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="26dp"
|
||||||
|
android:height="21dp"
|
||||||
|
android:viewportWidth="26"
|
||||||
|
android:viewportHeight="21">
|
||||||
|
<path
|
||||||
|
android:pathData="M13.468,20.291C15.669,20.291 17.795,19.548 19.307,18.33C20.22,17.627 20.414,16.646 19.821,15.943C19.207,15.229 18.34,15.224 17.526,15.794C16.296,16.745 15.074,17.235 13.468,17.235C10.109,17.235 7.327,15 6.532,12.011H8.261C9.138,12.011 9.378,11.134 8.868,10.451L5.96,6.434C5.449,5.74 4.581,5.695 4.055,6.434L1.184,10.451C0.674,11.15 0.899,12.011 1.776,12.011H3.556C4.435,16.889 8.431,20.291 13.468,20.291ZM13.438,0.291C11.255,0.291 9.111,1.019 7.617,2.238C6.7,2.94 6.509,3.921 7.102,4.624C7.717,5.338 8.584,5.34 9.38,4.773C10.612,3.837 11.835,3.332 13.438,3.332C16.8,3.332 19.579,5.567 20.392,8.556H18.57C17.678,8.556 17.45,9.432 17.948,10.116L20.871,14.133C21.382,14.827 22.249,14.872 22.775,14.133L25.647,10.116C26.156,9.432 25.932,8.556 25.04,8.556H23.35C22.485,3.675 18.492,0.291 13.438,0.291Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/ic_message_details__reply.xml
Normal file
9
app/src/main/res/drawable/ic_message_details__reply.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="26dp"
|
||||||
|
android:height="26dp"
|
||||||
|
android:viewportWidth="26"
|
||||||
|
android:viewportHeight="26">
|
||||||
|
<path
|
||||||
|
android:pathData="M10.847,3.572V7.974C20.432,8.707 23.521,16.919 23.868,20.933C19.76,14.869 13.476,14.412 10.847,14.942V19.466L2.962,11.702L10.847,3.572Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/ic_message_details__trash.xml
Normal file
9
app/src/main/res/drawable/ic_message_details__trash.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="26dp"
|
||||||
|
android:height="26dp"
|
||||||
|
android:viewportWidth="26"
|
||||||
|
android:viewportHeight="26">
|
||||||
|
<path
|
||||||
|
android:pathData="M8.749,24.43H19.167C21.186,24.43 22.073,23.433 22.385,21.427L23.972,5.825L22.071,5.907L20.492,21.315C20.35,22.226 19.89,22.591 19.076,22.591H8.847C8.017,22.591 7.566,22.226 7.432,21.315L5.853,5.907L3.952,5.825L5.539,21.427C5.843,23.441 6.738,24.43 8.749,24.43ZM4.063,6.85H23.863C25.195,6.85 25.962,5.998 25.962,4.677V3.244C25.962,1.924 25.195,1.072 23.863,1.072H4.063C2.782,1.072 1.962,1.924 1.962,3.244V4.677C1.962,5.998 2.732,6.85 4.063,6.85ZM4.44,5.102C3.99,5.102 3.794,4.898 3.794,4.446V3.474C3.794,3.023 3.99,2.819 4.44,2.819H23.492C23.942,2.819 24.13,3.023 24.13,3.474V4.446C24.13,4.898 23.942,5.102 23.492,5.102H4.44Z"
|
||||||
|
android:fillColor="#FF3A3A"/>
|
||||||
|
</vector>
|
8
app/src/main/res/drawable/ic_next.xml
Normal file
8
app/src/main/res/drawable/ic_next.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<vector android:autoMirrored="true" android:height="17dp"
|
||||||
|
android:viewportHeight="17" android:viewportWidth="13"
|
||||||
|
android:width="13dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<group>
|
||||||
|
<clip-path android:pathData="M13,16.004l-13,-0l-0,-16l13,-0z"/>
|
||||||
|
<path android:fillColor="#ffffff" android:pathData="M0.646,1.736L10.112,7.933L0.444,14.268C0.323,14.343 0.222,14.438 0.144,14.547C0.067,14.657 0.015,14.779 -0.007,14.906C-0.029,15.033 -0.022,15.163 0.014,15.287C0.05,15.412 0.115,15.529 0.203,15.632C0.292,15.734 0.404,15.82 0.532,15.885C0.66,15.95 0.801,15.991 0.948,16.008C1.095,16.024 1.244,16.015 1.386,15.981C1.529,15.946 1.662,15.887 1.778,15.808L12.353,8.88C12.466,8.805 12.562,8.711 12.635,8.605C12.687,8.563 12.734,8.518 12.778,8.47C12.955,8.266 13.031,8.009 12.99,7.756C12.949,7.503 12.794,7.274 12.559,7.12L1.984,0.193C1.868,0.117 1.736,0.061 1.595,0.029C1.454,-0.003 1.307,-0.011 1.163,0.006C1.018,0.024 0.88,0.066 0.754,0.13C0.628,0.194 0.519,0.279 0.431,0.381C0.343,0.482 0.278,0.597 0.241,0.72C0.204,0.843 0.195,0.971 0.215,1.097C0.235,1.223 0.284,1.344 0.358,1.454C0.432,1.563 0.53,1.659 0.646,1.736Z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
8
app/src/main/res/drawable/ic_prev.xml
Normal file
8
app/src/main/res/drawable/ic_prev.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<vector android:autoMirrored="true" android:height="17dp"
|
||||||
|
android:viewportHeight="17" android:viewportWidth="12"
|
||||||
|
android:width="12dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<group>
|
||||||
|
<clip-path android:pathData="M0,0.004h12v16h-12z"/>
|
||||||
|
<path android:fillColor="#ffffff" android:pathData="M11.403,14.272L2.666,8.075L11.59,1.74C11.701,1.665 11.795,1.57 11.867,1.46C11.938,1.351 11.986,1.229 12.006,1.102C12.027,0.975 12.02,0.845 11.987,0.721C11.954,0.596 11.894,0.479 11.812,0.376C11.73,0.274 11.627,0.187 11.509,0.123C11.391,0.058 11.26,0.016 11.125,0C10.989,-0.016 10.852,-0.007 10.72,0.027C10.589,0.062 10.466,0.12 10.359,0.2L0.597,7.127C0.493,7.203 0.405,7.297 0.337,7.403C0.289,7.444 0.245,7.49 0.205,7.538C0.042,7.742 -0.029,7.999 0.009,8.252C0.047,8.505 0.19,8.734 0.407,8.887L10.168,15.815C10.275,15.891 10.398,15.947 10.528,15.979C10.658,16.011 10.793,16.019 10.927,16.001C11.06,15.984 11.188,15.942 11.304,15.878C11.42,15.814 11.521,15.728 11.602,15.627C11.684,15.526 11.743,15.411 11.777,15.288C11.811,15.165 11.82,15.037 11.801,14.911C11.783,14.785 11.738,14.664 11.67,14.554C11.601,14.445 11.511,14.349 11.403,14.272Z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
@ -8,7 +8,7 @@
|
|||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical">
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/medium_profile_picture_size"
|
android:layout_width="@dimen/medium_profile_picture_size"
|
||||||
android:layout_height="@dimen/medium_profile_picture_size" />
|
android:layout_height="@dimen/medium_profile_picture_size" />
|
||||||
|
@ -27,9 +27,8 @@
|
|||||||
android:layout_marginLeft="20dp"
|
android:layout_marginLeft="20dp"
|
||||||
android:layout_marginRight="20dp">
|
android:layout_marginRight="20dp">
|
||||||
|
|
||||||
<include
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profileButton"
|
android:id="@+id/profileButton"
|
||||||
layout="@layout/view_profile_picture"
|
|
||||||
android:layout_width="@dimen/small_profile_picture_size"
|
android:layout_width="@dimen/small_profile_picture_size"
|
||||||
android:layout_height="@dimen/small_profile_picture_size"
|
android:layout_height="@dimen/small_profile_picture_size"
|
||||||
android:layout_alignParentLeft="true"
|
android:layout_alignParentLeft="true"
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:gravity="center_horizontal">
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/large_profile_picture_size"
|
android:layout_width="@dimen/large_profile_picture_size"
|
||||||
android:layout_height="@dimen/large_profile_picture_size"
|
android:layout_height="@dimen/large_profile_picture_size"
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:background="?selectableItemBackground"
|
android:background="?selectableItemBackground"
|
||||||
android:id="@+id/backgroundContainer">
|
android:id="@+id/backgroundContainer">
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_height="@dimen/small_profile_picture_size"
|
android:layout_height="@dimen/small_profile_picture_size"
|
||||||
android:layout_width="@dimen/small_profile_picture_size"
|
android:layout_width="@dimen/small_profile_picture_size"
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:layout_margin="30dp"
|
android:layout_margin="30dp"
|
||||||
android:id="@+id/profile_picture_view"
|
android:id="@+id/profile_picture_view"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
app:behavior_hideable="true"
|
app:behavior_hideable="true"
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/large_profile_picture_size"
|
android:layout_width="@dimen/large_profile_picture_size"
|
||||||
android:layout_height="@dimen/large_profile_picture_size"
|
android:layout_height="@dimen/large_profile_picture_size"
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
app:behavior_hideable="true"
|
app:behavior_hideable="true"
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/large_profile_picture_size"
|
android:layout_width="@dimen/large_profile_picture_size"
|
||||||
android:layout_height="@dimen/large_profile_picture_size"
|
android:layout_height="@dimen/large_profile_picture_size"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="52dp">
|
android:layout_height="52dp">
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/reactions_bottom_view_avatar"
|
android:id="@+id/reactions_bottom_view_avatar"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
android:paddingHorizontal="@dimen/large_spacing"
|
android:paddingHorizontal="@dimen/large_spacing"
|
||||||
android:paddingVertical="@dimen/small_spacing">
|
android:paddingVertical="@dimen/small_spacing">
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/small_profile_picture_size"
|
android:layout_width="@dimen/small_profile_picture_size"
|
||||||
android:layout_height="@dimen/small_profile_picture_size" />
|
android:layout_height="@dimen/small_profile_picture_size" />
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?colorAccent" />
|
android:background="?colorAccent" />
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/medium_profile_picture_size"
|
android:layout_width="@dimen/medium_profile_picture_size"
|
||||||
android:layout_height="@dimen/medium_profile_picture_size"
|
android:layout_height="@dimen/medium_profile_picture_size"
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
android:id="@+id/search_result_profile_picture_parent"
|
android:id="@+id/search_result_profile_picture_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:id="@+id/search_result_profile_picture"
|
android:id="@+id/search_result_profile_picture"
|
||||||
android:layout_width="@dimen/medium_profile_picture_size"
|
android:layout_width="@dimen/medium_profile_picture_size"
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
android:layout_width="26dp"
|
android:layout_width="26dp"
|
||||||
android:layout_height="32dp">
|
android:layout_height="32dp">
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/very_small_profile_picture_size"
|
android:layout_width="@dimen/very_small_profile_picture_size"
|
||||||
android:layout_height="@dimen/very_small_profile_picture_size"
|
android:layout_height="@dimen/very_small_profile_picture_size"
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
android:layout_width="26dp"
|
android:layout_width="26dp"
|
||||||
android:layout_height="32dp">
|
android:layout_height="32dp">
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/very_small_profile_picture_size"
|
android:layout_width="@dimen/very_small_profile_picture_size"
|
||||||
android:layout_height="@dimen/very_small_profile_picture_size"
|
android:layout_height="@dimen/very_small_profile_picture_size"
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/medium_profile_picture_size"
|
android:layout_width="@dimen/medium_profile_picture_size"
|
||||||
android:layout_height="@dimen/medium_profile_picture_size"
|
android:layout_height="@dimen/medium_profile_picture_size"
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<org.thoughtcrime.securesms.components.ProfilePictureView
|
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:contentDescription="@string/AccessibilityId_profile_picture"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/doubleModeImageViewContainer"
|
android:id="@+id/doubleModeImageViewContainer"
|
||||||
@ -44,4 +40,4 @@
|
|||||||
android:layout_height="@dimen/large_profile_picture_size"
|
android:layout_height="@dimen/large_profile_picture_size"
|
||||||
android:background="@drawable/profile_picture_view_large_background" />
|
android:background="@drawable/profile_picture_view_large_background" />
|
||||||
|
|
||||||
</org.thoughtcrime.securesms.components.ProfilePictureView>
|
</merge>
|
@ -15,7 +15,7 @@
|
|||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:paddingHorizontal="@dimen/medium_spacing">
|
android:paddingHorizontal="@dimen/medium_spacing">
|
||||||
|
|
||||||
<include layout="@layout/view_profile_picture"
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
android:layout_width="@dimen/small_profile_picture_size"
|
android:layout_width="@dimen/small_profile_picture_size"
|
||||||
android:layout_height="@dimen/small_profile_picture_size"
|
android:layout_height="@dimen/small_profile_picture_size"
|
||||||
|
@ -67,9 +67,8 @@
|
|||||||
android:gravity="bottom"
|
android:gravity="bottom"
|
||||||
android:paddingBottom="@dimen/small_spacing">
|
android:paddingBottom="@dimen/small_spacing">
|
||||||
|
|
||||||
<include
|
<org.thoughtcrime.securesms.components.ProfilePictureView
|
||||||
android:id="@+id/profilePictureView"
|
android:id="@+id/profilePictureView"
|
||||||
layout="@layout/view_profile_picture"
|
|
||||||
android:layout_marginBottom="@dimen/small_spacing"
|
android:layout_marginBottom="@dimen/small_spacing"
|
||||||
android:layout_marginEnd="@dimen/small_spacing"
|
android:layout_marginEnd="@dimen/small_spacing"
|
||||||
android:layout_width="@dimen/very_small_profile_picture_size"
|
android:layout_width="@dimen/very_small_profile_picture_size"
|
||||||
|
@ -4,11 +4,15 @@
|
|||||||
<string name="yes">Yes</string>
|
<string name="yes">Yes</string>
|
||||||
<string name="no">No</string>
|
<string name="no">No</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
|
<string name="resend">Resend</string>
|
||||||
|
<string name="reply">Reply</string>
|
||||||
<string name="ban">Ban</string>
|
<string name="ban">Ban</string>
|
||||||
<string name="please_wait">Please wait…</string>
|
<string name="please_wait">Please wait…</string>
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
|
<string name="image">Image</string>
|
||||||
<string name="note_to_self">Note to Self</string>
|
<string name="note_to_self">Note to Self</string>
|
||||||
<string name="version_s">Version %s</string>
|
<string name="version_s">Version %s</string>
|
||||||
|
<string name="expand">Expand</string>
|
||||||
<!--Accessibility ID's-->
|
<!--Accessibility ID's-->
|
||||||
<!-- Landing Page -->
|
<!-- Landing Page -->
|
||||||
<string name="AccessibilityId_create_session_id">Create session ID</string>
|
<string name="AccessibilityId_create_session_id">Create session ID</string>
|
||||||
@ -516,6 +520,12 @@
|
|||||||
<string name="message_details_header__to">To:</string>
|
<string name="message_details_header__to">To:</string>
|
||||||
<string name="message_details_header__from">From:</string>
|
<string name="message_details_header__from">From:</string>
|
||||||
<string name="message_details_header__with">With:</string>
|
<string name="message_details_header__with">With:</string>
|
||||||
|
<string name="message_details_header__file_id">File Id:</string>
|
||||||
|
<string name="message_details_header__file_type">File Type:</string>
|
||||||
|
<string name="message_details_header__file_size">File Size:</string>
|
||||||
|
<string name="message_details_header__resolution">Resolution:</string>
|
||||||
|
<string name="message_details_header__duration">Duration:</string>
|
||||||
|
|
||||||
<!-- AndroidManifest.xml -->
|
<!-- AndroidManifest.xml -->
|
||||||
<string name="AndroidManifest__create_passphrase">Create passphrase</string>
|
<string name="AndroidManifest__create_passphrase">Create passphrase</string>
|
||||||
<string name="AndroidManifest__select_contacts">Select contacts</string>
|
<string name="AndroidManifest__select_contacts">Select contacts</string>
|
||||||
|
@ -342,6 +342,8 @@
|
|||||||
<item name="prominentButtonColor">?colorAccent</item>
|
<item name="prominentButtonColor">?colorAccent</item>
|
||||||
<item name="elementBorderColor">@color/classic_dark_3</item>
|
<item name="elementBorderColor">@color/classic_dark_3</item>
|
||||||
|
|
||||||
|
<item name="isLightTheme">false</item>
|
||||||
|
|
||||||
<!-- Home screen -->
|
<!-- Home screen -->
|
||||||
<item name="searchBackgroundColor">#1B1B1B</item>
|
<item name="searchBackgroundColor">#1B1B1B</item>
|
||||||
<item name="searchIconColor">#E5E5E8</item>
|
<item name="searchIconColor">#E5E5E8</item>
|
||||||
@ -424,6 +426,7 @@
|
|||||||
<item name="android:colorBackgroundFloating">?colorPrimary</item>
|
<item name="android:colorBackgroundFloating">?colorPrimary</item>
|
||||||
<item name="android:windowLightStatusBar">true</item>
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
<item name="android:windowLightNavigationBar" tools:targetApi="O_MR1">true</item>
|
<item name="android:windowLightNavigationBar" tools:targetApi="O_MR1">true</item>
|
||||||
|
<item name="isLightTheme">true</item>
|
||||||
<item name="android:isLightTheme" tools:targetApi="Q">true</item>
|
<item name="android:isLightTheme" tools:targetApi="Q">true</item>
|
||||||
<item name="android:statusBarColor">?colorPrimary</item>
|
<item name="android:statusBarColor">?colorPrimary</item>
|
||||||
|
|
||||||
@ -487,7 +490,6 @@
|
|||||||
<item name="android:textColor">?android:textColorPrimary</item>
|
<item name="android:textColor">?android:textColorPrimary</item>
|
||||||
<item name="android:textColorHint">@color/ocean_dark_5</item>
|
<item name="android:textColorHint">@color/ocean_dark_5</item>
|
||||||
<item name="android:windowBackground">?colorPrimary</item>
|
<item name="android:windowBackground">?colorPrimary</item>
|
||||||
<item name="android:colorBackground">@color/default_background_start</item>
|
|
||||||
<item name="android:navigationBarColor">@color/navigation_bar</item>
|
<item name="android:navigationBarColor">@color/navigation_bar</item>
|
||||||
<item name="default_background_end">?colorPrimary</item>
|
<item name="default_background_end">?colorPrimary</item>
|
||||||
<item name="default_background_start">?colorPrimaryDark</item>
|
<item name="default_background_start">?colorPrimaryDark</item>
|
||||||
@ -507,6 +509,8 @@
|
|||||||
<item name="prominentButtonColor">?colorAccent</item>
|
<item name="prominentButtonColor">?colorAccent</item>
|
||||||
<item name="elementBorderColor">@color/ocean_dark_4</item>
|
<item name="elementBorderColor">@color/ocean_dark_4</item>
|
||||||
|
|
||||||
|
<item name="isLightTheme">false</item>
|
||||||
|
|
||||||
<!-- Home screen -->
|
<!-- Home screen -->
|
||||||
<item name="searchBackgroundColor">@color/ocean_dark_3</item>
|
<item name="searchBackgroundColor">@color/ocean_dark_3</item>
|
||||||
<item name="searchIconColor">@color/ocean_dark_7</item>
|
<item name="searchIconColor">@color/ocean_dark_7</item>
|
||||||
@ -570,7 +574,6 @@
|
|||||||
<item name="android:textColorHint">@color/ocean_light_6</item>
|
<item name="android:textColorHint">@color/ocean_light_6</item>
|
||||||
<item name="android:navigationBarColor">@color/ocean_light_navigation_bar</item>
|
<item name="android:navigationBarColor">@color/ocean_light_navigation_bar</item>
|
||||||
<item name="android:windowBackground">?colorPrimary</item>
|
<item name="android:windowBackground">?colorPrimary</item>
|
||||||
<item name="android:colorBackground">@color/default_background_start</item>
|
|
||||||
<item name="default_background_end">@color/ocean_light_7</item>
|
<item name="default_background_end">@color/ocean_light_7</item>
|
||||||
<item name="default_background_start">@color/ocean_light_6</item>
|
<item name="default_background_start">@color/ocean_light_6</item>
|
||||||
<item name="colorCellBackground">@color/ocean_light_5</item>
|
<item name="colorCellBackground">@color/ocean_light_5</item>
|
||||||
@ -594,6 +597,7 @@
|
|||||||
<item name="android:colorBackgroundFloating">?colorPrimary</item>
|
<item name="android:colorBackgroundFloating">?colorPrimary</item>
|
||||||
<item name="android:windowLightStatusBar">true</item>
|
<item name="android:windowLightStatusBar">true</item>
|
||||||
<item name="android:windowLightNavigationBar" tools:targetApi="O_MR1">true</item>
|
<item name="android:windowLightNavigationBar" tools:targetApi="O_MR1">true</item>
|
||||||
|
<item name="isLightTheme">true</item>
|
||||||
<item name="android:isLightTheme" tools:targetApi="Q">true</item>
|
<item name="android:isLightTheme" tools:targetApi="Q">true</item>
|
||||||
<item name="android:statusBarColor">?colorPrimary</item>
|
<item name="android:statusBarColor">?colorPrimary</item>
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ fun <T> LiveData<T>.getOrAwaitValue(
|
|||||||
var data: T? = null
|
var data: T? = null
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
val observer = object : Observer<T> {
|
val observer = object : Observer<T> {
|
||||||
override fun onChanged(o: T?) {
|
override fun onChanged(o: T) {
|
||||||
data = o
|
data = o
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
this@getOrAwaitValue.removeObserver(this)
|
this@getOrAwaitValue.removeObserver(this)
|
||||||
|
@ -8,9 +8,14 @@ buildscript {
|
|||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||||
classpath "com.google.gms:google-services:$googleServicesVersion"
|
classpath "com.google.gms:google-services:$googleServicesVersion"
|
||||||
classpath files('libs/gradle-witness.jar')
|
classpath files('libs/gradle-witness.jar')
|
||||||
|
classpath "com.squareup:javapoet:1.13.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plugins{
|
||||||
|
id("com.google.dagger.hilt.android") version "2.44" apply false
|
||||||
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
|
@ -12,26 +12,28 @@
|
|||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
#Mon Jun 26 09:56:43 AEST 2023
|
#Mon Jun 26 09:56:43 AEST 2023
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|
||||||
|
gradlePluginVersion=7.3.1
|
||||||
|
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||||
|
org.gradle.unsafe.configuration-cache=true
|
||||||
|
|
||||||
|
googleServicesVersion=4.3.12
|
||||||
|
kotlinVersion=1.8.21
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
appcompatVersion=1.6.1
|
appcompatVersion=1.6.1
|
||||||
coreVersion=1.8.0
|
coreVersion=1.8.0
|
||||||
coroutinesVersion=1.6.4
|
coroutinesVersion=1.6.4
|
||||||
curve25519Version=0.6.0
|
curve25519Version=0.6.0
|
||||||
daggerVersion=2.40.1
|
daggerVersion=2.46.1
|
||||||
glideVersion=4.11.0
|
glideVersion=4.11.0
|
||||||
googleServicesVersion=4.3.12
|
|
||||||
gradlePluginVersion=7.3.1
|
|
||||||
jacksonDatabindVersion=2.9.8
|
jacksonDatabindVersion=2.9.8
|
||||||
junitVersion=4.13.2
|
junitVersion=4.13.2
|
||||||
kotlinVersion=1.6.21
|
|
||||||
kotlinxJsonVersion=1.3.3
|
kotlinxJsonVersion=1.3.3
|
||||||
kovenantVersion=3.3.0
|
kovenantVersion=3.3.0
|
||||||
lifecycleVersion=2.5.1
|
lifecycleVersion=2.5.1
|
||||||
materialVersion=1.8.0
|
materialVersion=1.8.0
|
||||||
mockitoKotlinVersion=4.1.0
|
mockitoKotlinVersion=4.1.0
|
||||||
okhttpVersion=3.12.1
|
okhttpVersion=3.12.1
|
||||||
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
|
||||||
org.gradle.unsafe.configuration-cache=true
|
|
||||||
pagingVersion=3.0.0
|
pagingVersion=3.0.0
|
||||||
preferenceVersion=1.2.0
|
preferenceVersion=1.2.0
|
||||||
protobufVersion=2.5.0
|
protobufVersion=2.5.0
|
||||||
|
@ -285,10 +285,9 @@ class BatchMessageReceiveJob(
|
|||||||
val openGroupID = data.getStringOrDefault(OPEN_GROUP_ID_KEY, null)
|
val openGroupID = data.getStringOrDefault(OPEN_GROUP_ID_KEY, null)
|
||||||
|
|
||||||
val parameters = (0 until numMessages).map { index ->
|
val parameters = (0 until numMessages).map { index ->
|
||||||
val data = contents[index]
|
|
||||||
val serverHash = serverHashes[index].let { if (it.isEmpty()) null else it }
|
val serverHash = serverHashes[index].let { if (it.isEmpty()) null else it }
|
||||||
val serverId = openGroupMessageServerIDs[index].let { if (it == -1L) null else it }
|
val serverId = openGroupMessageServerIDs[index].let { if (it == -1L) null else it }
|
||||||
MessageReceiveParameters(data, serverHash, serverId)
|
MessageReceiveParameters(contents[index], serverHash, serverId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return BatchMessageReceiveJob(parameters, openGroupID)
|
return BatchMessageReceiveJob(parameters, openGroupID)
|
||||||
|
@ -2,6 +2,7 @@ package org.session.libsession.messaging.mentions
|
|||||||
|
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
object MentionsManager {
|
object MentionsManager {
|
||||||
var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys
|
var userPublicKeyCache = mutableMapOf<Long, Set<String>>() // Thread ID to set of user hex encoded public keys
|
||||||
@ -32,9 +33,9 @@ object MentionsManager {
|
|||||||
candidates.sortedBy { it.displayName }
|
candidates.sortedBy { it.displayName }
|
||||||
if (query.length >= 2) {
|
if (query.length >= 2) {
|
||||||
// Filter out any non-matching candidates
|
// Filter out any non-matching candidates
|
||||||
candidates = candidates.filter { it.displayName.toLowerCase().contains(query.toLowerCase()) }
|
candidates = candidates.filter { it.displayName.lowercase(Locale.getDefault()).contains(query.lowercase(Locale.getDefault())) }
|
||||||
// Sort based on where in the candidate the query occurs
|
// Sort based on where in the candidate the query occurs
|
||||||
candidates.sortedBy { it.displayName.toLowerCase().indexOf(query.toLowerCase()) }
|
candidates.sortedBy { it.displayName.lowercase(Locale.getDefault()).indexOf(query.lowercase(Locale.getDefault())) }
|
||||||
}
|
}
|
||||||
// Return
|
// Return
|
||||||
return candidates
|
return candidates
|
||||||
|
@ -4,52 +4,51 @@ import android.content.Context
|
|||||||
import org.session.libsession.R
|
import org.session.libsession.R
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.messaging.calls.CallMessageType
|
import org.session.libsession.messaging.calls.CallMessageType
|
||||||
|
import org.session.libsession.messaging.calls.CallMessageType.CALL_FIRST_MISSED
|
||||||
|
import org.session.libsession.messaging.calls.CallMessageType.CALL_INCOMING
|
||||||
|
import org.session.libsession.messaging.calls.CallMessageType.CALL_MISSED
|
||||||
|
import org.session.libsession.messaging.calls.CallMessageType.CALL_OUTGOING
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
|
||||||
import org.session.libsession.utilities.ExpirationUtil
|
import org.session.libsession.utilities.ExpirationUtil
|
||||||
import org.session.libsession.utilities.truncateIdForDisplay
|
import org.session.libsession.utilities.truncateIdForDisplay
|
||||||
|
|
||||||
object UpdateMessageBuilder {
|
object UpdateMessageBuilder {
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
|
||||||
|
fun getSenderName(senderId: String) = storage.getContactWithSessionID(senderId)
|
||||||
|
?.displayName(Contact.ContactContext.REGULAR)
|
||||||
|
?: truncateIdForDisplay(senderId)
|
||||||
|
|
||||||
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, senderId: String? = null, isOutgoing: Boolean = false): String {
|
fun buildGroupUpdateMessage(context: Context, updateMessageData: UpdateMessageData, senderId: String? = null, isOutgoing: Boolean = false): String {
|
||||||
var message = ""
|
val updateData = updateMessageData.kind
|
||||||
val updateData = updateMessageData.kind ?: return message
|
if (updateData == null || !isOutgoing && senderId == null) return ""
|
||||||
if (!isOutgoing && senderId == null) return message
|
val senderName: String = if (isOutgoing) context.getString(R.string.MessageRecord_you)
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
else getSenderName(senderId!!)
|
||||||
val senderName: String = if (!isOutgoing) {
|
|
||||||
storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
|
|
||||||
} else { context.getString(R.string.MessageRecord_you) }
|
|
||||||
|
|
||||||
when (updateData) {
|
return when (updateData) {
|
||||||
is UpdateMessageData.Kind.GroupCreation -> {
|
is UpdateMessageData.Kind.GroupCreation -> if (isOutgoing) {
|
||||||
message = if (isOutgoing) {
|
|
||||||
context.getString(R.string.MessageRecord_you_created_a_new_group)
|
context.getString(R.string.MessageRecord_you_created_a_new_group)
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
|
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
|
||||||
}
|
}
|
||||||
}
|
is UpdateMessageData.Kind.GroupNameChange -> if (isOutgoing) {
|
||||||
is UpdateMessageData.Kind.GroupNameChange -> {
|
|
||||||
message = if (isOutgoing) {
|
|
||||||
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
|
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
|
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
is UpdateMessageData.Kind.GroupMemberAdded -> {
|
is UpdateMessageData.Kind.GroupMemberAdded -> {
|
||||||
val members = updateData.updatedMembers.joinToString(", ") {
|
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
|
||||||
storage.getContactWithSessionID(it)?.displayName(Contact.ContactContext.REGULAR) ?: it
|
if (isOutgoing) {
|
||||||
}
|
|
||||||
message = if (isOutgoing) {
|
|
||||||
context.getString(R.string.MessageRecord_you_added_s_to_the_group, members)
|
context.getString(R.string.MessageRecord_you_added_s_to_the_group, members)
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
|
context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UpdateMessageData.Kind.GroupMemberRemoved -> {
|
is UpdateMessageData.Kind.GroupMemberRemoved -> {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
|
||||||
val userPublicKey = storage.getUserPublicKey()!!
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
// 1st case: you are part of the removed members
|
// 1st case: you are part of the removed members
|
||||||
message = if (userPublicKey in updateData.updatedMembers) {
|
return if (userPublicKey in updateData.updatedMembers) {
|
||||||
if (isOutgoing) {
|
if (isOutgoing) {
|
||||||
context.getString(R.string.MessageRecord_left_group)
|
context.getString(R.string.MessageRecord_left_group)
|
||||||
} else {
|
} else {
|
||||||
@ -57,9 +56,7 @@ object UpdateMessageBuilder {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 2nd case: you are not part of the removed members
|
// 2nd case: you are not part of the removed members
|
||||||
val members = updateData.updatedMembers.joinToString(", ") {
|
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
|
||||||
storage.getContactWithSessionID(it)?.displayName(Contact.ContactContext.REGULAR) ?: it
|
|
||||||
}
|
|
||||||
if (isOutgoing) {
|
if (isOutgoing) {
|
||||||
context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members)
|
context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members)
|
||||||
} else {
|
} else {
|
||||||
@ -67,23 +64,19 @@ object UpdateMessageBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UpdateMessageData.Kind.GroupMemberLeft -> {
|
is UpdateMessageData.Kind.GroupMemberLeft -> if (isOutgoing) {
|
||||||
message = if (isOutgoing) {
|
|
||||||
context.getString(R.string.MessageRecord_left_group)
|
context.getString(R.string.MessageRecord_left_group)
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.ConversationItem_group_action_left, senderName)
|
context.getString(R.string.ConversationItem_group_action_left, senderName)
|
||||||
}
|
}
|
||||||
|
else -> return ""
|
||||||
}
|
}
|
||||||
is UpdateMessageData.Kind.OpenGroupInvitation -> { /*Handled externally*/ }
|
|
||||||
}
|
|
||||||
return message
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildExpirationTimerMessage(context: Context, duration: Long, senderId: String? = null, isOutgoing: Boolean = false): String {
|
fun buildExpirationTimerMessage(context: Context, duration: Long, senderId: String? = null, isOutgoing: Boolean = false): String {
|
||||||
if (!isOutgoing && senderId == null) return ""
|
if (!isOutgoing && senderId == null) return ""
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val senderName: String = if (!isOutgoing) {
|
||||||
val senderName: String? = if (!isOutgoing) {
|
getSenderName(senderId!!)
|
||||||
storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
|
|
||||||
} else { context.getString(R.string.MessageRecord_you) }
|
} else { context.getString(R.string.MessageRecord_you) }
|
||||||
return if (duration <= 0) {
|
return if (duration <= 0) {
|
||||||
if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)
|
if (isOutgoing) context.getString(R.string.MessageRecord_you_disabled_disappearing_messages)
|
||||||
@ -96,8 +89,7 @@ object UpdateMessageBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null): String {
|
fun buildDataExtractionMessage(context: Context, kind: DataExtractionNotificationInfoMessage.Kind, senderId: String? = null): String {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val senderName = getSenderName(senderId!!)
|
||||||
val senderName = storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
|
|
||||||
return when (kind) {
|
return when (kind) {
|
||||||
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT ->
|
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT ->
|
||||||
context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName)
|
context.getString(R.string.MessageRecord_s_took_a_screenshot, senderName)
|
||||||
@ -106,18 +98,12 @@ object UpdateMessageBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String {
|
fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String =
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
when (type) {
|
||||||
val senderName = storage.getContactWithSessionID(sender)?.displayName(Contact.ContactContext.REGULAR) ?: sender
|
CALL_INCOMING -> R.string.MessageRecord_s_called_you
|
||||||
return when (type) {
|
CALL_OUTGOING -> R.string.MessageRecord_called_s
|
||||||
CallMessageType.CALL_MISSED ->
|
CALL_MISSED, CALL_FIRST_MISSED -> R.string.MessageRecord_missed_call_from
|
||||||
context.getString(R.string.MessageRecord_missed_call_from, senderName)
|
}.let {
|
||||||
CallMessageType.CALL_INCOMING ->
|
context.getString(it, storage.getContactWithSessionID(sender)?.displayName(Contact.ContactContext.REGULAR) ?: sender)
|
||||||
context.getString(R.string.MessageRecord_s_called_you, senderName)
|
|
||||||
CallMessageType.CALL_OUTGOING ->
|
|
||||||
context.getString(R.string.MessageRecord_called_s, senderName)
|
|
||||||
CallMessageType.CALL_FIRST_MISSED ->
|
|
||||||
context.getString(R.string.MessageRecord_missed_call_from, senderName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user