mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Add split-pane UI for create group screen.
This commit is contained in:
committed by
Greyson Parrelli
parent
d763baa270
commit
d6446d2954
@@ -117,7 +117,7 @@ private fun NewConversationScreen(
|
||||
contract = FindByActivity.Contract(),
|
||||
onResult = { recipientId ->
|
||||
if (recipientId != null) {
|
||||
viewModel.onMessage(recipientId)
|
||||
viewModel.openConversation(recipientId)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -125,14 +125,16 @@ private fun NewConversationScreen(
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val callbacks = remember {
|
||||
object : UiCallbacks {
|
||||
override fun onSearchQueryChanged(query: String) = viewModel.onSearchQueryChanged(query)
|
||||
override fun onCreateNewGroup() = createGroupLauncher.launch(CreateGroupActivity.newIntent(context))
|
||||
override fun onFindByUsername() = findByLauncher.launch(FindByMode.USERNAME)
|
||||
override fun onFindByPhoneNumber() = findByLauncher.launch(FindByMode.PHONE_NUMBER)
|
||||
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
||||
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = viewModel.onRecipientSelected(id, phone)
|
||||
override fun onMessage(id: RecipientId) = viewModel.onMessage(id)
|
||||
override fun onVoiceCall(recipient: Recipient) = CommunicationActions.startVoiceCall(context, recipient, viewModel::onUserAlreadyInACall)
|
||||
override fun onVideoCall(recipient: Recipient) = CommunicationActions.startVideoCall(context, recipient, viewModel::onUserAlreadyInACall)
|
||||
override suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean = true
|
||||
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = viewModel.openConversation(id, phone)
|
||||
override fun onPendingRecipientSelectionsConsumed() = Unit
|
||||
override fun onMessage(id: RecipientId) = viewModel.openConversation(id)
|
||||
override fun onVoiceCall(recipient: Recipient) = CommunicationActions.startVoiceCall(context, recipient, viewModel::showUserAlreadyInACall)
|
||||
override fun onVideoCall(recipient: Recipient) = CommunicationActions.startVideoCall(context, recipient, viewModel::showUserAlreadyInACall)
|
||||
|
||||
override fun onRemove(recipient: Recipient) = viewModel.showRemoveConfirmation(recipient)
|
||||
override fun onRemoveConfirmed(recipient: Recipient) {
|
||||
@@ -146,8 +148,8 @@ private fun NewConversationScreen(
|
||||
|
||||
override fun onInviteToSignal() = context.startActivity(AppSettingsActivity.invite(context))
|
||||
override fun onRefresh() = viewModel.refresh()
|
||||
override fun onUserMessageDismissed(userMessage: UserMessage) = viewModel.onUserMessageDismissed()
|
||||
override fun onContactsListReset() = viewModel.onContactsListReset()
|
||||
override fun onUserMessageDismissed(userMessage: UserMessage) = viewModel.clearUserMessage()
|
||||
override fun onContactsListReset() = viewModel.clearShouldResetContactsList()
|
||||
override fun onBackPressed() = closeScreen()
|
||||
}
|
||||
}
|
||||
@@ -256,7 +258,7 @@ private fun NewConversationScreenUi(
|
||||
snackbarHostState = snackbarHostState
|
||||
)
|
||||
|
||||
if (uiState.isRefreshingRecipient) {
|
||||
if (uiState.isLookingUpRecipient) {
|
||||
Dialogs.IndeterminateProgressDialog()
|
||||
}
|
||||
}
|
||||
@@ -320,11 +322,13 @@ private interface UiCallbacks :
|
||||
fun onBackPressed()
|
||||
|
||||
object Empty : UiCallbacks {
|
||||
override fun onSearchQueryChanged(query: String) = Unit
|
||||
override fun onCreateNewGroup() = Unit
|
||||
override fun onFindByUsername() = Unit
|
||||
override fun onFindByPhoneNumber() = Unit
|
||||
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
||||
override suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean = true
|
||||
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
|
||||
override fun onPendingRecipientSelectionsConsumed() = Unit
|
||||
override fun onMessage(id: RecipientId) = Unit
|
||||
override fun onVoiceCall(recipient: Recipient) = Unit
|
||||
override fun onVideoCall(recipient: Recipient) = Unit
|
||||
@@ -347,6 +351,7 @@ private fun NewConversationRecipientPicker(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
RecipientPicker(
|
||||
searchQuery = uiState.searchQuery,
|
||||
isRefreshing = uiState.isRefreshingContacts,
|
||||
shouldResetContactsList = uiState.shouldResetContactsList,
|
||||
callbacks = RecipientPickerCallbacks(
|
||||
|
||||
@@ -37,41 +37,39 @@ class NewConversationViewModel : ViewModel() {
|
||||
|
||||
private val contactsManagementRepo = ContactsManagementRepository(AppDependencies.application)
|
||||
|
||||
fun onMessage(id: RecipientId): Unit = openConversation(recipientId = id)
|
||||
fun onSearchQueryChanged(query: String) {
|
||||
internalUiState.update { it.copy(searchQuery = query) }
|
||||
}
|
||||
|
||||
fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) {
|
||||
fun openConversation(recipientId: RecipientId) {
|
||||
internalUiState.update { it.copy(pendingDestination = recipientId) }
|
||||
}
|
||||
|
||||
fun openConversation(id: RecipientId?, phone: PhoneNumber?) {
|
||||
when {
|
||||
id != null -> openConversation(recipientId = id)
|
||||
|
||||
SignalStore.account.isRegistered -> {
|
||||
Log.d(TAG, "[onRecipientSelected] Missing recipientId: attempting to look up.")
|
||||
resolveAndOpenConversation(phone)
|
||||
Log.d(TAG, "[openConversation] Missing recipientId: attempting to look up.")
|
||||
resolveAndOpenConversation(phone!!)
|
||||
}
|
||||
|
||||
else -> Log.w(TAG, "[onRecipientSelected] Cannot look up recipient: account not registered.")
|
||||
else -> Log.w(TAG, "[openConversation] Cannot look up recipient: account not registered.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun openConversation(recipientId: RecipientId) {
|
||||
internalUiState.update { it.copy(pendingDestination = recipientId) }
|
||||
}
|
||||
|
||||
private fun resolveAndOpenConversation(phone: PhoneNumber?) {
|
||||
private fun resolveAndOpenConversation(phone: PhoneNumber) {
|
||||
viewModelScope.launch {
|
||||
internalUiState.update { it.copy(isRefreshingRecipient = true) }
|
||||
internalUiState.update { it.copy(isLookingUpRecipient = true) }
|
||||
|
||||
val lookupResult = withContext(Dispatchers.IO) {
|
||||
if (phone != null) {
|
||||
RecipientRepository.lookupNewE164(inputE164 = phone.value)
|
||||
} else {
|
||||
RecipientRepository.LookupResult.InvalidEntry
|
||||
}
|
||||
RecipientRepository.lookupNewE164(inputE164 = phone.value)
|
||||
}
|
||||
|
||||
when (lookupResult) {
|
||||
is RecipientRepository.LookupResult.Success -> {
|
||||
val recipient = Recipient.resolved(lookupResult.recipientId)
|
||||
internalUiState.update { it.copy(isRefreshingRecipient = false) }
|
||||
internalUiState.update { it.copy(isLookingUpRecipient = false) }
|
||||
|
||||
if (recipient.isRegistered && recipient.hasServiceId) {
|
||||
openConversation(recipient.id)
|
||||
@@ -83,7 +81,7 @@ class NewConversationViewModel : ViewModel() {
|
||||
is RecipientRepository.LookupResult.NotFound, is RecipientRepository.LookupResult.InvalidEntry -> {
|
||||
internalUiState.update {
|
||||
it.copy(
|
||||
isRefreshingRecipient = false,
|
||||
isLookingUpRecipient = false,
|
||||
userMessage = Info.RecipientNotSignalUser(phone)
|
||||
)
|
||||
}
|
||||
@@ -92,7 +90,7 @@ class NewConversationViewModel : ViewModel() {
|
||||
is RecipientRepository.LookupResult.NetworkError -> {
|
||||
internalUiState.update {
|
||||
it.copy(
|
||||
isRefreshingRecipient = false,
|
||||
isLookingUpRecipient = false,
|
||||
userMessage = Info.NetworkError
|
||||
)
|
||||
}
|
||||
@@ -137,11 +135,11 @@ class NewConversationViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onUserAlreadyInACall() {
|
||||
fun showUserAlreadyInACall() {
|
||||
internalUiState.update { it.copy(userMessage = Info.UserAlreadyInAnotherCall) }
|
||||
}
|
||||
|
||||
fun onContactsListReset() {
|
||||
fun clearShouldResetContactsList() {
|
||||
internalUiState.update { it.copy(shouldResetContactsList = false) }
|
||||
}
|
||||
|
||||
@@ -157,14 +155,15 @@ class NewConversationViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onUserMessageDismissed() {
|
||||
fun clearUserMessage() {
|
||||
internalUiState.update { it.copy(userMessage = null) }
|
||||
}
|
||||
}
|
||||
|
||||
data class NewConversationUiState(
|
||||
val forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPaneOnCompactLandscape,
|
||||
val isRefreshingRecipient: Boolean = false,
|
||||
val searchQuery: String = "",
|
||||
val isLookingUpRecipient: Boolean = false,
|
||||
val isRefreshingContacts: Boolean = false,
|
||||
val shouldResetContactsList: Boolean = false,
|
||||
val pendingDestination: RecipientId? = null,
|
||||
@@ -174,7 +173,7 @@ data class NewConversationUiState(
|
||||
sealed interface Info : UserMessage {
|
||||
data class RecipientRemoved(val recipient: Recipient) : Info
|
||||
data class RecipientBlocked(val recipient: Recipient) : Info
|
||||
data class RecipientNotSignalUser(val phone: PhoneNumber?) : Info
|
||||
data class RecipientNotSignalUser(val phone: PhoneNumber) : Info
|
||||
data object UserAlreadyInAnotherCall : Info
|
||||
data object NetworkError : Info
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.fragment.compose.rememberFragmentState
|
||||
@@ -35,15 +37,20 @@ import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Fragments
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments
|
||||
import org.thoughtcrime.securesms.conversation.RecipientPicker.DisplayMode.Companion.flag
|
||||
import org.thoughtcrime.securesms.conversation.RecipientPickerCallbacks.ContextMenu
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.recipients.PhoneNumber
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -56,21 +63,24 @@ import java.util.function.Consumer
|
||||
*/
|
||||
@Composable
|
||||
fun RecipientPicker(
|
||||
searchQuery: String,
|
||||
displayModes: Set<RecipientPicker.DisplayMode> = setOf(RecipientPicker.DisplayMode.ALL),
|
||||
selectionLimits: SelectionLimits? = ContactSelectionArguments.Defaults.SELECTION_LIMITS,
|
||||
isRefreshing: Boolean,
|
||||
focusAndShowKeyboard: Boolean = LocalConfiguration.current.screenHeightDp.dp > 600.dp,
|
||||
shouldResetContactsList: Boolean,
|
||||
pendingRecipientSelections: Set<RecipientId> = emptySet(),
|
||||
shouldResetContactsList: Boolean = false,
|
||||
listBottomPadding: Dp? = null,
|
||||
clipListToPadding: Boolean = ContactSelectionArguments.Defaults.RECYCLER_CHILD_CLIPPING,
|
||||
callbacks: RecipientPickerCallbacks,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
) {
|
||||
RecipientSearchField(
|
||||
onFilterChanged = { filter ->
|
||||
searchQuery = filter
|
||||
},
|
||||
searchQuery = searchQuery,
|
||||
onFilterChanged = { filter -> callbacks.listActions.onSearchQueryChanged(query = filter) },
|
||||
focusAndShowKeyboard = focusAndShowKeyboard,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -78,9 +88,14 @@ fun RecipientPicker(
|
||||
)
|
||||
|
||||
RecipientSearchResultsList(
|
||||
displayModes = displayModes,
|
||||
selectionLimits = selectionLimits,
|
||||
searchQuery = searchQuery,
|
||||
isRefreshing = isRefreshing,
|
||||
pendingRecipientSelections = pendingRecipientSelections,
|
||||
shouldResetContactsList = shouldResetContactsList,
|
||||
bottomPadding = listBottomPadding,
|
||||
clipListToPadding = clipListToPadding,
|
||||
callbacks = callbacks,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -96,6 +111,7 @@ fun RecipientPicker(
|
||||
*/
|
||||
@Composable
|
||||
private fun RecipientSearchField(
|
||||
searchQuery: String,
|
||||
onFilterChanged: (String) -> Unit,
|
||||
@StringRes hintText: Int? = null,
|
||||
focusAndShowKeyboard: Boolean = false,
|
||||
@@ -108,6 +124,10 @@ private fun RecipientSearchField(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(searchQuery) {
|
||||
wrappedView.setText(searchQuery)
|
||||
}
|
||||
|
||||
// TODO [jeff] This causes the keyboard to re-open on rotation, which doesn't match the existing behavior of ContactFilterView. To fix this,
|
||||
// RecipientSearchField needs to be converted to compose so we can use FocusRequestor.
|
||||
LaunchedEffect(focusAndShowKeyboard) {
|
||||
@@ -134,17 +154,26 @@ private fun RecipientSearchField(
|
||||
|
||||
@Composable
|
||||
private fun RecipientSearchResultsList(
|
||||
displayModes: Set<RecipientPicker.DisplayMode>,
|
||||
searchQuery: String,
|
||||
isRefreshing: Boolean,
|
||||
pendingRecipientSelections: Set<RecipientId>,
|
||||
shouldResetContactsList: Boolean,
|
||||
selectionLimits: SelectionLimits? = ContactSelectionArguments.Defaults.SELECTION_LIMITS,
|
||||
bottomPadding: Dp? = null,
|
||||
clipListToPadding: Boolean = ContactSelectionArguments.Defaults.RECYCLER_CHILD_CLIPPING,
|
||||
callbacks: RecipientPickerCallbacks,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val fragmentArgs = ContactSelectionArguments(
|
||||
displayMode = displayModes.flag,
|
||||
isRefreshable = callbacks.refresh != null,
|
||||
enableCreateNewGroup = callbacks.newConversation != null,
|
||||
enableFindByUsername = callbacks.findByUsername != null,
|
||||
enableFindByPhoneNumber = callbacks.findByPhoneNumber != null
|
||||
enableFindByPhoneNumber = callbacks.findByPhoneNumber != null,
|
||||
selectionLimits = selectionLimits,
|
||||
recyclerPadBottom = with(LocalDensity.current) { bottomPadding?.toPx()?.toInt() ?: ContactSelectionArguments.Defaults.RECYCLER_PADDING_BOTTOM },
|
||||
recyclerChildClipping = clipListToPadding
|
||||
).toArgumentBundle()
|
||||
|
||||
val fragmentState = rememberFragmentState()
|
||||
@@ -186,6 +215,22 @@ private fun RecipientSearchResultsList(
|
||||
wasRefreshing = isRefreshing
|
||||
}
|
||||
|
||||
LaunchedEffect(pendingRecipientSelections) {
|
||||
if (pendingRecipientSelections.isNotEmpty()) {
|
||||
currentFragment?.let { fragment ->
|
||||
pendingRecipientSelections.forEach { recipientId ->
|
||||
currentFragment?.addRecipientToSelectionIfAble(recipientId)
|
||||
}
|
||||
callbacks.listActions.onPendingRecipientSelectionsConsumed()
|
||||
|
||||
callbacks.listActions.onSelectionChanged(
|
||||
newSelections = fragment.selectedContacts,
|
||||
totalMembersCount = fragment.totalMemberCount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldResetContactsList) {
|
||||
if (shouldResetContactsList) {
|
||||
currentFragment?.reset()
|
||||
@@ -226,19 +271,26 @@ private fun ContactSelectionListFragment.setUpCallbacks(
|
||||
chatType: Optional<ChatType?>,
|
||||
resultConsumer: Consumer<Boolean?>
|
||||
) {
|
||||
val recipientId = recipientId.get()
|
||||
val shouldAllowSelection = callbacks.listActions.shouldAllowSelection(recipientId)
|
||||
if (shouldAllowSelection) {
|
||||
callbacks.listActions.onRecipientSelected(
|
||||
id = recipientId,
|
||||
phone = number?.let(::PhoneNumber)
|
||||
)
|
||||
val recipientId = recipientId.orNull()
|
||||
val phone = number?.let(::PhoneNumber)
|
||||
|
||||
coroutineScope.launch {
|
||||
val shouldAllowSelection = callbacks.listActions.shouldAllowSelection(recipientId, phone)
|
||||
if (shouldAllowSelection) {
|
||||
callbacks.listActions.onRecipientSelected(recipientId, phone)
|
||||
}
|
||||
resultConsumer.accept(shouldAllowSelection)
|
||||
}
|
||||
resultConsumer.accept(shouldAllowSelection)
|
||||
}
|
||||
|
||||
override fun onContactDeselected(recipientId: Optional<RecipientId?>, number: String?, chatType: Optional<ChatType?>) = Unit
|
||||
override fun onSelectionChanged() = Unit
|
||||
|
||||
override fun onSelectionChanged() {
|
||||
callbacks.listActions.onSelectionChanged(
|
||||
newSelections = fragment.selectedContacts,
|
||||
totalMembersCount = fragment.totalMemberCount
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
fragment.setOnItemLongClickListener { anchorView, contactSearchKey, recyclerView ->
|
||||
@@ -331,6 +383,7 @@ private suspend fun showItemContextMenu(
|
||||
@Composable
|
||||
private fun RecipientPickerPreview() {
|
||||
RecipientPicker(
|
||||
searchQuery = "",
|
||||
isRefreshing = false,
|
||||
shouldResetContactsList = false,
|
||||
callbacks = RecipientPickerCallbacks(
|
||||
@@ -353,13 +406,18 @@ data class RecipientPickerCallbacks(
|
||||
*
|
||||
* This is called before [onRecipientSelected] to provide a chance to prevent the selection.
|
||||
*/
|
||||
fun shouldAllowSelection(id: RecipientId): Boolean
|
||||
fun onSearchQueryChanged(query: String)
|
||||
suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean
|
||||
fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?)
|
||||
fun onContactsListReset()
|
||||
fun onSelectionChanged(newSelections: List<SelectedContact>, totalMembersCount: Int) = Unit
|
||||
fun onPendingRecipientSelectionsConsumed()
|
||||
fun onContactsListReset() = Unit
|
||||
|
||||
object Empty : ListActions {
|
||||
override fun shouldAllowSelection(id: RecipientId): Boolean = false
|
||||
override fun onSearchQueryChanged(query: String) = Unit
|
||||
override suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean = true
|
||||
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
|
||||
override fun onPendingRecipientSelectionsConsumed() = Unit
|
||||
override fun onContactsListReset() = Unit
|
||||
}
|
||||
}
|
||||
@@ -389,3 +447,28 @@ data class RecipientPickerCallbacks(
|
||||
fun onFindByPhoneNumber()
|
||||
}
|
||||
}
|
||||
|
||||
object RecipientPicker {
|
||||
/**
|
||||
* Enum wrapper for [ContactSelectionDisplayMode].
|
||||
*/
|
||||
enum class DisplayMode(val flag: Int) {
|
||||
PUSH(flag = ContactSelectionDisplayMode.FLAG_PUSH),
|
||||
SMS(flag = ContactSelectionDisplayMode.FLAG_SMS),
|
||||
ACTIVE_GROUPS(flag = ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS),
|
||||
INACTIVE_GROUPS(flag = ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS),
|
||||
SELF(flag = ContactSelectionDisplayMode.FLAG_SELF),
|
||||
BLOCK(flag = ContactSelectionDisplayMode.FLAG_BLOCK),
|
||||
HIDE_GROUPS_V1(flag = ContactSelectionDisplayMode.FLAG_HIDE_GROUPS_V1),
|
||||
HIDE_NEW(flag = ContactSelectionDisplayMode.FLAG_HIDE_NEW),
|
||||
HIDE_RECENT_HEADER(flag = ContactSelectionDisplayMode.FLAG_HIDE_RECENT_HEADER),
|
||||
GROUPS_AFTER_CONTACTS(flag = ContactSelectionDisplayMode.FLAG_GROUPS_AFTER_CONTACTS),
|
||||
GROUP_MEMBERS(flag = ContactSelectionDisplayMode.FLAG_GROUP_MEMBERS),
|
||||
ALL(flag = ContactSelectionDisplayMode.FLAG_ALL);
|
||||
|
||||
companion object {
|
||||
val Set<DisplayMode>.flag: Int
|
||||
get() = fold(initial = 0) { acc, displayMode -> acc or displayMode.flag }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user