[SES-557] Update MessageDetailActivity with Compose

This commit is contained in:
Andrew 2023-07-21 12:17:52 +09:30 committed by GitHub
commit d39cf2754c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1194 additions and 275 deletions

View File

@ -1,3 +1,4 @@
buildscript {
repositories {
google()
@ -13,6 +14,11 @@ buildscript {
}
}
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'witness'
@ -22,11 +28,16 @@ apply plugin: 'com.google.gms.google-services'
apply plugin: 'kotlinx-serialization'
apply plugin: 'dagger.hilt.android.plugin'
configurations.all {
exclude module: "commons-logging"
}
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.recyclerview:recyclerview:1.2.1'
implementation "com.google.android.material:material:$materialVersion"
@ -39,7 +50,6 @@ dependencies {
implementation 'androidx.exifinterface:exifinterface:1.3.4'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
@ -154,6 +164,16 @@ dependencies {
testImplementation 'org.robolectric:robolectric: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
@ -199,6 +219,13 @@ android {
}
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.4.7'
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
versionName canonicalVersionName
@ -305,3 +332,8 @@ def autoResConfig() {
.collect { matcher -> matcher.group(1) }
.sort()
}
// Allow references to generated code
kapt {
correctErrorTypes = true
}

View File

@ -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) {
Intent previewIntent = null;
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
@ -524,7 +528,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
@Override
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
if (data != null) {
@SuppressWarnings("ConstantConditions")
CursorPagerAdapter adapter = new CursorPagerAdapter(this, GlideApp.with(this), getWindow(), data.first, data.second, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);

View File

@ -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?,
)

View File

@ -249,18 +249,12 @@ class WebRtcCallActivity : PassphraseRequiredActionBarActivity() {
viewModel.callState.collect { state ->
Log.d("Loki", "Consuming view model state $state")
when (state) {
CALL_RINGING -> {
if (wantsToAnswer) {
answerCall()
wantsToAnswer = false
}
}
CALL_OUTGOING -> {
}
CALL_CONNECTED -> {
CALL_RINGING -> if (wantsToAnswer) {
answerCall()
wantsToAnswer = false
}
else -> { /* do nothing */ }
CALL_CONNECTED -> wantsToAnswer = false
else -> {}
}
updateControls(state)
}

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.RelativeLayout
@ -9,6 +10,7 @@ import androidx.annotation.DimenRes
import com.bumptech.glide.load.engine.DiskCacheStrategy
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewProfilePictureBinding
import network.loki.messenger.databinding.ViewUserBinding
import org.session.libsession.avatars.ContactColors
import org.session.libsession.avatars.PlaceholderAvatarPhoto
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.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
class ProfilePictureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : RelativeLayout(context, attrs) {
private val binding: ViewProfilePictureBinding by lazy { ViewProfilePictureBinding.bind(this) }
lateinit var glide: GlideRequests
private val binding = ViewProfilePictureBinding.inflate(LayoutInflater.from(context), this)
private val glide: GlideRequests = GlideApp.with(this)
var publicKey: String? = null
var displayName: String? = null
var additionalPublicKey: String? = null
@ -37,8 +40,13 @@ class ProfilePictureView @JvmOverloads constructor(
private val unknownOpenGroupDrawable by lazy { ResourceContactPhoto(R.drawable.ic_notification)
.asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context), false) }
// endregion
constructor(context: Context, sender: Recipient): this(context) {
update(sender)
}
// region Updating
fun update(recipient: Recipient) {
fun getUserDisplayName(publicKey: String): String {
@ -80,7 +88,6 @@ class ProfilePictureView @JvmOverloads constructor(
}
fun update() {
if (!this::glide.isInitialized) return
val publicKey = publicKey ?: return
val additionalPublicKey = additionalPublicKey
if (additionalPublicKey != null) {

View File

@ -55,8 +55,7 @@ class UserView : LinearLayout {
return contact?.displayName(Contact.ContactContext.REGULAR) ?: publicKey
}
val address = user.address.serialize()
binding.profilePictureView.root.glide = glide
binding.profilePictureView.root.update(user)
binding.profilePictureView.update(user)
binding.actionIndicatorImageView.setImageResource(R.drawable.ic_baseline_edit_24)
binding.nameTextView.text = if (user.isGroupRecipient) user.name else getUserDisplayName(address)
when (actionIndicator) {
@ -88,7 +87,7 @@ class UserView : LinearLayout {
}
fun unbind() {
binding.profilePictureView.root.recycle()
binding.profilePictureView.recycle()
}
// endregion
}

View File

@ -32,14 +32,13 @@ class ContactListAdapter(
class ContactViewHolder(private val binding: ViewContactBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(contact: ContactListItem.Contact, glide: GlideRequests, listener: (Recipient) -> Unit) {
binding.profilePictureView.root.glide = glide
binding.profilePictureView.root.update(contact.recipient)
binding.profilePictureView.update(contact.recipient)
binding.nameTextView.text = contact.displayName
binding.root.setOnClickListener { listener(contact.recipient) }
}
fun unbind() {
binding.profilePictureView.root.recycle()
binding.profilePictureView.recycle()
}
}

View File

@ -33,6 +33,8 @@ import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.DimenRes
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.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
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.LinkPreviewDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog
@ -582,10 +588,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
R.dimen.small_profile_picture_size
}
val size = resources.getDimension(sizeID).roundToInt()
binding.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size)
binding.toolbarContent.profilePictureView.root.glide = glide
binding.toolbarContent.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
val profilePictureView = binding.toolbarContent.profilePictureView.root
val profilePictureView = binding.toolbarContent.profilePictureView
viewModel.recipient?.let(profilePictureView::update)
}
@ -795,7 +800,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
updateSendAfterApprovalText()
showOrHideInputIfNeeded()
binding?.toolbarContent?.profilePictureView?.root?.update(threadRecipient)
binding?.toolbarContent?.profilePictureView?.update(threadRecipient)
binding?.toolbarContent?.conversationTitleView?.text = when {
threadRecipient.isLocalNumber -> getString(R.string.note_to_self)
else -> threadRecipient.toShortString()
@ -1921,10 +1926,24 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
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>) {
val intent = Intent(this, MessageDetailActivity::class.java)
intent.putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, messages.first().timestamp)
push(intent)
Intent(this, MessageDetailActivity::class.java)
.apply { putExtra(MESSAGE_TIMESTAMP, messages.first().timestamp) }
.let { handleMessageDetail.launch(it) }
endActionMode()
}
@ -1963,7 +1982,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
override fun reply(messages: Set<MessageRecord>) {
val recipient = viewModel.recipient ?: return
binding?.inputBar?.draftQuote(recipient, messages.first(), glide)
messages.firstOrNull()?.let { binding?.inputBar?.draftQuote(recipient, it, glide) }
endActionMode()
}

View File

@ -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)));
}
// 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
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)));

