mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-08 09:18:39 +01:00
Add in-call info sheet for call links.
This commit is contained in:
committed by
Cody Henthorne
parent
369ca189d3
commit
886c149c3f
@@ -470,6 +470,7 @@ dependencies {
|
|||||||
implementation libs.androidx.gridlayout
|
implementation libs.androidx.gridlayout
|
||||||
implementation libs.androidx.exifinterface
|
implementation libs.androidx.exifinterface
|
||||||
implementation libs.androidx.compose.rxjava3
|
implementation libs.androidx.compose.rxjava3
|
||||||
|
implementation libs.androidx.compose.runtime.livedata
|
||||||
implementation libs.androidx.constraintlayout
|
implementation libs.androidx.constraintlayout
|
||||||
implementation libs.androidx.multidex
|
implementation libs.androidx.multidex
|
||||||
implementation libs.androidx.navigation.fragment.ktx
|
implementation libs.androidx.navigation.fragment.ktx
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import org.signal.core.util.logging.Log;
|
|||||||
import org.signal.libsignal.protocol.IdentityKey;
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||||
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
|
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.CallParticipantsListUpdatePopupWindow;
|
||||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||||
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
|
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.events.WebRtcViewModel;
|
||||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
|
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
|
||||||
@@ -901,7 +903,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCallInfoClicked() {
|
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
|
@Override
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
package org.thoughtcrime.securesms.avatar.text
|
package org.thoughtcrime.securesms.avatar.text
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
import org.thoughtcrime.securesms.avatar.Avatar
|
import org.thoughtcrime.securesms.avatar.Avatar
|
||||||
import org.thoughtcrime.securesms.avatar.Avatars
|
import org.thoughtcrime.securesms.avatar.Avatars
|
||||||
import org.thoughtcrime.securesms.util.livedata.Store
|
import org.thoughtcrime.securesms.util.livedata.Store
|
||||||
@@ -12,7 +12,7 @@ class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() {
|
|||||||
|
|
||||||
private val store = Store(TextAvatarCreationState(initialText))
|
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) {
|
fun setColor(colorPair: Avatars.ColorPair) {
|
||||||
store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) }
|
store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) }
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ private fun SignalCallRowPreview() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun SignalCallRow(
|
fun SignalCallRow(
|
||||||
callLink: CallLinkTable.CallLink,
|
callLink: CallLinkTable.CallLink,
|
||||||
onJoinClicked: () -> Unit,
|
onJoinClicked: (() -> Unit)?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -122,13 +122,15 @@ fun SignalCallRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
if (onJoinClicked != null) {
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
|
||||||
Buttons.Small(
|
Buttons.Small(
|
||||||
onClick = onJoinClicked,
|
onClick = onJoinClicked,
|
||||||
modifier = Modifier.align(CenterVertically)
|
modifier = Modifier.align(CenterVertically)
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
|
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -34,6 +34,7 @@ import androidx.core.app.ShareCompat
|
|||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||||
|
import org.signal.core.ui.BottomSheets
|
||||||
import org.signal.core.ui.Buttons
|
import org.signal.core.ui.Buttons
|
||||||
import org.signal.core.ui.Dividers
|
import org.signal.core.ui.Dividers
|
||||||
import org.signal.core.ui.Rows
|
import org.signal.core.ui.Rows
|
||||||
@@ -86,7 +87,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
|||||||
) {
|
) {
|
||||||
val callLink: CallLinkTable.CallLink by viewModel.callLink
|
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))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -3,9 +3,10 @@ package org.thoughtcrime.securesms.components.settings.conversation
|
|||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.distinctUntilChanged
|
||||||
|
import androidx.lifecycle.map
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
@@ -62,7 +63,7 @@ sealed class ConversationSettingsViewModel(
|
|||||||
protected val disposable = CompositeDisposable()
|
protected val disposable = CompositeDisposable()
|
||||||
|
|
||||||
init {
|
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 updater: LiveData<Long> = LiveDataUtil.combineLatest(threadId, sharedMediaUpdateTrigger) { tId, _ -> tId }
|
||||||
|
|
||||||
val sharedMedia: LiveData<Optional<Cursor>> = LiveDataUtil.mapAsync(SignalExecutors.BOUNDED, updater) { tId ->
|
val sharedMedia: LiveData<Optional<Cursor>> = LiveDataUtil.mapAsync(SignalExecutors.BOUNDED, updater) { tId ->
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -30,6 +30,7 @@ import androidx.fragment.app.viewModels
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import org.signal.core.ui.BottomSheets
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||||
@@ -49,7 +50,7 @@ class WebRtcAudioOutputBottomSheet : ComposeBottomSheetDialogFragment(), DialogI
|
|||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.wrapContentSize()
|
.wrapContentSize()
|
||||||
) {
|
) {
|
||||||
Handle()
|
BottomSheets.Handle()
|
||||||
DeviceList(audioOutputOptions = viewModel.audioRoutes.toImmutableList(), initialDeviceId = viewModel.defaultDeviceId, modifier = Modifier.fillMaxWidth(), onDeviceSelected = viewModel.onClick)
|
DeviceList(audioOutputOptions = viewModel.audioRoutes.toImmutableList(), initialDeviceId = viewModel.defaultDeviceId, modifier = Modifier.fillMaxWidth(), onDeviceSelected = viewModel.onClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-32
@@ -4,16 +4,9 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
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.ComposeView
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||||
@@ -41,29 +34,4 @@ abstract class ComposeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetD
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
abstract fun SheetContent()
|
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -2,9 +2,10 @@ package org.thoughtcrime.securesms.contacts.paged
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.map
|
||||||
|
import androidx.lifecycle.switchMap
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
@@ -48,8 +49,8 @@ class ContactSearchViewModel(
|
|||||||
private val selectionStore = Store<Set<ContactSearchKey>>(emptySet())
|
private val selectionStore = Store<Set<ContactSearchKey>>(emptySet())
|
||||||
private val errorEvents = PublishSubject.create<ContactSearchError>()
|
private val errorEvents = PublishSubject.create<ContactSearchError>()
|
||||||
|
|
||||||
val controller: LiveData<PagingController<ContactSearchKey>> = Transformations.map(pagedData) { it.controller }
|
val controller: LiveData<PagingController<ContactSearchKey>> = pagedData.map { it.controller }
|
||||||
val data: LiveData<List<ContactSearchData>> = Transformations.switchMap(pagedData) { it.data }
|
val data: LiveData<List<ContactSearchData>> = pagedData.switchMap { it.data }
|
||||||
val configurationState: LiveData<ContactSearchState> = configurationStore.stateLiveData
|
val configurationState: LiveData<ContactSearchState> = configurationStore.stateLiveData
|
||||||
val selectionState: LiveData<Set<ContactSearchKey>> = selectionStore.stateLiveData
|
val selectionState: LiveData<Set<ContactSearchKey>> = selectionStore.stateLiveData
|
||||||
val errorEventsStream: Observable<ContactSearchError> = errorEvents
|
val errorEventsStream: Observable<ContactSearchError> = errorEvents
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package org.thoughtcrime.securesms.conversation.colors
|
package org.thoughtcrime.securesms.conversation.colors
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.map
|
||||||
|
import androidx.lifecycle.switchMap
|
||||||
import com.annimon.stream.Stream
|
import com.annimon.stream.Stream
|
||||||
import org.signal.core.util.MapUtil
|
import org.signal.core.util.MapUtil
|
||||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette.Names.all
|
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette.Names.all
|
||||||
@@ -23,13 +24,13 @@ object NameColors {
|
|||||||
recipientId: LiveData<RecipientId>,
|
recipientId: LiveData<RecipientId>,
|
||||||
sessionMemberCache: MutableMap<GroupId, Set<Recipient>>
|
sessionMemberCache: MutableMap<GroupId, Set<Recipient>>
|
||||||
): LiveData<Map<RecipientId, NameColor>> {
|
): LiveData<Map<RecipientId, NameColor>> {
|
||||||
val recipient = Transformations.switchMap(recipientId) { r: RecipientId? -> Recipient.live(r!!).liveData }
|
val recipient = recipientId.switchMap { r: RecipientId? -> Recipient.live(r!!).liveData }
|
||||||
val group = Transformations.map(recipient) { obj: Recipient -> obj.groupId }
|
val group = recipient.map { obj: Recipient -> obj.groupId }
|
||||||
val groupMembers = Transformations.switchMap(group) { g: Optional<GroupId> ->
|
val groupMembers = group.switchMap { g: Optional<GroupId> ->
|
||||||
g.map { groupId: GroupId -> this.getSessionGroupRecipients(groupId, sessionMemberCache) }
|
g.map { groupId: GroupId -> this.getSessionGroupRecipients(groupId, sessionMemberCache) }
|
||||||
.orElseGet { DefaultValueLiveData(emptySet()) }
|
.orElseGet { DefaultValueLiveData(emptySet()) }
|
||||||
}
|
}
|
||||||
return Transformations.map(groupMembers) { members: Set<Recipient>? ->
|
return groupMembers.map { members: Set<Recipient>? ->
|
||||||
val sorted = Stream.of(members)
|
val sorted = Stream.of(members)
|
||||||
.filter { member: Recipient? -> member != Recipient.self() }
|
.filter { member: Recipient? -> member != Recipient.self() }
|
||||||
.sortBy { obj: Recipient -> obj.requireStringId() }
|
.sortBy { obj: Recipient -> obj.requireStringId() }
|
||||||
@@ -44,14 +45,13 @@ object NameColors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getSessionGroupRecipients(groupId: GroupId, sessionMemberCache: MutableMap<GroupId, Set<Recipient>>): LiveData<Set<Recipient>> {
|
private fun getSessionGroupRecipients(groupId: GroupId, sessionMemberCache: MutableMap<GroupId, Set<Recipient>>): LiveData<Set<Recipient>> {
|
||||||
val fullMembers = Transformations.map(
|
val fullMembers = LiveGroup(groupId).fullMembers.map { members: List<FullMember>? ->
|
||||||
LiveGroup(groupId).fullMembers
|
|
||||||
) { members: List<FullMember>? ->
|
|
||||||
Stream.of(members)
|
Stream.of(members)
|
||||||
.map { it.member }
|
.map { it.member }
|
||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
return Transformations.map(fullMembers) { currentMembership: List<Recipient>? ->
|
|
||||||
|
return fullMembers.map { currentMembership: List<Recipient>? ->
|
||||||
val cachedMembers: MutableSet<Recipient> = MapUtil.getOrDefault(sessionMemberCache, groupId, HashSet()).toMutableSet()
|
val cachedMembers: MutableSet<Recipient> = MapUtil.getOrDefault(sessionMemberCache, groupId, HashSet()).toMutableSet()
|
||||||
cachedMembers.addAll(currentMembership!!)
|
cachedMembers.addAll(currentMembership!!)
|
||||||
sessionMemberCache[groupId] = cachedMembers
|
sessionMemberCache[groupId] = cachedMembers
|
||||||
|
|||||||
+2
-1
@@ -15,6 +15,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import org.signal.core.ui.BottomSheets
|
||||||
import org.signal.core.ui.Buttons
|
import org.signal.core.ui.Buttons
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||||
@@ -35,7 +36,7 @@ class RelinkDevicesReminderBottomSheetFragment : ComposeBottomSheetDialogFragmen
|
|||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.wrapContentSize()
|
.wrapContentSize()
|
||||||
) {
|
) {
|
||||||
Handle()
|
BottomSheets.Handle()
|
||||||
Column(horizontalAlignment = Alignment.Start) {
|
Column(horizontalAlignment = Alignment.Start) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.RelinkDevicesReminderFragment__relink_your_devices),
|
text = stringResource(id = R.string.RelinkDevicesReminderFragment__relink_your_devices),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import androidx.annotation.VisibleForTesting
|
|||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.map
|
||||||
import com.google.protobuf.InvalidProtocolBufferException
|
import com.google.protobuf.InvalidProtocolBufferException
|
||||||
import com.mobilecoin.lib.Mnemonics
|
import com.mobilecoin.lib.Mnemonics
|
||||||
import com.mobilecoin.lib.exceptions.BadMnemonicException
|
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 liveCurrentCurrency: MutableLiveData<Currency> by lazy { MutableLiveData(currentCurrency()) }
|
||||||
private val enclaveFailure: MutableLiveData<Boolean> by lazy { MutableLiveData(false) }
|
private val enclaveFailure: MutableLiveData<Boolean> by lazy { MutableLiveData(false) }
|
||||||
private val liveMobileCoinLedger: MutableLiveData<MobileCoinLedgerWrapper> by lazy { MutableLiveData(mobileCoinLatestFullLedger()) }
|
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() {}
|
public override fun onFirstEverAppLaunch() {}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@ import androidx.core.view.updateLayoutParams
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.map
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import org.signal.core.util.Stopwatch
|
import org.signal.core.util.Stopwatch
|
||||||
@@ -140,8 +140,8 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val galleryItemsWithSelection = LiveDataUtil.combineLatest(
|
val galleryItemsWithSelection = LiveDataUtil.combineLatest(
|
||||||
Transformations.map(viewModel.state) { it.items },
|
viewModel.state.map { it.items },
|
||||||
Transformations.map(viewStateLiveData) { it.selectedMedia }
|
viewStateLiveData.map { it.selectedMedia }
|
||||||
) { galleryItems, selectedMedia ->
|
) { galleryItems, selectedMedia ->
|
||||||
galleryItems.map {
|
galleryItems.map {
|
||||||
if (it is MediaGallerySelectableItem.FileModel) {
|
if (it is MediaGallerySelectableItem.FileModel) {
|
||||||
|
|||||||
+2
-1
@@ -19,6 +19,7 @@ import androidx.compose.ui.text.withStyle
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.signal.core.ui.BottomSheets
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
@@ -41,7 +42,7 @@ class ContactSupportBottomSheetFragment : ComposeBottomSheetDialogFragment() {
|
|||||||
.wrapContentSize(Alignment.Center)
|
.wrapContentSize(Alignment.Center)
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Handle()
|
BottomSheets.Handle()
|
||||||
Text(
|
Text(
|
||||||
text = buildAnnotatedString {
|
text = buildAnnotatedString {
|
||||||
withStyle(SpanStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)) {
|
withStyle(SpanStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)) {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import kotlin.jvm.functions.Function1;
|
||||||
|
|
||||||
public final class LiveDataUtil {
|
public final class LiveDataUtil {
|
||||||
|
|
||||||
private 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.
|
* 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));
|
return Transformations.distinctUntilChanged(Transformations.map(source, mapFunction));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.ui
|
||||||
|
|
||||||
|
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.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.signal.core.ui.theme.SignalTheme
|
||||||
|
|
||||||
|
object BottomSheets {
|
||||||
|
/**
|
||||||
|
* Handle for bottom sheets
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun HandlePreview() {
|
||||||
|
SignalTheme(isDarkMode = false) {
|
||||||
|
BottomSheets.Handle()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,6 +103,7 @@ object Rows {
|
|||||||
fun TextRow(
|
fun TextRow(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
iconModifier: Modifier = Modifier,
|
||||||
icon: ImageVector? = null,
|
icon: ImageVector? = null,
|
||||||
foregroundTint: Color = MaterialTheme.colorScheme.onSurface
|
foregroundTint: Color = MaterialTheme.colorScheme.onSurface
|
||||||
) {
|
) {
|
||||||
@@ -115,14 +116,15 @@ object Rows {
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = foregroundTint
|
tint = foregroundTint,
|
||||||
|
modifier = iconModifier
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(24.dp))
|
Spacer(modifier = Modifier.width(24.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f).align(CenterVertically),
|
||||||
color = foregroundTint
|
color = foregroundTint
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -137,7 +139,7 @@ object Rows {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun defaultPadding(): PaddingValues {
|
fun defaultPadding(): PaddingValues {
|
||||||
return PaddingValues(
|
return PaddingValues(
|
||||||
horizontal = dimensionResource(id = R.dimen.core_ui__gutter),
|
horizontal = dimensionResource(id = R.dimen.core_ui__gutter),
|
||||||
vertical = 16.dp
|
vertical = 16.dp
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ dependencyResolutionManagement {
|
|||||||
alias('androidx-compose-material3').to('androidx.compose.material3', 'material3').withoutVersion();
|
alias('androidx-compose-material3').to('androidx.compose.material3', 'material3').withoutVersion();
|
||||||
alias('androidx-compose-ui-tooling-preview').to('androidx.compose.ui', 'ui-tooling-preview').withoutVersion()
|
alias('androidx-compose-ui-tooling-preview').to('androidx.compose.ui', 'ui-tooling-preview').withoutVersion()
|
||||||
alias('androidx-compose-ui-tooling-core').to('androidx.compose.ui', 'ui-tooling').withoutVersion()
|
alias('androidx-compose-ui-tooling-core').to('androidx.compose.ui', 'ui-tooling').withoutVersion()
|
||||||
|
alias('androidx-compose-runtime-livedata').to('androidx.compose.runtime', 'runtime-livedata').withoutVersion()
|
||||||
alias('androidx-compose-rxjava3').to('androidx.compose.runtime:runtime-rxjava3:1.4.2')
|
alias('androidx-compose-rxjava3').to('androidx.compose.runtime:runtime-rxjava3:1.4.2')
|
||||||
alias('ktlint-twitter-compose').to('com.twitter.compose.rules:ktlint:0.0.26')
|
alias('ktlint-twitter-compose').to('com.twitter.compose.rules:ktlint:0.0.26')
|
||||||
|
|
||||||
|
|||||||
@@ -612,6 +612,14 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="328c920d5c707fa7ee885cf8c81e4a228d6f9db3dd0d93bf334eead762b6da7f" origin="Generated by Gradle"/>
|
<sha256 value="328c920d5c707fa7ee885cf8c81e4a228d6f9db3dd0d93bf334eead762b6da7f" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="androidx.compose.runtime" name="runtime-livedata" version="1.4.2">
|
||||||
|
<artifact name="runtime-livedata-1.4.2.aar">
|
||||||
|
<sha256 value="caa5f34b824f0df6fbf55cc2a55ffff371d365dbc8383a5faa9f2b57b63276cb" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
<artifact name="runtime-livedata-1.4.2.module">
|
||||||
|
<sha256 value="8dc9966778def6caa45eff77be682b08d649d15b23924f6fd16e90406ee60939" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="androidx.compose.runtime" name="runtime-rxjava3" version="1.4.2">
|
<component group="androidx.compose.runtime" name="runtime-rxjava3" version="1.4.2">
|
||||||
<artifact name="runtime-rxjava3-1.4.2.aar">
|
<artifact name="runtime-rxjava3-1.4.2.aar">
|
||||||
<sha256 value="685877e672f95cf7368ada6e3af07eee107529b6962bf2272b4b1c8d22da7200" origin="Generated by Gradle"/>
|
<sha256 value="685877e672f95cf7368ada6e3af07eee107529b6962bf2272b4b1c8d22da7200" origin="Generated by Gradle"/>
|
||||||
@@ -654,6 +662,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="57031a6ac9b60e5b56792ebf5cde6e16812ff566ed9190cbd188b00b46c13779" origin="Generated by Gradle"/>
|
<sha256 value="57031a6ac9b60e5b56792ebf5cde6e16812ff566ed9190cbd188b00b46c13779" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="androidx.compose.ui" name="ui" version="1.2.1">
|
||||||
|
<artifact name="ui-1.2.1.module">
|
||||||
|
<sha256 value="f068eba19a90f039b626afe78ef74f352f124d2834fde52c3bf1a0ea1726b041" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="androidx.compose.ui" name="ui" version="1.3.1">
|
<component group="androidx.compose.ui" name="ui" version="1.3.1">
|
||||||
<artifact name="ui-1.3.1.module">
|
<artifact name="ui-1.3.1.module">
|
||||||
<sha256 value="ba79d53cd1b581ade9c96cbeba9fb7a57606c966130b38daa29013cfe1056e9f" origin="Generated by Gradle"/>
|
<sha256 value="ba79d53cd1b581ade9c96cbeba9fb7a57606c966130b38daa29013cfe1056e9f" origin="Generated by Gradle"/>
|
||||||
@@ -802,6 +815,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
|
|||||||
<sha256 value="a09871728e5a9d050d2fdcb99a875ef2120dc6deea808f5a6d443dd887e081ca" origin="Generated by Gradle"/>
|
<sha256 value="a09871728e5a9d050d2fdcb99a875ef2120dc6deea808f5a6d443dd887e081ca" origin="Generated by Gradle"/>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
|
<component group="androidx.compose.ui" name="ui-util" version="1.2.1">
|
||||||
|
<artifact name="ui-util-1.2.1.module">
|
||||||
|
<sha256 value="37fdd696cd9562d8e8390d6ca0389980b51e325fbbb65ffb05f7990742009e59" origin="Generated by Gradle"/>
|
||||||
|
</artifact>
|
||||||
|
</component>
|
||||||
<component group="androidx.compose.ui" name="ui-util" version="1.3.3">
|
<component group="androidx.compose.ui" name="ui-util" version="1.3.3">
|
||||||
<artifact name="ui-util-1.3.3.aar">
|
<artifact name="ui-util-1.3.3.aar">
|
||||||
<sha256 value="8276a623a1170dbcc259c806bd71f1ac64f2299ef30ada6e5d23d581d08e172c" origin="Generated by Gradle"/>
|
<sha256 value="8276a623a1170dbcc259c806bd71f1ac64f2299ef30ada6e5d23d581d08e172c" origin="Generated by Gradle"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user