Add in-call info sheet for call links.

This commit is contained in:
Alex Hart
2023-06-08 11:44:33 -03:00
committed by Cody Henthorne
parent 369ca189d3
commit 886c149c3f
20 changed files with 452 additions and 69 deletions

View File

@@ -470,6 +470,7 @@ dependencies {
implementation libs.androidx.gridlayout
implementation libs.androidx.exifinterface
implementation libs.androidx.compose.rxjava3
implementation libs.androidx.compose.runtime.livedata
implementation libs.androidx.constraintlayout
implementation libs.androidx.multidex
implementation libs.androidx.navigation.fragment.ktx

View File

@@ -55,6 +55,7 @@ import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.webrtc.CallLinkInfoSheet;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
@@ -71,6 +72,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
@@ -901,7 +903,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onCallInfoClicked() {
CallParticipantsListDialog.show(getSupportFragmentManager());
LiveRecipient liveRecipient = viewModel.getRecipient();
if (liveRecipient.get().isCallLink()) {
CallLinkInfoSheet.show(getSupportFragmentManager(), liveRecipient.get().requireCallLinkRoomId());
} else {
CallParticipantsListDialog.show(getSupportFragmentManager());
}
}
@Override

View File

@@ -1,9 +1,9 @@
package org.thoughtcrime.securesms.avatar.text
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.distinctUntilChanged
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.util.livedata.Store
@@ -12,7 +12,7 @@ class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() {
private val store = Store(TextAvatarCreationState(initialText))
val state: LiveData<TextAvatarCreationState> = Transformations.distinctUntilChanged(store.stateLiveData)
val state: LiveData<TextAvatarCreationState> = store.stateLiveData.distinctUntilChanged()
fun setColor(colorPair: Avatars.ColorPair) {
store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) }

View File

@@ -76,7 +76,7 @@ private fun SignalCallRowPreview() {
@Composable
fun SignalCallRow(
callLink: CallLinkTable.CallLink,
onJoinClicked: () -> Unit,
onJoinClicked: (() -> Unit)?,
modifier: Modifier = Modifier
) {
Row(
@@ -122,13 +122,15 @@ fun SignalCallRow(
)
}
Spacer(modifier = Modifier.width(10.dp))
if (onJoinClicked != null) {
Spacer(modifier = Modifier.width(10.dp))
Buttons.Small(
onClick = onJoinClicked,
modifier = Modifier.align(CenterVertically)
) {
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
Buttons.Small(
onClick = onJoinClicked,
modifier = Modifier.align(CenterVertically)
) {
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
}
}
}
}

View File

@@ -34,6 +34,7 @@ import androidx.core.app.ShareCompat
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dividers
import org.signal.core.ui.Rows
@@ -86,7 +87,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
) {
val callLink: CallLinkTable.CallLink by viewModel.callLink
Handle(modifier = Modifier.align(Alignment.CenterHorizontally))
BottomSheets.Handle(modifier = Modifier.align(Alignment.CenterHorizontally))
Spacer(modifier = Modifier.height(20.dp))

View File

@@ -3,9 +3,10 @@ package org.thoughtcrime.securesms.components.settings.conversation
import android.database.Cursor
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
@@ -62,7 +63,7 @@ sealed class ConversationSettingsViewModel(
protected val disposable = CompositeDisposable()
init {
val threadId: LiveData<Long> = Transformations.distinctUntilChanged(Transformations.map(state) { it.threadId })
val threadId: LiveData<Long> = state.map { it.threadId }.distinctUntilChanged()
val updater: LiveData<Long> = LiveDataUtil.combineLatest(threadId, sharedMediaUpdateTrigger) { tId, _ -> tId }
val sharedMedia: LiveData<Optional<Cursor>> = LiveDataUtil.mapAsync(SignalExecutors.BOUNDED, updater) { tId ->

View File

@@ -0,0 +1,332 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ShareCompat
import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Dividers
import org.signal.core.ui.Rows
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.concurrent.addTo
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallLinkState.Restrictions
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragmentArgs
import org.thoughtcrime.securesms.calls.links.SignalCallRow
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsViewModel
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.events.WebRtcViewModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
import org.thoughtcrime.securesms.util.BottomSheetUtil
/**
* Displays information about the in-progress CallLink call from
* within WebRtcActivity. If the user is able to modify call link
* state, provides options to do so.
*/
class CallLinkInfoSheet : ComposeBottomSheetDialogFragment() {
companion object {
private val TAG = Log.tag(CallLinkInfoSheet::class.java)
private const val CALL_LINK_ROOM_ID = "call_link_room_id"
@JvmStatic
fun show(fragmentManager: FragmentManager, callLinkRoomId: CallLinkRoomId) {
CallLinkInfoSheet().apply {
arguments = bundleOf(CALL_LINK_ROOM_ID to callLinkRoomId)
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
private val webRtcCallViewModel: WebRtcCallViewModel by activityViewModels()
private val callLinkDetailsViewModel: CallLinkDetailsViewModel by viewModels(factoryProducer = {
CallLinkDetailsViewModel.Factory(BundleCompat.getParcelable(requireArguments(), CALL_LINK_ROOM_ID, CallLinkRoomId::class.java)!!)
})
private val lifecycleDisposable = LifecycleDisposable()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleDisposable.bindTo(viewLifecycleOwner)
parentFragmentManager.setFragmentResultListener(EditCallLinkNameDialogFragment.RESULT_KEY, viewLifecycleOwner) { resultKey, bundle ->
if (bundle.containsKey(resultKey)) {
setName(bundle.getString(resultKey)!!)
}
}
}
@Composable
override fun SheetContent() {
val callLinkDetailsState by callLinkDetailsViewModel.state
val callParticipantsState by webRtcCallViewModel.callParticipantsState.observeAsState()
val participants = if (callParticipantsState?.callState == WebRtcViewModel.State.CALL_CONNECTED) {
listOf(Recipient.self()) + (callParticipantsState?.allRemoteParticipants?.map { it.recipient } ?: emptyList())
} else {
emptyList()
}.toImmutableList()
val onEditNameClicked: () -> Unit = remember(callLinkDetailsState) {
{
EditCallLinkNameDialogFragment().apply {
arguments = EditCallLinkNameDialogFragmentArgs.Builder(callLinkDetailsState.callLink?.state?.name ?: "").build().toBundle()
}.show(parentFragmentManager, null)
}
}
val callLink = callLinkDetailsState.callLink
if (callLink != null) {
Sheet(
callLink = callLink,
participants = participants,
onShareLinkClicked = this::shareLink,
onEditNameClicked = onEditNameClicked,
onToggleAdminApprovalClicked = this::onApproveAllMembersChanged,
onBlock = {} // TODO [alex] -- Blocking
)
}
}
private fun onApproveAllMembersChanged(checked: Boolean) {
callLinkDetailsViewModel.setApproveAllMembers(checked)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onSuccess = {
if (it !is UpdateCallLinkResult.Success) {
Log.w(TAG, "Failed to change restrictions. $it")
toastFailure()
}
}, onError = handleError("onApproveAllMembersChanged"))
.addTo(lifecycleDisposable)
}
private fun shareLink() {
val mimeType = Intent.normalizeMimeType("text/plain")
val shareIntent = ShareCompat.IntentBuilder(requireContext())
.setText(CallLinks.url(callLinkDetailsViewModel.rootKeySnapshot))
.setType(mimeType)
.createChooserIntent()
try {
startActivity(shareIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__failed_to_open_share_sheet, Toast.LENGTH_LONG).show()
}
}
private fun setName(name: String) {
callLinkDetailsViewModel.setName(name)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onSuccess = {
if (it !is UpdateCallLinkResult.Success) {
Log.w(TAG, "Failed to set name. $it")
toastFailure()
}
},
onError = handleError("setName")
)
.addTo(lifecycleDisposable)
}
private fun handleError(method: String): (throwable: Throwable) -> Unit {
return {
Log.w(TAG, "Failure during $method", it)
toastFailure()
}
}
private fun toastFailure() {
Toast.makeText(requireContext(), R.string.CallLinkDetailsFragment__couldnt_save_changes, Toast.LENGTH_LONG).show()
}
}
@Preview
@Composable
private fun SheetPreview() {
SignalTheme(isDarkMode = true) {
Surface {
Sheet(
callLink = CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = CallLinkRoomId.fromBytes(byteArrayOf(1, 2, 3, 4, 5)),
credentials = CallLinkCredentials(
linkKeyBytes = byteArrayOf(1, 2, 3, 4, 5),
adminPassBytes = byteArrayOf(1, 2, 3, 4, 5)
),
state = SignalCallLinkState(),
avatarColor = AvatarColor.random()
),
participants = listOf(Recipient.UNKNOWN).toImmutableList(),
onShareLinkClicked = {},
onEditNameClicked = {},
onToggleAdminApprovalClicked = {},
onBlock = {}
)
}
}
}
@Composable
private fun Sheet(
callLink: CallLinkTable.CallLink,
participants: ImmutableList<Recipient>,
onShareLinkClicked: () -> Unit,
onEditNameClicked: () -> Unit,
onToggleAdminApprovalClicked: (Boolean) -> Unit,
onBlock: (Recipient) -> Unit
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
BottomSheets.Handle()
SignalCallRow(callLink = callLink, onJoinClicked = null)
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__share_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_link_24),
iconModifier = Modifier
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = CircleShape
)
.size(42.dp)
.padding(9.dp),
modifier = Modifier
.defaultMinSize(minHeight = 64.dp)
.clickable(onClick = onShareLinkClicked)
)
}
items(participants, { it.id }, { null }) {
CallLinkMemberRow(
recipient = it,
isSelfAdmin = callLink.credentials?.adminPassBytes != null,
onBlockClicked = onBlock
)
}
if (callLink.credentials?.adminPassBytes != null) {
item {
Dividers.Default()
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__add_call_name),
modifier = Modifier.clickable(onClick = onEditNameClicked)
)
Rows.ToggleRow(
checked = callLink.state.restrictions == Restrictions.ADMIN_APPROVAL,
text = stringResource(id = R.string.CallLinkDetailsFragment__approve_all_members),
onCheckChanged = onToggleAdminApprovalClicked
)
}
}
}
}
@Preview
@Composable
private fun CallLinkMemberRowPreview() {
SignalTheme(isDarkMode = true) {
Surface {
CallLinkMemberRow(
Recipient.UNKNOWN,
isSelfAdmin = true,
{}
)
}
}
}
@Composable
private fun CallLinkMemberRow(
recipient: Recipient,
isSelfAdmin: Boolean,
onBlockClicked: (Recipient) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(Rows.defaultPadding())
) {
AndroidView(
factory = ::AvatarImageView,
modifier = Modifier.size(40.dp)
) {
it.setAvatarUsingProfile(recipient)
}
Spacer(modifier = Modifier.width(24.dp))
Text(
text = recipient.getShortDisplayName(LocalContext.current),
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)
)
if (isSelfAdmin) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_minus_circle_24),
contentDescription = null,
modifier = Modifier
.clickable(onClick = { onBlockClicked(recipient) })
.align(Alignment.CenterVertically)
)
}
}
}