View File

@ -1,99 +1,401 @@
package org.thoughtcrime.securesms.conversation.v2
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import android.view.LayoutInflater
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 kotlinx.coroutines.launch
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityMessageDetailBinding
import org.session.libsession.messaging.MessagingModuleConfiguration
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 network.loki.messenger.databinding.ViewVisibleMessageContentBinding
import org.thoughtcrime.securesms.MediaPreviewActivity.getPreviewIntent
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.DateUtils
import java.text.SimpleDateFormat
import java.util.*
import org.thoughtcrime.securesms.ui.AppTheme
import org.thoughtcrime.securesms.ui.Avatar
import org.thoughtcrime.securesms.ui.CarouselNextButton
import org.thoughtcrime.securesms.ui.CarouselPrevButton
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
@AndroidEntryPoint
class MessageDetailActivity: PassphraseRequiredActionBarActivity() {
private lateinit var binding: ActivityMessageDetailBinding
var messageRecord: MessageRecord? = null
class MessageDetailActivity : PassphraseRequiredActionBarActivity() {
@Inject
lateinit var storage: Storage
// region Settings
private val viewModel: MessageDetailsViewModel by viewModels()
companion object {
// Extras
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) {
super.onCreate(savedInstanceState, ready)
binding = ActivityMessageDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
title = resources.getString(R.string.conversation_context__menu_message_details)
val timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
// We only show this screen for messages fail to send,
// so the author of the messages must be the current user.
val author = Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!)
messageRecord = DatabaseComponent.get(this).mmsSmsDatabase().getMessageFor(timestamp, author) ?: run {
finish()
return
}
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)
finish()
viewModel.timestamp = intent.getLongExtra(MESSAGE_TIMESTAMP, -1L)
ComposeView(this)
.apply { setContent { MessageDetailsScreen() } }
.let(::setContentView)
lifecycleScope.launch {
viewModel.eventFlow.collect {
when (it) {
Event.Finish -> finish()
is Event.StartMediaPreview -> startActivity(
getPreviewIntent(this@MessageDetailActivity, it.args)
)
}
}
}
}
fun updateContent() {
val dateLocale = Locale.getDefault()
val dateFormatter: SimpleDateFormat = DateUtils.getDetailedDateFormatter(this, dateLocale)
binding.sentTime.text = dateFormatter.format(Date(messageRecord!!.dateSent))
val errorMessage = DatabaseComponent.get(this).lokiMessageDatabase().getErrorMessage(messageRecord!!.getId())
if (errorMessage != null) {
binding.errorMessage.text = errorMessage
binding.resendContainer.isVisible = true
binding.errorContainer.isVisible = true
} else {
binding.errorContainer.isVisible = false
binding.resendContainer.isVisible = false
@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,
)
}
}
if (messageRecord!!.expiresIn <= 0 || messageRecord!!.expireStarted <= 0) {
binding.expiresContainer.visibility = View.GONE
} else {
binding.expiresContainer.visibility = View.VISIBLE
val elapsed = SnodeAPI.nowWithOffset - messageRecord!!.expireStarted
val remaining = messageRecord!!.expiresIn - elapsed
private fun setResultAndFinish(code: Int) {
Bundle().apply { putLong(MESSAGE_TIMESTAMP, viewModel.timestamp) }
.let(Intent()::putExtras)
.let { setResult(code, it) }
val duration = ExpirationUtil.getExpirationDisplayValue(this, Math.max((remaining / 1000).toInt(), 1))
binding.expiresIn.text = duration
finish()
}
}
@SuppressLint("ClickableViewAccessibility")
@Composable
fun MessageDetails(
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
)
setOnTouchListener { _, event ->
if (event.actionMasked == ACTION_UP) onContentClick(event)
true
}
}
}
)
}
Carousel(state.imageAttachments) { onClickImage(it) }
state.nonImageAttachmentFileDetails?.let { FileDetails(it) }
CellMetadata(state)
CellButtons(
onReply,
onResend,
onDelete,
)
}
}
@Composable
fun CellMetadata(
state: MessageDetailsState,
) {
state.apply {
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)
}
}
}
}
}
}
}
@Composable
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)
}

