feat: adding edit contact list views and presenters and various storage functions to build UI state

This commit is contained in:
0x330a 2023-11-09 18:01:24 +11:00
parent 8e1e228946
commit 35f4643c5c
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
11 changed files with 260 additions and 63 deletions

View File

@ -158,6 +158,7 @@
android:screenOrientation="portrait" />
<activity
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
android:theme="@style/Theme.Session.DayNight.NoActionBar"
android:label="@string/activity_edit_closed_group_title"
android:screenOrientation="portrait" />
<activity

View File

@ -16,6 +16,7 @@ import network.loki.messenger.libsession_util.UserProfile
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.Conversation
import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.GroupDisplayInfo
import network.loki.messenger.libsession_util.util.GroupInfo
import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.avatars.AvatarHelper
@ -1251,8 +1252,13 @@ open class Storage(
}
}
override fun getLibSessionClosedGroup(groupSessionId: String) =
configFactory.userGroups?.getClosedGroup(groupSessionId)
override fun getLibSessionClosedGroup(groupSessionId: String): GroupInfo.ClosedGroupInfo? {
return configFactory.userGroups?.getClosedGroup(groupSessionId)
}
override fun getClosedGroupDisplayInfo(groupSessionId: String): GroupDisplayInfo? {
return configFactory.getGroupInfoConfig(SessionId.from(groupSessionId))?.use(GroupInfoConfig::displayInfo)
}
override fun setServerCapabilities(server: String, capabilities: List<String>) {
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)

View File

@ -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
}
}
)
}

View File

@ -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)
}

View File

@ -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(

View File

@ -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<MemberViewModel>,
): EditGroupViewState()
}

View File

@ -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)) {

View File

@ -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 {}
})
}
}

View File

@ -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()
}

View File

@ -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
)

View File

@ -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<GroupRecord>