View File

@@ -30,6 +30,7 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.signal.core.ui.BottomSheets
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.BottomSheetUtil
@@ -49,7 +50,7 @@ class WebRtcAudioOutputBottomSheet : ComposeBottomSheetDialogFragment(), DialogI
.padding(16.dp)
.wrapContentSize()
) {
Handle()
BottomSheets.Handle()
DeviceList(audioOutputOptions = viewModel.audioRoutes.toImmutableList(), initialDeviceId = viewModel.defaultDeviceId, modifier = Modifier.fillMaxWidth(), onDeviceSelected = viewModel.onClick)
}
}

View File

@@ -4,16 +4,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.ViewCompositionStrategy
@@ -41,29 +34,4 @@ abstract class ComposeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetD
@Composable
abstract fun SheetContent()
/**
* BottomSheet Handle, according to our design specs.
* This can be placed in a column with the other page content like so:
*
* ```
* Column(modifier = Modifier
* .fillMaxWidth()
* .wrapContentSize(Alignment.Center)
* ) {
* Handle()
* Text("Hello!")
* }
* ```
*/
@Composable
protected fun Handle(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(width = 48.dp, height = 22.dp)
.padding(vertical = 10.dp)
.clip(RoundedCornerShape(1000.dp))
.background(MaterialTheme.colorScheme.outline)
)
}
}

