diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogContextMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogContextMenu.kt index 532f2fe0b5..d06cf72ad4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogContextMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogContextMenu.kt @@ -70,7 +70,7 @@ class CallLogContextMenu( iconRes = R.drawable.symbol_info_24, title = fragment.getString(R.string.CallContextMenu__info) ) { - val intent = ConversationSettingsActivity.forCall(fragment.requireContext(), call.peer) + val intent = ConversationSettingsActivity.forCall(fragment.requireContext(), call.peer, longArrayOf(call.call.messageId)) fragment.startActivity(intent) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsActivity.kt index 7dd09d4c82..e25fba49d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsActivity.kt @@ -67,7 +67,7 @@ class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSettings @JvmStatic fun forGroup(context: Context, groupId: GroupId): Intent { - val startBundle = ConversationSettingsFragmentArgs.Builder(null, ParcelableGroupId.from(groupId)) + val startBundle = ConversationSettingsFragmentArgs.Builder(null, ParcelableGroupId.from(groupId), null) .build() .toBundle() @@ -77,7 +77,7 @@ class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSettings @JvmStatic fun forRecipient(context: Context, recipientId: RecipientId): Intent { - val startBundle = ConversationSettingsFragmentArgs.Builder(recipientId, null) + val startBundle = ConversationSettingsFragmentArgs.Builder(recipientId, null, null) .build() .toBundle() @@ -86,17 +86,14 @@ class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSettings } @JvmStatic - fun forCall(context: Context, callPeer: Recipient): Intent { + fun forCall(context: Context, callPeer: Recipient, callMessageIds: LongArray): Intent { val startBundleBuilder = if (callPeer.isGroup) { - ConversationSettingsFragmentArgs.Builder(null, ParcelableGroupId.from(callPeer.requireGroupId())) + ConversationSettingsFragmentArgs.Builder(null, ParcelableGroupId.from(callPeer.requireGroupId()), callMessageIds) } else { - ConversationSettingsFragmentArgs.Builder(callPeer.id, null) + ConversationSettingsFragmentArgs.Builder(callPeer.id, null, callMessageIds) } - val startBundle = startBundleBuilder - .setIsCallInfo(true) - .build() - .toBundle() + val startBundle = startBundleBuilder.build().toBundle() return getIntent(context) .putExtra(ARG_START_BUNDLE, startBundle) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index 799ea40e15..054907c826 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -19,6 +19,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.doOnPreDraw import androidx.fragment.app.viewModels import androidx.navigation.Navigation +import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import app.cash.exhaustive.Exhaustive @@ -49,6 +50,7 @@ import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.conversation.preferences.AvatarPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.BioTextPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference +import org.thoughtcrime.securesms.components.settings.conversation.preferences.CallPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.GroupDescriptionPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.InternalPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.LargeIconClickPreference @@ -83,6 +85,7 @@ import org.thoughtcrime.securesms.stories.viewer.AddToGroupStoryDelegate import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.ContextUtil +import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.ExpirationUtil import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.Material3OnScrollHelper @@ -92,6 +95,7 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.views.SimpleProgressDialog import org.thoughtcrime.securesms.verify.VerifyIdentityActivity import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity +import java.util.Locale private const val REQUEST_CODE_VIEW_CONTACT = 1 private const val REQUEST_CODE_ADD_CONTACT = 2 @@ -103,6 +107,7 @@ class ConversationSettingsFragment : DSLSettingsFragment( menuId = R.menu.conversation_settings ) { + private val args: ConversationSettingsFragmentArgs by navArgs() private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) } private val blockIcon by lazy { ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24).apply { @@ -122,13 +127,12 @@ class ConversationSettingsFragment : DSLSettingsFragment( private val viewModel by viewModels( factoryProducer = { - val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments()) val groupId = args.groupId as? ParcelableGroupId ConversationSettingsViewModel.Factory( recipientId = args.recipientId, groupId = ParcelableGroupId.get(groupId), - isCallInfo = args.isCallInfo, + callMessageIds = args.callMessageIds ?: longArrayOf(), repository = ConversationSettingsRepository(requireContext()) ) } @@ -221,6 +225,7 @@ class ConversationSettingsFragment : DSLSettingsFragment( InternalPreference.register(adapter) GroupDescriptionPreference.register(adapter) LegacyGroupPreference.register(adapter) + CallPreference.register(adapter) val recipientId = args.recipientId if (recipientId != null) { @@ -437,6 +442,17 @@ class ConversationSettingsFragment : DSLSettingsFragment( dividerPref() + if (state.calls.isNotEmpty()) { + val firstCall = state.calls.first() + sectionHeaderPref(DSLSettingsText.from(DateUtils.formatDate(Locale.getDefault(), firstCall.record.timestamp))) + + for (call in state.calls) { + customPref(call) + } + + dividerPref() + } + val summary = DSLSettingsText.from(formatDisappearingMessagesLifespan(state.disappearingMessagesLifespan)) val icon = if (state.disappearingMessagesLifespan <= 0 || state.recipient.isBlocked) { R.drawable.ic_update_timer_disabled_16 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt index 15e77a9daf..f1e80b9367 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt @@ -5,6 +5,7 @@ import android.database.Cursor import androidx.annotation.WorkerThread import androidx.lifecycle.LiveData import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log @@ -15,6 +16,7 @@ import org.thoughtcrime.securesms.database.MediaTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.GroupRecord import org.thoughtcrime.securesms.database.model.IdentityRecord +import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.StoryViewState import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.groups.GroupId @@ -37,6 +39,16 @@ class ConversationSettingsRepository( private val groupManagementRepository: GroupManagementRepository = GroupManagementRepository(context) ) { + fun getCallEvents(callMessageIds: LongArray): Single> { + return if (callMessageIds.isEmpty()) { + Single.just(emptyList()) + } else { + Single.fromCallable { + SignalDatabase.messages.getMessages(callMessageIds.toList()).iterator().asSequence().toList() + } + } + } + @WorkerThread fun getThreadMedia(threadId: Long): Optional { return if (threadId <= 0) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsState.kt index a81049d690..8195a3f85e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsState.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.conversation import android.database.Cursor import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference +import org.thoughtcrime.securesms.components.settings.conversation.preferences.CallPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference import org.thoughtcrime.securesms.database.model.IdentityRecord import org.thoughtcrime.securesms.database.model.StoryViewState @@ -19,6 +20,7 @@ data class ConversationSettingsState( val sharedMedia: Cursor? = null, val sharedMediaIds: List = listOf(), val displayInternalRecipientDetails: Boolean = false, + val calls: List = emptyList(), private val sharedMediaLoaded: Boolean = false, private val specificSettingsState: SpecificSettingsState ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt index 9daa1f6cbc..25124e5886 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt @@ -16,6 +16,7 @@ import org.signal.core.util.CursorUtil import org.signal.core.util.ThreadUtil import org.signal.core.util.concurrent.SignalExecutors import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference +import org.thoughtcrime.securesms.components.settings.conversation.preferences.CallPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference import org.thoughtcrime.securesms.database.AttachmentTable import org.thoughtcrime.securesms.database.RecipientTable @@ -33,6 +34,7 @@ import org.thoughtcrime.securesms.util.livedata.Store import java.util.Optional sealed class ConversationSettingsViewModel( + private val callMessageIds: LongArray, private val repository: ConversationSettingsRepository, specificSettingsState: SpecificSettingsState ) : ViewModel() { @@ -64,6 +66,10 @@ sealed class ConversationSettingsViewModel( repository.getThreadMedia(tId) } + store.update(repository.getCallEvents(callMessageIds).toObservable()) { callRecords, state -> + state.copy(calls = callRecords.map { CallPreference.Model(it) }) + } + store.update(sharedMedia) { cursor, state -> if (!cleared) { if (cursor.isPresent) { @@ -128,9 +134,10 @@ sealed class ConversationSettingsViewModel( private class RecipientSettingsViewModel( private val recipientId: RecipientId, - private val isCallInfo: Boolean, + private val callMessageIds: LongArray, private val repository: ConversationSettingsRepository ) : ConversationSettingsViewModel( + callMessageIds, repository, SpecificSettingsState.RecipientSettingsState() ) { @@ -152,13 +159,13 @@ sealed class ConversationSettingsViewModel( state.copy( recipient = recipient, buttonStripState = ButtonStripPreference.State( - isMessageAvailable = isCallInfo, + isMessageAvailable = callMessageIds.isNotEmpty(), isVideoAvailable = recipient.registered == RecipientTable.RegisteredState.REGISTERED && !recipient.isSelf && !recipient.isBlocked && !recipient.isReleaseNotes, isAudioAvailable = isAudioAvailable, isAudioSecure = recipient.registered == RecipientTable.RegisteredState.REGISTERED, isMuted = recipient.isMuted, isMuteAvailable = !recipient.isSelf, - isSearchAvailable = !isCallInfo + isSearchAvailable = callMessageIds.isEmpty() ), disappearingMessagesLifespan = recipient.expiresInSeconds, canModifyBlockedState = !recipient.isSelf && RecipientUtil.isBlockable(recipient), @@ -258,9 +265,9 @@ sealed class ConversationSettingsViewModel( private class GroupSettingsViewModel( private val groupId: GroupId, - private val isCallInfo: Boolean, + private val callMessageIds: LongArray, private val repository: ConversationSettingsRepository - ) : ConversationSettingsViewModel(repository, SpecificSettingsState.GroupSettingsState(groupId)) { + ) : ConversationSettingsViewModel(callMessageIds, repository, SpecificSettingsState.GroupSettingsState(groupId)) { private val liveGroup = LiveGroup(groupId) @@ -274,13 +281,13 @@ sealed class ConversationSettingsViewModel( state.copy( recipient = recipient, buttonStripState = ButtonStripPreference.State( - isMessageAvailable = isCallInfo, + isMessageAvailable = callMessageIds.isNotEmpty(), isVideoAvailable = recipient.isPushV2Group && !recipient.isBlocked && isActive, isAudioAvailable = false, isAudioSecure = recipient.isPushV2Group, isMuted = recipient.isMuted, isMuteAvailable = true, - isSearchAvailable = !isCallInfo, + isSearchAvailable = callMessageIds.isEmpty(), isAddToStoryAvailable = recipient.isPushV2Group && !recipient.isBlocked && isActive && !SignalStore.storyValues().isFeatureDisabled ), canModifyBlockedState = RecipientUtil.isBlockable(recipient), @@ -483,7 +490,7 @@ sealed class ConversationSettingsViewModel( class Factory( private val recipientId: RecipientId? = null, private val groupId: GroupId? = null, - private val isCallInfo: Boolean, + private val callMessageIds: LongArray, private val repository: ConversationSettingsRepository ) : ViewModelProvider.Factory { @@ -491,8 +498,8 @@ sealed class ConversationSettingsViewModel( return requireNotNull( modelClass.cast( when { - recipientId != null -> RecipientSettingsViewModel(recipientId, isCallInfo, repository) - groupId != null -> GroupSettingsViewModel(groupId, isCallInfo, repository) + recipientId != null -> RecipientSettingsViewModel(recipientId, callMessageIds, repository) + groupId != null -> GroupSettingsViewModel(groupId, callMessageIds, repository) else -> error("One of RecipientId or GroupId required.") } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/CallPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/CallPreference.kt new file mode 100644 index 0000000000..06324e8e9b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/CallPreference.kt @@ -0,0 +1,71 @@ +package org.thoughtcrime.securesms.components.settings.conversation.preferences + +import androidx.annotation.DrawableRes +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.database.MessageTypes +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.databinding.ConversationSettingsCallPreferenceItemBinding +import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory +import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder +import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter +import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel +import java.util.Locale + +/** + * Renders a single call preference row when displaying call info. + */ +object CallPreference { + fun register(mappingAdapter: MappingAdapter) { + mappingAdapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, ConversationSettingsCallPreferenceItemBinding::inflate)) + } + + class Model( + val record: MessageRecord + ) : MappingModel { + override fun areItemsTheSame(newItem: Model): Boolean = record.id == newItem.record.id + + override fun areContentsTheSame(newItem: Model): Boolean { + return record.type == newItem.record.type && + record.isOutgoing == newItem.record.isOutgoing && + record.timestamp == newItem.record.timestamp && + record.id == newItem.record.id + } + } + + private class ViewHolder(binding: ConversationSettingsCallPreferenceItemBinding) : BindingViewHolder(binding) { + override fun bind(model: Model) { + binding.callIcon.setImageResource(getCallIcon(model.record)) + binding.callType.text = getCallType(model.record) + binding.callTime.text = getCallTime(model.record) + } + + @DrawableRes + private fun getCallIcon(messageRecord: MessageRecord): Int { + return when (messageRecord.type) { + MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_24 + MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_downleft_24 + MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_24 + else -> error("Unexpected type ${messageRecord.type}") + } + } + + private fun getCallType(messageRecord: MessageRecord): String { + val id = when (messageRecord.type) { + MessageTypes.MISSED_VIDEO_CALL_TYPE -> R.string.MessageRecord_missed_voice_call + MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.string.MessageRecord_missed_video_call + MessageTypes.INCOMING_AUDIO_CALL_TYPE -> R.string.MessageRecord_incoming_voice_call + MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.MessageRecord_incoming_video_call + MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.MessageRecord_outgoing_voice_call + MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.MessageRecord_outgoing_video_call + else -> error("Unexpected type ${messageRecord.type}") + } + + return context.getString(id) + } + + private fun getCallTime(messageRecord: MessageRecord): String { + return DateUtils.getOnlyTimeString(context, Locale.getDefault(), messageRecord.timestamp) + } + } +} diff --git a/app/src/main/res/drawable/symbol_arrow_downleft_24.xml b/app/src/main/res/drawable/symbol_arrow_downleft_24.xml new file mode 100644 index 0000000000..0c8d298c44 --- /dev/null +++ b/app/src/main/res/drawable/symbol_arrow_downleft_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/symbol_arrow_upright_24.xml b/app/src/main/res/drawable/symbol_arrow_upright_24.xml new file mode 100644 index 0000000000..5c318a042c --- /dev/null +++ b/app/src/main/res/drawable/symbol_arrow_upright_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/symbol_missed_incoming_24.xml b/app/src/main/res/drawable/symbol_missed_incoming_24.xml new file mode 100644 index 0000000000..bdc08f675e --- /dev/null +++ b/app/src/main/res/drawable/symbol_missed_incoming_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/conversation_settings_call_preference_item.xml b/app/src/main/res/layout/conversation_settings_call_preference_item.xml new file mode 100644 index 0000000000..2eb6ed9a3f --- /dev/null +++ b/app/src/main/res/layout/conversation_settings_call_preference_item.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/conversation_settings.xml b/app/src/main/res/navigation/conversation_settings.xml index 249b6925b6..403a6e039a 100644 --- a/app/src/main/res/navigation/conversation_settings.xml +++ b/app/src/main/res/navigation/conversation_settings.xml @@ -48,9 +48,9 @@ app:nullable="true" /> + android:name="call_message_ids" + app:argType="long[]" + app:nullable="true" />