From 026f0056b9b9a23b517ff938112b2e394a4ff86e Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Thu, 7 Sep 2023 17:33:24 +1000 Subject: [PATCH] feat: creating the basic storage code to test creating group in configs and pushing --- .../securesms/database/Storage.kt | 25 +++ .../securesms/groups/CreateGroupFragment.kt | 209 ++++++++++-------- .../securesms/groups/CreateGroupViewModel.kt | 49 ++-- .../thoughtcrime/securesms/ui/Components.kt | 3 +- libsession-util/src/main/cpp/group_info.cpp | 17 ++ .../loki/messenger/libsession_util/Config.kt | 4 +- .../libsession/database/StorageProtocol.kt | 2 + 7 files changed, 195 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index a11916ea23..fe0152da74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -880,6 +880,31 @@ open class Storage(context: Context, helper: SQLCipherOpenHelper, private val co DatabaseComponent.get(context).groupDatabase().create(groupId, title, members, avatar, relay, admins, formationTimestamp) } + override fun createNewGroup(groupName: String, groupDescription: String, members: List): Long? { + val userGroups = configFactory.userGroups ?: return null + val ourSessionId = getUserPublicKey() ?: return null + + val group = userGroups.createGroup() + userGroups.set(group) + val groupInfo = configFactory.groupInfoConfig(group.groupSessionId) ?: return null + val groupMembers = configFactory.groupMemberConfig(group.groupSessionId) ?: return null + val groupKeys = configFactory.groupKeysConfig(group.groupSessionId) ?: return null + + with (groupInfo) { + setName(groupName) + setDescription(groupDescription) + } + + groupMembers.set( + LibSessionGroupMember(ourSessionId, "admin", admin = true) + ) + + // Test the sending + val userGroupsUpdate = + + TODO() + } + override fun createInitialConfigGroup(groupPublicKey: String, name: String, members: Map, formationTimestamp: Long, encryptionKeyPair: ECKeyPair) { val volatiles = configFactory.convoVolatile ?: return val userGroups = configFactory.userGroups ?: return diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt index 1001663231..acb4db100a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt @@ -6,47 +6,47 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast +import androidx.annotation.StringRes +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.FragmentCreateGroupBinding -import nl.komponents.kovenant.ui.failUi -import nl.komponents.kovenant.ui.successUi -import org.session.libsession.messaging.sending_receiving.MessageSender -import org.session.libsession.messaging.sending_receiving.groupSizeLimit -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.Contact import org.session.libsession.utilities.Device -import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.SessionId -import org.thoughtcrime.securesms.contacts.SelectContactsAdapter import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView -import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.ui.EditableAvatar import org.thoughtcrime.securesms.ui.NavigationBar import org.thoughtcrime.securesms.ui.PreviewTheme import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider -import org.thoughtcrime.securesms.util.fadeIn -import org.thoughtcrime.securesms.util.fadeOut import javax.inject.Inject @AndroidEntryPoint @@ -64,71 +64,13 @@ class CreateGroupFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + + val isLoading = viewModel.viewState. + return ComposeView(requireContext()).apply { setContent { - - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val adapter = SelectContactsAdapter(requireContext(), GlideApp.with(requireContext())) - binding.backButton.setOnClickListener { delegate.onDialogBackPressed() } - binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() } - binding.contactSearch.callbacks = object : KeyboardPageSearchView.Callbacks { - override fun onQueryChanged(query: String) { - adapter.members = viewModel.filter(query).map { it.address.serialize() } - } - } - binding.createNewPrivateChatButton.setOnClickListener { delegate.onNewMessageSelected() } - binding.recyclerView.adapter = adapter - val divider = ContextCompat.getDrawable(requireActivity(), R.drawable.conversation_menu_divider)!!.let { - DividerItemDecoration(requireActivity(), RecyclerView.VERTICAL).apply { - setDrawable(it) - } - } - binding.recyclerView.addItemDecoration(divider) - var isLoading = false - binding.createClosedGroupButton.setOnClickListener { - if (isLoading) return@setOnClickListener - val name = binding.nameEditText.text.trim() - if (name.isEmpty()) { - return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show() - } - if (name.length >= 30) { - return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show() - } - val selectedMembers = adapter.selectedMembers - if (selectedMembers.isEmpty()) { - return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() + CreateGroupScreen(createGroupState = CreateGroupState("", "", emptySet())) } - if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later - return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() - } - val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!! - isLoading = true - binding.loaderContainer.fadeIn() - MessageSender.createClosedGroup(device, name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID -> - binding.loaderContainer.fadeOut() - isLoading = false - val threadID = DatabaseComponent.get(requireContext()).threadDatabase().getOrCreateThreadIdFor(Recipient.from(requireContext(), Address.fromSerialized(groupID), false)) - openConversationActivity( - requireContext(), - threadID, - Recipient.from(requireContext(), Address.fromSerialized(groupID), false) - ) - delegate.onDialogClosePressed() - }.failUi { - binding.loaderContainer.fadeOut() - isLoading = false - Toast.makeText(context, it.message, Toast.LENGTH_LONG).show() - } - } - binding.mainContentGroup.isVisible = !viewModel.recipients.value.isNullOrEmpty() - binding.emptyStateGroup.isVisible = viewModel.recipients.value.isNullOrEmpty() - viewModel.recipients.observe(viewLifecycleOwner) { recipients -> - adapter.members = recipients.map { it.address.serialize() } } } @@ -144,17 +86,23 @@ class CreateGroupFragment : Fragment() { CreateGroup( createGroupState, onCreate = { - + // launch something to create here }, onClose = { - + delegate.onDialogClosePressed() }, onBack = { - + delegate.onDialogBackPressed() } ) } + data class ViewState( + val isLoading: Boolean, + @StringRes val error: Int?, + val createdThreadId: Long? + ) + } data class CreateGroupState ( @@ -166,29 +114,93 @@ data class CreateGroupState ( @Composable fun CreateGroup( createGroupState: CreateGroupState, + onCreate: (CreateGroupState) -> Unit, onBack: () -> Unit, onClose: () -> Unit, - onCreate: suspend (CreateGroupState) -> Unit, modifier: Modifier = Modifier) { + + var name by remember { mutableStateOf(createGroupState.groupName) } + var description by remember { mutableStateOf(createGroupState.groupDescription) } + val members by remember { mutableStateOf(createGroupState.members) } + + val scrollState = rememberScrollState() + Column( modifier .fillMaxWidth()) { - NavigationBar( - title = stringResource(id = R.string.activity_create_group_title), - onBack = { - onBack() - }, - onClose = { - onClose() - } - ) + Column(modifier.scrollable(scrollState, orientation = Orientation.Vertical)) { + // Top bar + NavigationBar( + title = stringResource(id = R.string.activity_create_group_title), + onBack = onBack, + onClose = onClose + ) + // Editable avatar (future chunk) + EditableAvatar( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(top = 16.dp) + ) + // Title + OutlinedTextField( + value = name, + onValueChange = { name = it }, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .padding(vertical = 8.dp, horizontal = 24.dp), + ) + // Description + OutlinedTextField( + value = description, + onValueChange = { description = it }, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally) + .padding(vertical = 8.dp, horizontal = 24.dp), + ) + // Group list + MemberList(contacts = members, modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp)) + } + // Create button + OutlinedButton( + onClick = { onCreate(CreateGroupState(name, description, members)) }, + enabled = name.isNotBlank(), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(16.dp), + shape = RoundedCornerShape(32.dp) + ) { + Text( + text = stringResource(id = R.string.activity_create_group_create_button_title), + // TODO: colours of everything here probably needs to be redone + color = MaterialTheme.colors.onBackground, + modifier = Modifier.width(160.dp), + textAlign = TextAlign.Center + ) + } } } @Composable -fun MemberList(contacts: List, modifier: Modifier = Modifier) { - +fun MemberList(contacts: Collection, modifier: Modifier = Modifier) { + Column(modifier = modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 240.dp)) { + Text(text = stringResource(id = R.string.conversation_settings_group_members), + modifier = Modifier + .align(Alignment.Start) + .padding(vertical = 8.dp) + ) + // TODO group list representation + Text( + text = stringResource(id = R.string.activity_create_closed_group_not_enough_group_members_error), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(vertical = 8.dp) + ) + } } @Preview @@ -197,6 +209,11 @@ fun ClosedGroupPreview( @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int ) { PreviewTheme(themeResId) { - CreateGroup(CreateGroupState("Group Name", "Test Group Description", emptySet()), {}) + CreateGroup( + CreateGroupState("Group Name", "Test Group Description", emptySet()), + onCreate = {}, + onClose = {}, + onBack = {}, + ) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt index b3dbb49384..c174a526ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupViewModel.kt @@ -5,37 +5,54 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient -import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.Storage import javax.inject.Inject @HiltViewModel class CreateGroupViewModel @Inject constructor( - private val threadDb: ThreadDatabase, - private val textSecurePreferences: TextSecurePreferences + private val textSecurePreferences: TextSecurePreferences, + private val storage: Storage, ) : ViewModel() { private val _recipients = MutableLiveData>() val recipients: LiveData> = _recipients + private val _viewState = MutableLiveData(CreateGroupFragment.ViewState(false, null, null)) + val viewState: LiveData = _viewState + init { viewModelScope.launch { - threadDb.approvedConversationList.use { openCursor -> - val reader = threadDb.readerFor(openCursor) - val recipients = mutableListOf() - while (true) { - recipients += reader.next?.recipient ?: break - } - withContext(Dispatchers.Main) { - _recipients.value = recipients - .filter { !it.isGroupRecipient && it.hasApprovedMe() && it.address.serialize() != textSecurePreferences.getLocalNumber() } - } - } +// threadDb.approvedConversationList.use { openCursor -> +// val reader = threadDb.readerFor(openCursor) +// val recipients = mutableListOf() +// while (true) { +// recipients += reader.next?.recipient ?: break +// } +// withContext(Dispatchers.Main) { +// _recipients.value = recipients +// .filter { !it.isGroupRecipient && it.hasApprovedMe() && it.address.serialize() != textSecurePreferences.getLocalNumber() } +// } +// } + } + } + + fun tryCreateGroup(createGroupState: CreateGroupState) { + _viewState.postValue(CreateGroupFragment.ViewState(true, null, null)) + + // do some validations + if (createGroupState.groupName.isEmpty()) { + return _viewState.postValue( + CreateGroupFragment.ViewState(false, R.string.error, null) + ) } + // TODO: add future validation for empty group ? we'll add ourselves anyway ig + + // make a group + storage.createGroup() } fun filter(query: String): List { 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 20bea146de..0f6b184cfc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -197,8 +197,9 @@ fun RowScope.Avatar(recipient: Recipient) { @Composable fun EditableAvatar( // TODO: add attachment-based state for current view rendering? + modifier: Modifier = Modifier ) { - Box(modifier = Modifier + Box(modifier = modifier .size(110.dp) .padding(15.dp) ) { diff --git a/libsession-util/src/main/cpp/group_info.cpp b/libsession-util/src/main/cpp/group_info.cpp index e72fee45c7..3ff3eb29ab 100644 --- a/libsession-util/src/main/cpp/group_info.cpp +++ b/libsession-util/src/main/cpp/group_info.cpp @@ -184,4 +184,21 @@ Java_network_loki_messenger_libsession_1util_GroupInfoConfig_id(JNIEnv *env, job std::lock_guard guard{util::util_mutex_}; auto group_info = ptrToInfo(env, thiz); return util::serialize_session_id(env, group_info->id); +} + +extern "C" +JNIEXPORT jstring JNICALL +Java_network_loki_messenger_libsession_1util_GroupInfoConfig_getDescription(JNIEnv *env, + jobject thiz) { + std::lock_guard guard{util::util_mutex_}; + auto group_info = ptrToInfo(env, thiz); +} + +extern "C" +JNIEXPORT void JNICALL +Java_network_loki_messenger_libsession_1util_GroupInfoConfig_setDescription(JNIEnv *env, + jobject thiz, + jstring new_description) { + std::lock_guard guard{util::util_mutex_}; + auto group_info = ptrToInfo(env, thiz); } \ No newline at end of file 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 a36c170615..1eb00e7853 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 @@ -237,7 +237,7 @@ class GroupInfoConfig(pointer: Long): ConfigBase(pointer), Closeable { external fun getDeleteAttachmentsBefore(): Long? external fun getDeleteBefore(): Long? external fun getExpiryTimer(): Long? // TODO: maybe refactor this to new type when disappearing messages merged - external fun getName(): String? + external fun getName(): String external fun getProfilePic(): UserPic external fun isDestroyed(): Boolean external fun setCreated(createdAt: Long) @@ -245,6 +245,8 @@ class GroupInfoConfig(pointer: Long): ConfigBase(pointer), Closeable { external fun setDeleteBefore(deleteBefore: Long) external fun setExpiryTimer(expireSeconds: Long) external fun setName(newName: String) + external fun getDescription(): String + external fun setDescription(newDescription: String) external fun setProfilePic(newProfilePic: UserPic) external fun storageNamespace(): Long override fun close() { 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 016686bda1..2494821cff 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -32,6 +32,7 @@ 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.utilities.SessionId import network.loki.messenger.libsession_util.util.Contact as LibSessionContact interface StorageProtocol { @@ -154,6 +155,7 @@ interface StorageProtocol { fun setExpirationTimer(address: String, duration: Int) // Closed Groups + fun createNewGroup(groupName: String, groupDescription: String, members: List): Long? fun getMembers(groupPublicKey: String): List // Groups