View File

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

View File

@ -28,11 +28,10 @@ class MentionCandidateView : LinearLayout {
private fun update() = with(binding) {
mentionCandidateNameTextView.text = mentionCandidate.displayName
profilePictureView.root.publicKey = mentionCandidate.publicKey
profilePictureView.root.displayName = mentionCandidate.displayName
profilePictureView.root.additionalPublicKey = null
profilePictureView.root.glide = glide!!
profilePictureView.root.update()
profilePictureView.publicKey = mentionCandidate.publicKey
profilePictureView.displayName = mentionCandidate.displayName
profilePictureView.additionalPublicKey = null
profilePictureView.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", mentionCandidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE

View File

@ -28,11 +28,10 @@ class MentionCandidateView : RelativeLayout {
private fun update() = with(binding) {
mentionCandidateNameTextView.text = candidate.displayName
profilePictureView.root.publicKey = candidate.publicKey
profilePictureView.root.displayName = candidate.displayName
profilePictureView.root.additionalPublicKey = null
profilePictureView.root.glide = glide!!
profilePictureView.root.update()
profilePictureView.publicKey = candidate.publicKey
profilePictureView.displayName = candidate.displayName
profilePictureView.additionalPublicKey = null
profilePictureView.update()
if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupManager.isUserModerator(context, "$openGroupRoom.$openGroupServer", candidate.publicKey)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE

View File

@ -67,7 +67,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
menu.findItem(R.id.menu_context_copy_public_key).isVisible =
(thread.isGroupRecipient && !thread.isOpenGroupRecipient && selectedItems.size == 1 && firstMessage.individualRecipient.address.toString() != userPublicKey)
// 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
menu.findItem(R.id.menu_context_resend).isVisible = (selectedItems.size == 1 && firstMessage.isFailed)
// Resync

View File

@ -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.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.SmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.GlowViewUtilities
import org.thoughtcrime.securesms.util.SearchUtil
@ -45,7 +46,6 @@ import kotlin.math.roundToInt
class VisibleMessageContentView : ConstraintLayout {
private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) }
var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
var onContentDoubleTap: (() -> Unit)? = null
var delegate: VisibleMessageViewDelegate? = null
var indexInAdapter: Int = -1
@ -59,13 +59,14 @@ class VisibleMessageContentView : ConstraintLayout {
// region Updating
fun bind(
message: MessageRecord,
isStartOfMessageCluster: Boolean,
isEndOfMessageCluster: Boolean,
glide: GlideRequests,
isStartOfMessageCluster: Boolean = true,
isEndOfMessageCluster: Boolean = true,
glide: GlideRequests = GlideApp.with(this),
thread: Recipient,
searchQuery: String?,
contactIsTrusted: Boolean,
onAttachmentNeedsDownload: (Long, Long) -> Unit
searchQuery: String? = null,
contactIsTrusted: Boolean = true,
onAttachmentNeedsDownload: (Long, Long) -> Unit,
suppressThumbnails: Boolean = false
) {
// Background
val color = if (message.isOutgoing) context.getAccentColor()
@ -184,7 +185,7 @@ class VisibleMessageContentView : ConstraintLayout {
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
*/
@ -237,6 +238,12 @@ class VisibleMessageContentView : ConstraintLayout {
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 =
listOf<View>(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Rect
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.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.disableClipping
@ -72,7 +72,6 @@ class VisibleMessageView : LinearLayout {
@Inject lateinit var mmsDb: MmsDatabase
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 swipeToReplyIconRect = Rect()
private var dx = 0.0f
@ -124,14 +123,14 @@ class VisibleMessageView : LinearLayout {
// region Updating
fun bind(
message: MessageRecord,
previous: MessageRecord?,
next: MessageRecord?,
glide: GlideRequests,
searchQuery: String?,
contact: Contact?,
previous: MessageRecord? = null,
next: MessageRecord? = null,
glide: GlideRequests = GlideApp.with(this),
searchQuery: String? = null,
contact: Contact? = null,
senderSessionID: String,
lastSeen: Long,
delegate: VisibleMessageViewDelegate?,
delegate: VisibleMessageViewDelegate? = null,
onAttachmentNeedsDownload: (Long, Long) -> Unit
) {
val threadID = message.threadId
@ -142,7 +141,7 @@ class VisibleMessageView : LinearLayout {
// Show profile picture and sender name if this is a group thread AND
// the message is incoming
binding.moderatorIconImageView.isVisible = false
binding.profilePictureView.root.visibility = when {
binding.profilePictureView.visibility = when {
thread.isGroupRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE
thread.isGroupRecipient -> View.INVISIBLE
else -> View.GONE
@ -151,22 +150,21 @@ class VisibleMessageView : LinearLayout {
val bottomMargin = if (isEndOfMessageCluster) resources.getDimensionPixelSize(R.dimen.small_spacing)
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
expirationParams.bottomMargin = bottomMargin
binding.messageInnerContainer.layoutParams = expirationParams
} else {
val avatarLayoutParams = binding.profilePictureView.root.layoutParams as MarginLayoutParams
val avatarLayoutParams = binding.profilePictureView.layoutParams as MarginLayoutParams
avatarLayoutParams.bottomMargin = bottomMargin
binding.profilePictureView.root.layoutParams = avatarLayoutParams
binding.profilePictureView.layoutParams = avatarLayoutParams
}
if (isGroupThread && !message.isOutgoing) {
if (isEndOfMessageCluster) {
binding.profilePictureView.root.publicKey = senderSessionID
binding.profilePictureView.root.glide = glide
binding.profilePictureView.root.update(message.individualRecipient)
binding.profilePictureView.root.setOnClickListener {
binding.profilePictureView.publicKey = senderSessionID
binding.profilePictureView.update(message.individualRecipient)
binding.profilePictureView.setOnClickListener {
if (thread.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID)
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 iconSize = toPx(24, context.resources)
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 bottom = top + iconSize
swipeToReplyIconRect.left = left
@ -418,7 +416,7 @@ class VisibleMessageView : LinearLayout {
}
fun recycle() {
binding.profilePictureView.root.recycle()
binding.profilePictureView.recycle()
binding.messageContentView.root.recycle()
}
@ -519,7 +517,7 @@ class VisibleMessageView : LinearLayout {
}
fun onContentClick(event: MotionEvent) {
binding.messageContentView.root.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
binding.messageContentView.root.onContentClick(event)
}
private fun onPress(event: MotionEvent) {

View File

@ -65,7 +65,6 @@ class ConversationView : LinearLayout {
} else {
ContextCompat.getDrawable(context, R.drawable.conversation_view_background)
}
binding.profilePictureView.root.glide = glide
val unreadCount = thread.unreadCount
if (thread.recipient.isBlocked) {
binding.accentView.setBackgroundResource(R.color.destructive)
@ -125,11 +124,11 @@ class ConversationView : LinearLayout {
thread.isRead -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
else -> binding.statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
}
binding.profilePictureView.root.update(thread.recipient)
binding.profilePictureView.update(thread.recipient)
}
fun recycle() {
binding.profilePictureView.root.recycle()
binding.profilePictureView.recycle()
}
private fun getUserDisplayName(recipient: Recipient): String? {

View File

@ -168,8 +168,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
// Set up Glide
glide = GlideApp.with(this)
// Set up toolbar buttons
binding.profileButton.root.glide = glide
binding.profileButton.root.setOnClickListener { openSettings() }
binding.profileButton.setOnClickListener { openSettings() }
binding.searchViewContainer.setOnClickListener {
binding.globalSearchInputLayout.requestFocus()
}
@ -364,8 +363,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(true)
if (textSecurePreferences.getLocalNumber() == null) { return; } // This can be the case after a secondary device is auto-cleared
IdentityKeyUtil.checkUpdate(this)
binding.profileButton.root.recycle() // clear cached image before update tje profilePictureView
binding.profileButton.root.update()
binding.profileButton.recycle() // clear cached image before update tje profilePictureView
binding.profileButton.update()
if (textSecurePreferences.getHasViewedSeed()) {
binding.seedReminderView.isVisible = false
}
@ -440,10 +439,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
private fun updateProfileButton() {
binding.profileButton.root.publicKey = publicKey
binding.profileButton.root.displayName = textSecurePreferences.getProfileName()
binding.profileButton.root.recycle()
binding.profileButton.root.update()
binding.profileButton.publicKey = publicKey
binding.profileButton.displayName = textSecurePreferences.getProfileName()
binding.profileButton.recycle()
binding.profileButton.update()
}
// endregion

View File

@ -53,10 +53,9 @@ class UserDetailsBottomSheet: BottomSheetDialogFragment() {
val recipient = Recipient.from(requireContext(), Address.fromSerialized(publicKey), false)
val threadRecipient = threadDb.getRecipientForThreadId(threadID) ?: return dismiss()
with(binding) {
profilePictureView.root.publicKey = publicKey
profilePictureView.root.glide = GlideApp.with(this@UserDetailsBottomSheet)
profilePictureView.root.isLarge = true
profilePictureView.root.update(recipient)
profilePictureView.publicKey = publicKey
profilePictureView.isLarge = true
profilePictureView.update(recipient)
nameTextViewContainer.visibility = View.VISIBLE
nameTextViewContainer.setOnClickListener {
if (recipient.isOpenGroupInboxRecipient || recipient.isOpenGroupOutboxRecipient) return@setOnClickListener

View File

@ -83,22 +83,20 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
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) {
val binding = ViewGlobalSearchResultBinding.bind(view).apply {
searchResultProfilePicture.root.glide = GlideApp.with(root)
}
val binding = ViewGlobalSearchResultBinding.bind(view)
fun bindPayload(newQuery: String, model: Model) {
bindQuery(newQuery, model)
}
fun bind(query: String, model: Model) {
binding.searchResultProfilePicture.root.recycle()
binding.searchResultProfilePicture.recycle()
when (model) {
is Model.GroupConversation -> bindModel(query, model)
is Model.Contact -> bindModel(query, model)

View File

@ -87,12 +87,12 @@ private fun getHighlight(query: String?, toSearch: String): Spannable? {
}
fun ContentView.bindModel(query: String?, model: GroupConversation) {
binding.searchResultProfilePicture.root.isVisible = true
binding.searchResultProfilePicture.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultSubtitle.isVisible = model.groupRecord.isClosedGroup
binding.searchResultTimestamp.isVisible = 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
binding.searchResultTitle.text = getHighlight(query, nameString)
@ -108,14 +108,14 @@ fun ContentView.bindModel(query: String?, model: GroupConversation) {
}
fun ContentView.bindModel(query: String?, model: ContactModel) {
binding.searchResultProfilePicture.root.isVisible = true
binding.searchResultProfilePicture.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultSubtitle.isVisible = false
binding.searchResultTimestamp.isVisible = false
binding.searchResultSubtitle.text = null
val recipient =
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()
binding.searchResultTitle.text = getHighlight(query, nameString)
}
@ -124,12 +124,12 @@ fun ContentView.bindModel(model: SavedMessages) {
binding.searchResultSubtitle.isVisible = false
binding.searchResultTimestamp.isVisible = false
binding.searchResultTitle.setText(R.string.note_to_self)
binding.searchResultProfilePicture.root.isVisible = false
binding.searchResultProfilePicture.isVisible = false
binding.searchResultSavedMessages.isVisible = true
}
fun ContentView.bindModel(query: String?, model: Message) {
binding.searchResultProfilePicture.root.isVisible = true
binding.searchResultProfilePicture.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultTimestamp.isVisible = true
// val hasUnreads = model.unread > 0
@ -138,7 +138,7 @@ fun ContentView.bindModel(query: String?, model: Message) {
// binding.unreadCountTextView.text = model.unread.toString()
// }
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()
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {
// group chat, bind

View File

@ -34,7 +34,6 @@ class MessageRequestView : LinearLayout {
// region Updating
fun bind(thread: ThreadRecord, glide: GlideRequests) {
this.thread = thread
binding.profilePictureView.root.glide = glide
val senderDisplayName = getUserDisplayName(thread.recipient)
?: thread.recipient.address.toString()
binding.displayNameTextView.text = senderDisplayName
@ -44,12 +43,12 @@ class MessageRequestView : LinearLayout {
binding.snippetTextView.text = snippet
post {
binding.profilePictureView.root.update(thread.recipient)
binding.profilePictureView.update(thread.recipient)
}
}
fun recycle() {
binding.profilePictureView.root.recycle()
binding.profilePictureView.recycle()
}
private fun getUserDisplayName(recipient: Recipient): String? {

View File

@ -38,7 +38,7 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
holder.binding.profilePictureView.root.recycle()
holder.binding.profilePictureView.recycle()
}
class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
@ -48,8 +48,7 @@ class BlockedContactsAdapter(val viewModel: BlockedContactsViewModel) : ListAdap
fun bind(selectable: SelectableRecipient, toggle: (SelectableRecipient) -> Unit) {
binding.recipientName.text = selectable.item.name
with (binding.profilePictureView.root) {
glide = this@ViewHolder.glide
with (binding.profilePictureView) {
update(selectable.item)
}
binding.root.setOnClickListener { toggle(selectable) }

View File

@ -88,10 +88,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val displayName = getDisplayName()
glide = GlideApp.with(this)
with(binding) {
setupProfilePictureView(profilePictureView.root)
profilePictureView.root.setOnClickListener {
showEditProfilePictureUI()
}
setupProfilePictureView(profilePictureView)
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
btnGroupNameDisplay.text = displayName
publicKeyTextView.text = hexEncodedPublicKey
@ -116,7 +114,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey)
private fun setupProfilePictureView(view: ProfilePictureView) {
view.glide = glide
view.apply {
publicKey = hexEncodedPublicKey
displayName = getDisplayName()
@ -255,8 +252,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
binding.btnGroupNameDisplay.text = displayName
}
if (isUpdatingProfilePicture) {
binding.profilePictureView.root.recycle() // Clear the cached image before updating
binding.profilePictureView.root.update()
binding.profilePictureView.recycle() // Clear the cached image before updating
binding.profilePictureView.update()
}
binding.loader.isVisible = false
}

View File

@ -144,7 +144,6 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
super(itemView);
this.callback = callback;
avatar = itemView.findViewById(R.id.reactions_bottom_view_avatar);
avatar.glide = GlideApp.with(itemView);
recipient = itemView.findViewById(R.id.reactions_bottom_view_recipient_name);
remove = itemView.findViewById(R.id.reactions_bottom_view_recipient_remove);
}

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

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

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

View 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,
)
}

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@ -8,7 +8,7 @@
android:orientation="horizontal"
android:gravity="center_vertical">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size" />

View File

@ -27,9 +27,8 @@
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp">
<include
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profileButton"
layout="@layout/view_profile_picture"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentLeft="true"

View File

@ -21,7 +21,7 @@
android:orientation="vertical"
android:gravity="center_horizontal">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"

View File

@ -9,7 +9,7 @@
android:gravity="center_vertical"
android:background="?selectableItemBackground"
android:id="@+id/backgroundContainer">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_width="@dimen/small_profile_picture_size"

View File

@ -45,7 +45,7 @@
</FrameLayout>
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:layout_margin="30dp"
android:id="@+id/profile_picture_view"
android:layout_gravity="center"

View File

@ -13,7 +13,7 @@
app:behavior_hideable="true"
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:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"

View File

@ -13,7 +13,7 @@
app:behavior_hideable="true"
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:layout_width="@dimen/large_profile_picture_size"
android:layout_height="@dimen/large_profile_picture_size"

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="52dp">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/reactions_bottom_view_avatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"

View File

@ -15,7 +15,7 @@
android:paddingHorizontal="@dimen/large_spacing"
android:paddingVertical="@dimen/small_spacing">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size" />

View File

@ -14,7 +14,7 @@
android:layout_height="match_parent"
android:background="?colorAccent" />
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size"

View File

@ -18,7 +18,7 @@
android:id="@+id/search_result_profile_picture_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:visibility="gone"
android:id="@+id/search_result_profile_picture"
android:layout_width="@dimen/medium_profile_picture_size"

View File

@ -13,7 +13,7 @@
android:layout_width="26dp"
android:layout_height="32dp">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/very_small_profile_picture_size"
android:layout_height="@dimen/very_small_profile_picture_size"

View File

@ -17,7 +17,7 @@
android:layout_width="26dp"
android:layout_height="32dp">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/very_small_profile_picture_size"
android:layout_height="@dimen/very_small_profile_picture_size"

View File

@ -7,7 +7,7 @@
android:gravity="center_vertical"
android:orientation="horizontal">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/medium_profile_picture_size"
android:layout_height="@dimen/medium_profile_picture_size"

View File

@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.ProfilePictureView
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">
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:id="@+id/doubleModeImageViewContainer"
@ -44,4 +40,4 @@
android:layout_height="@dimen/large_profile_picture_size"
android:background="@drawable/profile_picture_view_large_background" />
</org.thoughtcrime.securesms.components.ProfilePictureView>
</merge>

View File

@ -15,7 +15,7 @@
android:gravity="center_vertical"
android:paddingHorizontal="@dimen/medium_spacing">
<include layout="@layout/view_profile_picture"
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"

View File

@ -67,9 +67,8 @@
android:gravity="bottom"
android:paddingBottom="@dimen/small_spacing">
<include
<org.thoughtcrime.securesms.components.ProfilePictureView
android:id="@+id/profilePictureView"
layout="@layout/view_profile_picture"
android:layout_marginBottom="@dimen/small_spacing"
android:layout_marginEnd="@dimen/small_spacing"
android:layout_width="@dimen/very_small_profile_picture_size"

View File

@ -4,11 +4,15 @@
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="delete">Delete</string>
<string name="resend">Resend</string>
<string name="reply">Reply</string>
<string name="ban">Ban</string>
<string name="please_wait">Please wait…</string>
<string name="save">Save</string>
<string name="image">Image</string>
<string name="note_to_self">Note to Self</string>
<string name="version_s">Version %s</string>
<string name="expand">Expand</string>
<!--Accessibility ID's-->
<!-- Landing Page -->
<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__from">From:</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 -->
<string name="AndroidManifest__create_passphrase">Create passphrase</string>
<string name="AndroidManifest__select_contacts">Select contacts</string>

View File

@ -342,6 +342,8 @@
<item name="prominentButtonColor">?colorAccent</item>
<item name="elementBorderColor">@color/classic_dark_3</item>
<item name="isLightTheme">false</item>
<!-- Home screen -->
<item name="searchBackgroundColor">#1B1B1B</item>
<item name="searchIconColor">#E5E5E8</item>
@ -424,6 +426,7 @@
<item name="android:colorBackgroundFloating">?colorPrimary</item>
<item name="android:windowLightStatusBar">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:statusBarColor">?colorPrimary</item>
@ -487,7 +490,6 @@
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:textColorHint">@color/ocean_dark_5</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="default_background_end">?colorPrimary</item>
<item name="default_background_start">?colorPrimaryDark</item>
@ -507,6 +509,8 @@
<item name="prominentButtonColor">?colorAccent</item>
<item name="elementBorderColor">@color/ocean_dark_4</item>
<item name="isLightTheme">false</item>
<!-- Home screen -->
<item name="searchBackgroundColor">@color/ocean_dark_3</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:navigationBarColor">@color/ocean_light_navigation_bar</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_start">@color/ocean_light_6</item>
<item name="colorCellBackground">@color/ocean_light_5</item>
@ -594,6 +597,7 @@
<item name="android:colorBackgroundFloating">?colorPrimary</item>
<item name="android:windowLightStatusBar">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:statusBarColor">?colorPrimary</item>

View File

@ -22,7 +22,7 @@ fun <T> LiveData<T>.getOrAwaitValue(
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
override fun onChanged(o: T) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)

View File

@ -8,9 +8,14 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "com.google.gms:google-services:$googleServicesVersion"
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 {
repositories {
google()

View File

@ -12,26 +12,28 @@
# org.gradle.parallel=true
#Mon Jun 26 09:56:43 AEST 2023
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
appcompatVersion=1.6.1
coreVersion=1.8.0
coroutinesVersion=1.6.4
curve25519Version=0.6.0
daggerVersion=2.40.1
daggerVersion=2.46.1
glideVersion=4.11.0
googleServicesVersion=4.3.12
gradlePluginVersion=7.3.1
jacksonDatabindVersion=2.9.8
junitVersion=4.13.2
kotlinVersion=1.6.21
kotlinxJsonVersion=1.3.3
kovenantVersion=3.3.0
lifecycleVersion=2.5.1
materialVersion=1.8.0
mockitoKotlinVersion=4.1.0
okhttpVersion=3.12.1
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
org.gradle.unsafe.configuration-cache=true
pagingVersion=3.0.0
preferenceVersion=1.2.0
protobufVersion=2.5.0

View File

@ -285,10 +285,9 @@ class BatchMessageReceiveJob(
val openGroupID = data.getStringOrDefault(OPEN_GROUP_ID_KEY, null)
val parameters = (0 until numMessages).map { index ->
val data = contents[index]
val serverHash = serverHashes[index].let { if (it.isEmpty()) 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)

View File

@ -2,6 +2,7 @@ package org.session.libsession.messaging.mentions
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.contacts.Contact
import java.util.*
object MentionsManager {
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 }
if (query.length >= 2) {
// 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
candidates.sortedBy { it.displayName.toLowerCase().indexOf(query.toLowerCase()) }
candidates.sortedBy { it.displayName.lowercase(Locale.getDefault()).indexOf(query.lowercase(Locale.getDefault())) }
}
// Return
return candidates

View File

@ -4,52 +4,51 @@ import android.content.Context
import org.session.libsession.R
import org.session.libsession.messaging.MessagingModuleConfiguration
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.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.truncateIdForDisplay
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 {
var message = ""
val updateData = updateMessageData.kind ?: return message
if (!isOutgoing && senderId == null) return message
val storage = MessagingModuleConfiguration.shared.storage
val senderName: String = if (!isOutgoing) {
storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
} else { context.getString(R.string.MessageRecord_you) }
val updateData = updateMessageData.kind
if (updateData == null || !isOutgoing && senderId == null) return ""
val senderName: String = if (isOutgoing) context.getString(R.string.MessageRecord_you)
else getSenderName(senderId!!)
when (updateData) {
is UpdateMessageData.Kind.GroupCreation -> {
message = if (isOutgoing) {
context.getString(R.string.MessageRecord_you_created_a_new_group)
} else {
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
}
return when (updateData) {
is UpdateMessageData.Kind.GroupCreation -> if (isOutgoing) {
context.getString(R.string.MessageRecord_you_created_a_new_group)
} else {
context.getString(R.string.MessageRecord_s_added_you_to_the_group, senderName)
}
is UpdateMessageData.Kind.GroupNameChange -> {
message = if (isOutgoing) {
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
} else {
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
}
is UpdateMessageData.Kind.GroupNameChange -> if (isOutgoing) {
context.getString(R.string.MessageRecord_you_renamed_the_group_to_s, updateData.name)
} else {
context.getString(R.string.MessageRecord_s_renamed_the_group_to_s, senderName, updateData.name)
}
is UpdateMessageData.Kind.GroupMemberAdded -> {
val members = updateData.updatedMembers.joinToString(", ") {
storage.getContactWithSessionID(it)?.displayName(Contact.ContactContext.REGULAR) ?: it
}
message = if (isOutgoing) {
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
if (isOutgoing) {
context.getString(R.string.MessageRecord_you_added_s_to_the_group, members)
} else {
context.getString(R.string.MessageRecord_s_added_s_to_the_group, senderName, members)
}
}
is UpdateMessageData.Kind.GroupMemberRemoved -> {
val storage = MessagingModuleConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
// 1st case: you are part of the removed members
message = if (userPublicKey in updateData.updatedMembers) {
return if (userPublicKey in updateData.updatedMembers) {
if (isOutgoing) {
context.getString(R.string.MessageRecord_left_group)
} else {
@ -57,9 +56,7 @@ object UpdateMessageBuilder {
}
} else {
// 2nd case: you are not part of the removed members
val members = updateData.updatedMembers.joinToString(", ") {
storage.getContactWithSessionID(it)?.displayName(Contact.ContactContext.REGULAR) ?: it
}
val members = updateData.updatedMembers.joinToString(", ", transform = ::getSenderName)
if (isOutgoing) {
context.getString(R.string.MessageRecord_you_removed_s_from_the_group, members)
} else {
@ -67,23 +64,19 @@ object UpdateMessageBuilder {
}
}
}
is UpdateMessageData.Kind.GroupMemberLeft -> {
message = if (isOutgoing) {
context.getString(R.string.MessageRecord_left_group)
} else {
context.getString(R.string.ConversationItem_group_action_left, senderName)
}
is UpdateMessageData.Kind.GroupMemberLeft -> if (isOutgoing) {
context.getString(R.string.MessageRecord_left_group)
} else {
context.getString(R.string.ConversationItem_group_action_left, senderName)
}
is UpdateMessageData.Kind.OpenGroupInvitation -> { /*Handled externally*/ }
else -> return ""
}
return message
}
fun buildExpirationTimerMessage(context: Context, duration: Long, senderId: String? = null, isOutgoing: Boolean = false): String {
if (!isOutgoing && senderId == null) return ""
val storage = MessagingModuleConfiguration.shared.storage
val senderName: String? = if (!isOutgoing) {
storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
val senderName: String = if (!isOutgoing) {
getSenderName(senderId!!)
} else { context.getString(R.string.MessageRecord_you) }
return if (duration <= 0) {
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 {
val storage = MessagingModuleConfiguration.shared.storage
val senderName = storage.getContactWithSessionID(senderId!!)?.displayName(Contact.ContactContext.REGULAR) ?: truncateIdForDisplay(senderId)
val senderName = getSenderName(senderId!!)
return when (kind) {
DataExtractionNotificationInfoMessage.Kind.SCREENSHOT ->
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 {
val storage = MessagingModuleConfiguration.shared.storage
val senderName = storage.getContactWithSessionID(sender)?.displayName(Contact.ContactContext.REGULAR) ?: sender
return when (type) {
CallMessageType.CALL_MISSED ->
context.getString(R.string.MessageRecord_missed_call_from, senderName)
CallMessageType.CALL_INCOMING ->
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)
fun buildCallMessage(context: Context, type: CallMessageType, sender: String): String =
when (type) {
CALL_INCOMING -> R.string.MessageRecord_s_called_you
CALL_OUTGOING -> R.string.MessageRecord_called_s
CALL_MISSED, CALL_FIRST_MISSED -> R.string.MessageRecord_missed_call_from
}.let {
context.getString(it, storage.getContactWithSessionID(sender)?.displayName(Contact.ContactContext.REGULAR) ?: sender)
}
}
}