mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-31 01:56:18 +00:00
feat: creating the basic storage code to test creating group in configs and pushing
This commit is contained in:
@@ -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<SessionId>): 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<String, Boolean>, formationTimestamp: Long, encryptionKeyPair: ECKeyPair) {
|
||||
val volatiles = configFactory.convoVolatile ?: return
|
||||
val userGroups = configFactory.userGroups ?: return
|
||||
|
||||
@@ -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,74 +64,16 @@ class CreateGroupFragment : Fragment() {
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
|
||||
val isLoading = viewModel.viewState.
|
||||
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
|
||||
CreateGroupScreen(createGroupState = CreateGroupState("", "", emptySet()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
|
||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
|
||||
@@ -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<Contact>, modifier: Modifier = Modifier) {
|
||||
|
||||
fun MemberList(contacts: Collection<SessionId>, 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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,39 +5,56 @@ 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<List<Recipient>>()
|
||||
val recipients: LiveData<List<Recipient>> = _recipients
|
||||
|
||||
private val _viewState = MutableLiveData(CreateGroupFragment.ViewState(false, null, null))
|
||||
val viewState: LiveData<CreateGroupFragment.ViewState> = _viewState
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
threadDb.approvedConversationList.use { openCursor ->
|
||||
val reader = threadDb.readerFor(openCursor)
|
||||
val recipients = mutableListOf<Recipient>()
|
||||
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<Recipient>()
|
||||
// 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<Recipient> {
|
||||
return _recipients.value?.filter {
|
||||
it.address.serialize().contains(query, ignoreCase = true) || it.name?.contains(query, ignoreCase = true) == true
|
||||
|
||||
@@ -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)
|
||||
) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<SessionId>): Long?
|
||||
fun getMembers(groupPublicKey: String): List<network.loki.messenger.libsession_util.util.GroupMember>
|
||||
|
||||
// Groups
|
||||
|
||||
Reference in New Issue
Block a user