From 35f4643c5cb93370b6b7cc340c82ad7b429f2e27 Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:01:24 +1100 Subject: [PATCH] feat: adding edit contact list views and presenters and various storage functions to build UI state --- app/src/main/AndroidManifest.xml | 1 + .../securesms/database/Storage.kt | 10 +- .../groups/EditClosedGroupActivity.kt | 9 + .../securesms/groups/compose/Components.kt | 20 +- .../securesms/groups/compose/CreateGroup.kt | 3 +- .../securesms/groups/compose/EditGroup.kt | 189 +++++++++++++++--- .../groups/compose/SelectContacts.kt | 3 +- .../thoughtcrime/securesms/ui/Components.kt | 59 ++++-- .../loki/messenger/libsession_util/Config.kt | 12 ++ .../libsession_util/util/GroupDisplayInfo.kt | 13 ++ .../libsession/database/StorageProtocol.kt | 4 +- 11 files changed, 260 insertions(+), 63 deletions(-) create mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupDisplayInfo.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9668b0d83..d0a6e63bc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -158,6 +158,7 @@ android:screenOrientation="portrait" /> ) { return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt index 702762b84..3ca341910 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/EditClosedGroupActivity.kt @@ -7,6 +7,7 @@ import com.ramcosta.composedestinations.navigation.dependency import dagger.hilt.android.AndroidEntryPoint import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.groups.compose.EditGroupViewModel +import org.thoughtcrime.securesms.groups.destinations.EditClosedGroupScreenDestination import org.thoughtcrime.securesms.ui.AppTheme import javax.inject.Inject @@ -19,8 +20,13 @@ class EditClosedGroupActivity: PassphraseRequiredActionBarActivity() { @Inject lateinit var factory: EditGroupViewModel.Factory + private fun onFinish() { + finish() + } + override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { setContent { + AppTheme { DestinationsNavHost( navGraph = NavGraphs.editGroup, @@ -28,6 +34,9 @@ class EditClosedGroupActivity: PassphraseRequiredActionBarActivity() { dependency(NavGraphs.editGroup) { factory.create(intent.getStringExtra(groupIDKey)!!) } + dependency(EditClosedGroupScreenDestination) { + ::onFinish + } } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt index c46d5b2e5..92a441c44 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/Components.kt @@ -77,11 +77,11 @@ fun LazyListScope.multiSelectMemberList( verticalAlignment = CenterVertically ) { ContactPhoto( - contact = contact, + contact.sessionID, modifier = Modifier .size(48.dp) ) - MemberName(name = contact.getSearchName()) + MemberName(name = contact.getSearchName(), modifier = Modifier.padding(16.dp)) RadioButton(selected = isSelected, onClick = update) } } @@ -89,13 +89,13 @@ fun LazyListScope.multiSelectMemberList( @Composable fun RowScope.MemberName( - name: String + name: String, + modifier: Modifier = Modifier ) = Text( text = name, fontWeight = FontWeight.Bold, - modifier = Modifier + modifier = modifier .weight(1f) - .padding(16.dp) .align(CenterVertically) ) @@ -120,12 +120,12 @@ fun LazyListScope.deleteMemberList( items(contacts) { contact -> Row(modifier.fillMaxWidth()) { ContactPhoto( - contact, + contact.sessionID, modifier = Modifier .size(48.dp) .align(CenterVertically) ) - MemberName(name = contact.getSearchName()) + MemberName(name = contact.getSearchName(), modifier = Modifier.padding(16.dp)) Image( painterResource(id = R.drawable.ic_baseline_close_24), null, @@ -143,7 +143,7 @@ fun LazyListScope.deleteMemberList( @Composable -fun RowScope.ContactPhoto(contact: Contact, modifier: Modifier = Modifier) { +fun RowScope.ContactPhoto(sessionId: String, modifier: Modifier = Modifier) { return if (LocalPreviewMode.current) { Image( painterResource(id = R.drawable.ic_profile_default), @@ -158,8 +158,8 @@ fun RowScope.ContactPhoto(contact: Contact, modifier: Modifier = Modifier) { } else { val context = LocalContext.current // Ideally we migrate to something that doesn't require recipient, or get contact photo another way - val recipient = remember(contact) { - Recipient.from(context, Address.fromSerialized(contact.sessionID), false) + val recipient = remember(sessionId) { + Recipient.from(context, Address.fromSerialized(sessionId), false) } Avatar(recipient) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroup.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroup.kt index 433bb5588..406ba8dfd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroup.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/CreateGroup.kt @@ -34,6 +34,7 @@ import network.loki.messenger.R import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin +import org.thoughtcrime.securesms.ui.CloseIcon import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.EditableAvatar import org.thoughtcrime.securesms.ui.NavigationBar @@ -69,7 +70,7 @@ fun CreateGroup( NavigationBar( title = stringResource(id = R.string.activity_create_group_title), onBack = onBack, - onClose = onClose + actionElement = { CloseIcon(onClose) } ) // Editable avatar (future chunk) EditableAvatar( diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt index 2cb5942d9..9f00a182a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/EditGroup.kt @@ -1,15 +1,29 @@ package org.thoughtcrime.securesms.groups.compose -import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Scaffold import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.cash.molecule.RecompositionMode.Immediate @@ -19,35 +33,108 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import network.loki.messenger.R +import network.loki.messenger.libsession_util.util.GroupMember import org.session.libsession.database.StorageProtocol +import org.thoughtcrime.securesms.ui.Divider +import org.thoughtcrime.securesms.ui.NavigationBar @EditGroupNavGraph(start = true) @Composable @Destination fun EditClosedGroupScreen( navigator: DestinationsNavigator, - viewModel: EditGroupViewModel + viewModel: EditGroupViewModel, + onFinish: () -> Unit ) { val group by viewModel.viewState.collectAsState() val viewState = group.viewState val eventSink = group.eventSink - when (viewState) { - is EditGroupViewState.Display -> { - Text( - text = viewState.text, - modifier = Modifier.fillMaxSize() - .clickable { - eventSink(Unit) - } - ) - } - else -> { + EditGroupView( + onBack = { + onFinish() + }, + viewState = viewState as EditGroupViewState.Group + ) +} + +@Composable +fun EditGroupView( + onBack: ()->Unit, + viewState: EditGroupViewState.Group, +) { + val scaffoldState = rememberScaffoldState() + + Scaffold( + scaffoldState = scaffoldState, + topBar = { + NavigationBar( + title = stringResource(id = R.string.activity_edit_closed_group_title), + onBack = onBack + ) { + Text( + text = stringResource(id = R.string.menu_done_button), + modifier = Modifier + .fillMaxWidth() + .padding(2.dp) + .align(Alignment.Center), + textAlign = TextAlign.Center, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + } + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + // Group name title + Text( + text = viewState.groupName, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + fontSize = 26.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + // members header + Divider() + Row( + modifier = Modifier + .fillMaxWidth() + .height(60.dp) + ) { + Text( + text = stringResource(id = R.string.activity_edit_closed_group_edit_members), + modifier = Modifier + .weight(1f) + .padding(horizontal = 24.dp) + .align(CenterVertically), + ) + // if admin add member outline button TODO + } + Divider() + LazyColumn(modifier = Modifier) { + + items(viewState.memberStateList) { member -> + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp)) { + ContactPhoto(member.memberSessionId) + Column(modifier = Modifier.weight(1f)) { + Row(modifier = Modifier.fillMaxSize()) { + MemberName(name = member.memberName ?: member.memberSessionId, modifier = Modifier.align(CenterVertically)) + } + } + } + } + + } } } - } @@ -58,19 +145,41 @@ class EditGroupViewModel @AssistedInject constructor( val viewState = viewModelScope.launchMolecule(Immediate) { - val closedGroup = remember { - storage.getLibSessionClosedGroup(groupSessionId) + val currentUserId = rememberSaveable { + storage.getUserPublicKey()!! } - var displayText by remember { - mutableStateOf(closedGroup!!.groupSessionId.hexString()) + val closedGroupInfo = remember { + storage.getLibSessionClosedGroup(groupSessionId)!! } - EditGroupState(EditGroupViewState.Display(displayText)) { event -> - when (event) { - Unit -> displayText = "different" + val closedGroup = remember(closedGroupInfo) { + storage.getClosedGroupDisplayInfo(groupSessionId)!! + } + + val closedGroupMembers = remember(closedGroupInfo) { + storage.getMembers(groupSessionId).map { member -> + MemberViewModel( + memberName = member.name, + memberSessionId = member.sessionId, + currentUser = member.sessionId == currentUserId, + memberState = memberStateOf(member) + ) } } + + val name = closedGroup.name + val description = closedGroup.description + + EditGroupState( + EditGroupViewState.Group( + groupName = name, + groupDescription = description, + memberStateList = closedGroupMembers, + ) + ) { event -> + + } } @AssistedFactory @@ -85,7 +194,37 @@ data class EditGroupState( val eventSink: (Unit)->Unit ) +data class MemberViewModel( + val memberName: String?, + val memberSessionId: String, + val memberState: MemberState, + val currentUser: Boolean, +) + +enum class MemberState { + InviteSent, + Inviting, // maybe just use these in view + InviteFailed, + PromotionSent, + Promoting, // maybe just use these in view + PromotionFailed, + Admin, + Member +} + +fun memberStateOf(member: GroupMember): MemberState = when { + member.invitePending -> MemberState.InviteSent + member.inviteFailed -> MemberState.InviteFailed + member.promotionPending -> MemberState.PromotionSent + member.promotionFailed -> MemberState.PromotionFailed + member.admin -> MemberState.Admin + else -> MemberState.Member +} + sealed class EditGroupViewState { - data object NoOp: EditGroupViewState() - data class Display(val text: String) : EditGroupViewState() + data class Group( + val groupName: String, + val groupDescription: String?, + val memberStateList: List, + ): EditGroupViewState() } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/SelectContacts.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/SelectContacts.kt index 46247218c..9653b3b0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/compose/SelectContacts.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/compose/SelectContacts.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp import network.loki.messenger.R import org.session.libsession.messaging.contacts.Contact import org.thoughtcrime.securesms.home.search.getSearchName +import org.thoughtcrime.securesms.ui.CloseIcon import org.thoughtcrime.securesms.ui.NavigationBar import org.thoughtcrime.securesms.ui.SearchBar @@ -54,7 +55,7 @@ fun SelectContacts( NavigationBar( title = stringResource(id = R.string.activity_create_closed_group_select_contacts), onBack = onBack, - onClose = onClose + actionElement = { CloseIcon(onClose) } ) LazyColumn(modifier = Modifier.weight(1f)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 1de4d62a9..e1d5c1e83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -5,11 +5,13 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -30,7 +32,9 @@ import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextButton +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -264,12 +268,10 @@ fun SearchBar( @Composable fun NavigationBar( - // title: String, titleAlignment: Alignment = Alignment.Center, - // if onBack: (() -> Unit)? = null, - onClose: (()->Unit)? = null, + actionElement: (@Composable BoxScope.() -> Unit)? = null ) { Row( Modifier @@ -278,6 +280,8 @@ fun NavigationBar( // Optional back button, layout should still take up space Box(modifier = Modifier .fillMaxHeight() + .aspectRatio(1.0f, true) + .padding(16.dp) ) { if (onBack != null) { Icon( @@ -286,8 +290,10 @@ fun NavigationBar( id = R.string.new_conversation_dialog_back_button_content_description ), Modifier - .clickable { onBack() } - .padding(16.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = 16.dp), + ) { onBack() } .align(Alignment.Center) ) } @@ -305,32 +311,41 @@ fun NavigationBar( fontWeight = FontWeight.Bold ) } - // Optional close button - Box(modifier = Modifier - .fillMaxHeight() - .align(Alignment.CenterVertically) - ) { - if (onClose != null) { - Icon( - painter = painterResource(id = R.drawable.ic_baseline_close_24), - contentDescription = stringResource( - id = R.string.new_conversation_dialog_close_button_content_description - ), - Modifier - .clickable { onClose() } - .padding(16.dp) - .align(Alignment.Center) - ) + // Optional action + if (actionElement != null) { + Box(modifier = Modifier + .fillMaxHeight() + .align(Alignment.CenterVertically) + .aspectRatio(1.0f, true), + contentAlignment = Alignment.Center + ) { + actionElement(this) } } } } +@Composable +fun BoxScope.CloseIcon(onClose: ()->Unit) { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_close_24), + contentDescription = stringResource( + id = R.string.new_conversation_dialog_close_button_content_description + ), + Modifier + .clickable { onClose() } + .align(Alignment.Center) + .padding(16.dp) + ) +} + @Composable @Preview fun PreviewNavigationBar(@PreviewParameter(provider = ThemeResPreviewParameterProvider::class) themeResId: Int) { PreviewTheme(themeResId = themeResId) { - NavigationBar(title = "Create Group", onBack = {}, onClose = {}) + NavigationBar(title = "Create Group", onBack = {}, actionElement = { + CloseIcon {} + }) } } diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt index 59ab227b1..d0c5ee6a5 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/Config.kt @@ -4,6 +4,7 @@ import network.loki.messenger.libsession_util.util.BaseCommunityInfo import network.loki.messenger.libsession_util.util.ConfigPush import network.loki.messenger.libsession_util.util.Contact import network.loki.messenger.libsession_util.util.Conversation +import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupInfo import network.loki.messenger.libsession_util.util.GroupMember import network.loki.messenger.libsession_util.util.UserPic @@ -262,6 +263,17 @@ class GroupInfoConfig(pointer: Long): ConfigBase(pointer), Closeable { external fun setDescription(newDescription: String) external fun setProfilePic(newProfilePic: UserPic) external fun storageNamespace(): Long + + fun displayInfo() = GroupDisplayInfo( + id = id(), + name = getName(), + description = null, + created = getCreated(), + destroyed = isDestroyed(), + expiryTimer = getExpiryTimer(), + profilePic = getProfilePic() + ) + override fun close() { free() } diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupDisplayInfo.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupDisplayInfo.kt new file mode 100644 index 000000000..033614f14 --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/GroupDisplayInfo.kt @@ -0,0 +1,13 @@ +package network.loki.messenger.libsession_util.util + +import org.session.libsignal.utilities.SessionId + +data class GroupDisplayInfo( + val id: SessionId, + val created: Long?, + val expiryTimer: Long?, + val name: String, + val description: String?, + val destroyed: Boolean, + val profilePic: UserPic +) \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 46c516732..b904f2309 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -3,7 +3,7 @@ package org.session.libsession.database import android.content.Context import android.net.Uri import network.loki.messenger.libsession_util.Config -import network.loki.messenger.libsession_util.util.Conversation +import network.loki.messenger.libsession_util.util.GroupDisplayInfo import network.loki.messenger.libsession_util.util.GroupInfo import org.session.libsession.messaging.BlindedIdMapping import org.session.libsession.messaging.calls.CallMessageType @@ -34,7 +34,6 @@ import org.session.libsession.utilities.recipients.Recipient.RecipientSettings import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceGroup -import org.session.libsignal.protos.SignalServiceProtos.ConfigurationMessage.ClosedGroup import org.session.libsignal.utilities.SessionId import org.session.libsignal.utilities.guava.Optional import network.loki.messenger.libsession_util.util.Contact as LibSessionContact @@ -165,6 +164,7 @@ interface StorageProtocol { fun acceptClosedGroupInvite(groupId: SessionId, name: String, authData: ByteArray, invitingAdmin: SessionId) fun setGroupInviteCompleteIfNeeded(approved: Boolean, invitee: String, closedGroup: SessionId) fun getLibSessionClosedGroup(groupSessionId: String): GroupInfo.ClosedGroupInfo? + fun getClosedGroupDisplayInfo(groupSessionId: String): GroupDisplayInfo? // Groups fun getAllGroups(includeInactive: Boolean): List