View File

@@ -2,9 +2,10 @@ package org.thoughtcrime.securesms.contacts.paged
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
@@ -48,8 +49,8 @@ class ContactSearchViewModel(
private val selectionStore = Store<Set<ContactSearchKey>>(emptySet())
private val errorEvents = PublishSubject.create<ContactSearchError>()
val controller: LiveData<PagingController<ContactSearchKey>> = Transformations.map(pagedData) { it.controller }
val data: LiveData<List<ContactSearchData>> = Transformations.switchMap(pagedData) { it.data }
val controller: LiveData<PagingController<ContactSearchKey>> = pagedData.map { it.controller }
val data: LiveData<List<ContactSearchData>> = pagedData.switchMap { it.data }
val configurationState: LiveData<ContactSearchState> = configurationStore.stateLiveData
val selectionState: LiveData<Set<ContactSearchKey>> = selectionStore.stateLiveData
val errorEventsStream: Observable<ContactSearchError> = errorEvents

View File

@@ -1,7 +1,8 @@
package org.thoughtcrime.securesms.conversation.colors
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.map
import androidx.lifecycle.switchMap
import com.annimon.stream.Stream
import org.signal.core.util.MapUtil
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette.Names.all
@@ -23,13 +24,13 @@ object NameColors {
recipientId: LiveData<RecipientId>,
sessionMemberCache: MutableMap<GroupId, Set<Recipient>>
): LiveData<Map<RecipientId, NameColor>> {
val recipient = Transformations.switchMap(recipientId) { r: RecipientId? -> Recipient.live(r!!).liveData }
val group = Transformations.map(recipient) { obj: Recipient -> obj.groupId }
val groupMembers = Transformations.switchMap(group) { g: Optional<GroupId> ->
val recipient = recipientId.switchMap { r: RecipientId? -> Recipient.live(r!!).liveData }
val group = recipient.map { obj: Recipient -> obj.groupId }
val groupMembers = group.switchMap { g: Optional<GroupId> ->
g.map { groupId: GroupId -> this.getSessionGroupRecipients(groupId, sessionMemberCache) }
.orElseGet { DefaultValueLiveData(emptySet()) }
}
return Transformations.map(groupMembers) { members: Set<Recipient>? ->
return groupMembers.map { members: Set<Recipient>? ->
val sorted = Stream.of(members)
.filter { member: Recipient? -> member != Recipient.self() }
.sortBy { obj: Recipient -> obj.requireStringId() }
@@ -44,14 +45,13 @@ object NameColors {
}
private fun getSessionGroupRecipients(groupId: GroupId, sessionMemberCache: MutableMap<GroupId, Set<Recipient>>): LiveData<Set<Recipient>> {
val fullMembers = Transformations.map(
LiveGroup(groupId).fullMembers
) { members: List<FullMember>? ->
val fullMembers = LiveGroup(groupId).fullMembers.map { members: List<FullMember>? ->
Stream.of(members)
.map { it.member }
.toList()
}
return Transformations.map(fullMembers) { currentMembership: List<Recipient>? ->
return fullMembers.map { currentMembership: List<Recipient>? ->
val cachedMembers: MutableSet<Recipient> = MapUtil.getOrDefault(sessionMemberCache, groupId, HashSet()).toMutableSet()
cachedMembers.addAll(currentMembership!!)
sessionMemberCache[groupId] = cachedMembers

View File

@@ -15,6 +15,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentManager
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
@@ -35,7 +36,7 @@ class RelinkDevicesReminderBottomSheetFragment : ComposeBottomSheetDialogFragmen
.padding(16.dp)
.wrapContentSize()
) {
Handle()
BottomSheets.Handle()
Column(horizontalAlignment = Alignment.Start) {
Text(
text = stringResource(id = R.string.RelinkDevicesReminderFragment__relink_your_devices),

View File

@@ -4,7 +4,7 @@ import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.map
import com.google.protobuf.InvalidProtocolBufferException
import com.mobilecoin.lib.Mnemonics
import com.mobilecoin.lib.exceptions.BadMnemonicException
@@ -70,7 +70,7 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa
private val liveCurrentCurrency: MutableLiveData<Currency> by lazy { MutableLiveData(currentCurrency()) }
private val enclaveFailure: MutableLiveData<Boolean> by lazy { MutableLiveData(false) }
private val liveMobileCoinLedger: MutableLiveData<MobileCoinLedgerWrapper> by lazy { MutableLiveData(mobileCoinLatestFullLedger()) }
private val liveMobileCoinBalance: LiveData<Balance> by lazy { Transformations.map(liveMobileCoinLedger) { obj: MobileCoinLedgerWrapper -> obj.balance } }
private val liveMobileCoinBalance: LiveData<Balance> by lazy { liveMobileCoinLedger.map { obj: MobileCoinLedgerWrapper -> obj.balance } }
public override fun onFirstEverAppLaunch() {}

View File

@@ -9,7 +9,7 @@ import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.map
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import org.signal.core.util.Stopwatch
@@ -140,8 +140,8 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
}
val galleryItemsWithSelection = LiveDataUtil.combineLatest(
Transformations.map(viewModel.state) { it.items },
Transformations.map(viewStateLiveData) { it.selectedMedia }
viewModel.state.map { it.items },
viewStateLiveData.map { it.selectedMedia }
) { galleryItems, selectedMedia ->
galleryItems.map {
if (it is MediaGallerySelectableItem.FileModel) {

View File

@@ -19,6 +19,7 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.BottomSheets
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.CommunicationActions
@@ -41,7 +42,7 @@ class ContactSupportBottomSheetFragment : ComposeBottomSheetDialogFragment() {
.wrapContentSize(Alignment.Center)
.padding(16.dp)
) {
Handle()
BottomSheets.Handle()
Text(
text = buildAnnotatedString {
withStyle(SpanStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)) {

View File

@@ -23,6 +23,8 @@ import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Function;
import kotlin.jvm.functions.Function1;
public final class LiveDataUtil {
private LiveDataUtil() {
@@ -82,7 +84,7 @@ public final class LiveDataUtil {
/**
* Performs a map operation on the source observable and then only emits the mapped item if it has changed since the previous emission.
*/
public static <A, B> LiveData<B> mapDistinct(@NonNull LiveData<A> source, @NonNull androidx.arch.core.util.Function<A, B> mapFunction) {
public static <A, B> LiveData<B> mapDistinct(@NonNull LiveData<A> source, @NonNull Function1<A, B> mapFunction) {
return Transformations.distinctUntilChanged(Transformations.map(source, mapFunction));
}