From f9939aae927e45871bc2ecb84d94637d5eac621d Mon Sep 17 00:00:00 2001 From: jubb Date: Wed, 21 Apr 2021 17:00:57 +1000 Subject: [PATCH] feat: handling default group requests and open group api updates for proper image endpoint handling --- .../conversation/ConversationFragment.java | 42 +++++++----- .../loki/activities/JoinPublicChatActivity.kt | 48 +++++++++++-- .../securesms/loki/api/PublicChatManager.kt | 2 +- .../loki/database/LokiThreadDatabase.kt | 39 ++++++++--- .../loki/utilities/OpenGroupUtilities.kt | 13 ++++ .../loki/viewmodel/DefaultGroupsViewModel.kt | 41 ++++-------- .../securesms/loki/viewmodel/State.kt | 2 +- .../fragment_enter_chat_url.xml | 67 ++++++++----------- .../main/res/layout/default_group_chip.xml | 15 +++++ .../res/layout/fragment_enter_chat_url.xml | 38 ++++++++--- .../main/res/layout/grid_layout_filler.xml | 5 ++ app/src/main/res/values/strings.xml | 1 + .../messaging/opengroups/OpenGroupAPIV2.kt | 41 ++++++++++-- 13 files changed, 237 insertions(+), 117 deletions(-) create mode 100644 app/src/main/res/layout/default_group_chip.xml create mode 100644 app/src/main/res/layout/grid_layout_filler.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 0d30b8ce97..ed8f644bc2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -57,48 +57,49 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener; import com.annimon.stream.Stream; +import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; +import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; import org.session.libsession.messaging.messages.visible.Quote; import org.session.libsession.messaging.messages.visible.VisibleMessage; import org.session.libsession.messaging.opengroups.OpenGroupAPI; +import org.session.libsession.messaging.opengroups.OpenGroupAPIV2; +import org.session.libsession.messaging.opengroups.OpenGroupV2; +import org.session.libsession.messaging.sending_receiving.MessageSender; +import org.session.libsession.messaging.sending_receiving.attachments.Attachment; +import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; +import org.session.libsession.messaging.threads.Address; +import org.session.libsession.messaging.threads.recipients.Recipient; +import org.session.libsession.utilities.TextSecurePreferences; +import org.session.libsession.utilities.Util; +import org.session.libsession.utilities.ViewUtil; +import org.session.libsession.utilities.concurrent.SimpleTask; +import org.session.libsession.utilities.task.ProgressDialogAsyncTask; +import org.session.libsignal.libsignal.util.guava.Optional; +import org.session.libsignal.service.loki.api.opengroups.PublicChat; +import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.MessageDetailsActivity; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; import org.thoughtcrime.securesms.ShareActivity; -import org.session.libsession.messaging.sending_receiving.attachments.Attachment; import org.thoughtcrime.securesms.components.ConversationTypingView; import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager; import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder; import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener; -import org.session.libsession.messaging.threads.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.loaders.ConversationLoader; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; -import org.session.libsignal.utilities.logging.Log; import org.thoughtcrime.securesms.longmessage.LongMessageActivity; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.GlideApp; -import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.permissions.Permissions; -import org.session.libsession.messaging.threads.recipients.Recipient; -import org.session.libsession.messaging.sending_receiving.MessageSender; -import org.session.libsession.messaging.messages.signal.OutgoingTextMessage; -import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; -import org.session.libsession.utilities.task.ProgressDialogAsyncTask; -import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.service.loki.api.opengroups.PublicChat; - -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.ViewUtil; -import org.session.libsession.utilities.concurrent.SimpleTask; import java.io.IOException; import java.io.InputStream; @@ -396,7 +397,8 @@ public class ConversationFragment extends Fragment if (isGroupChat) { PublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); - boolean isPublicChat = (publicChat != null); + OpenGroupV2 openGroupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getOpenGroupChat(threadId); + boolean isPublicChat = (publicChat != null || openGroupChat != null); int selectedMessageCount = messageRecords.size(); boolean areAllSentByUser = true; Set uniqueUserSet = new HashSet<>(); @@ -407,7 +409,11 @@ public class ConversationFragment extends Fragment menu.findItem(R.id.menu_context_copy_public_key).setVisible(selectedMessageCount == 1 && !areAllSentByUser); menu.findItem(R.id.menu_context_reply).setVisible(selectedMessageCount == 1); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); - boolean userCanModerate = isPublicChat && OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer()); + boolean userCanModerate = + (isPublicChat && + (OpenGroupAPI.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer()) + || OpenGroupAPIV2.isUserModerator(userHexEncodedPublicKey, openGroupChat.getRoom(), openGroupChat.getServer())) + ); boolean isDeleteOptionVisible = !isPublicChat || (areAllSentByUser || userCanModerate); // allow banning if moderating a public chat and only one user's messages are selected boolean isBanOptionVisible = isPublicChat && userCanModerate && !areAllSentByUser && uniqueUserSet.size() == 1; diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index 51e6816d73..f88765d223 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.activities import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.graphics.BitmapFactory import android.os.Bundle import android.util.Patterns import android.view.LayoutInflater @@ -9,14 +10,20 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.activity.viewModels +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory +import androidx.core.view.isVisible import androidx.fragment.app.* import androidx.lifecycle.lifecycleScope +import com.google.android.material.chip.Chip import kotlinx.android.synthetic.main.activity_join_public_chat.* import kotlinx.android.synthetic.main.fragment_enter_chat_url.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import network.loki.messenger.R +import okhttp3.HttpUrl +import org.session.libsession.messaging.opengroups.OpenGroupAPIV2.DefaultGroup import org.session.libsignal.utilities.logging.Log import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity @@ -24,11 +31,13 @@ import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities -import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroup import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel import org.thoughtcrime.securesms.loki.viewmodel.State class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { + + private val viewModel by viewModels() + private val adapter = JoinPublicChatActivityAdapter(this) // region Lifecycle @@ -70,12 +79,24 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode if (!Patterns.WEB_URL.matcher(url).matches() || !url.startsWith("https://")) { return Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show() } + + val properString = if (!url.startsWith("http")) "http://$url" else url + val httpUrl = HttpUrl.parse(url) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show() + + val room = httpUrl.pathSegments().firstOrNull() + val publicKey = httpUrl.queryParameter("public_key") + val isV2OpenGroup = !room.isNullOrEmpty() showLoader() val channel: Long = 1 lifecycleScope.launch(Dispatchers.IO) { try { - OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel) + if (isV2OpenGroup) { + val server = httpUrl.newBuilder().removeAllQueryParameters("public_key").removePathSegment(0).build().toString() + OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server, room, publicKey) + } else { + OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel) + } MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity) } catch (e: Exception) { Log.e("JoinPublicChatActivity", "Fialed to join open group.", e) @@ -111,7 +132,7 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity } } - override fun getPageTitle(index: Int): CharSequence? { + override fun getPageTitle(index: Int): CharSequence { return when (index) { 0 -> activity.resources.getString(R.string.activity_join_public_chat_enter_group_url_tab_title) 1 -> activity.resources.getString(R.string.activity_join_public_chat_scan_qr_code_tab_title) @@ -124,7 +145,6 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity // region Enter Chat URL Fragment class EnterChatURLFragment : Fragment() { - // factory producer is app scoped because default groups will want to stick around for app lifetime private val viewModel by activityViewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -132,7 +152,23 @@ class EnterChatURLFragment : Fragment() { } private fun populateDefaultGroups(groups: List) { - Log.d("Loki", "Got some default groups $groups") + defaultRoomsGridLayout.removeAllViews() + groups.forEach { defaultGroup -> + val chip = layoutInflater.inflate(R.layout.default_group_chip,defaultRoomsGridLayout, false) as Chip + val drawable = defaultGroup.image?.let { bytes -> + val bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.size) + RoundedBitmapDrawableFactory.create(resources,bitmap).apply { + isCircular = true + } + } + chip.chipIcon = drawable + chip.text = defaultGroup.name + defaultRoomsGridLayout.addView(chip) + } + if (groups.size and 1 != 0) { + // add a filler weight 1 view + layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout) + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -140,6 +176,8 @@ class EnterChatURLFragment : Fragment() { chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } viewModel.defaultRooms.observe(viewLifecycleOwner) { state -> + defaultRoomsParent.isVisible = state is State.Success + defaultRoomsLoader.isVisible = state is State.Loading when (state) { State.Loading -> { // show a loader here probs diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt index de45a92075..bf8e82bc43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/api/PublicChatManager.kt @@ -123,7 +123,7 @@ class PublicChatManager(private val context: Context) { if (threadID < 0) { val imageID = info.imageID if (!imageID.isNullOrEmpty()) { - val profilePictureAsByteArray = OpenGroupAPIV2.downloadOpenGroupProfilePicture(imageID) + val profilePictureAsByteArray = OpenGroupAPIV2.downloadOpenGroupProfilePicture(info.id,server) profilePicture = BitmapUtil.fromByteArray(profilePictureAsByteArray) } val result = GroupManager.createOpenGroup(chat.id, context, profilePicture, info.name) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt index 60057f606d..189e0a5471 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/LokiThreadDatabase.kt @@ -25,8 +25,10 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa private val friendRequestStatus = "friend_request_status" private val sessionResetStatus = "session_reset_status" val publicChat = "public_chat" - @JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTable ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);" - @JvmStatic val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);" + @JvmStatic + val createSessionResetTableCommand = "CREATE TABLE $sessionResetTable ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);" + @JvmStatic + val createPublicChatTableCommand = "CREATE TABLE $publicChatTable ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);" } override fun getThreadID(hexEncodedPublicKey: String): Long { @@ -45,11 +47,13 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa val threadID = cursor.getLong(threadID) val string = cursor.getString(publicChat) val publicChat = PublicChat.fromJSON(string) - if (publicChat != null) { result[threadID] = publicChat } + if (publicChat != null) { + result[threadID] = publicChat + } } } catch (e: Exception) { // Do nothing - } finally { + } finally { cursor?.close() } return result @@ -79,25 +83,40 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) } } - override fun getPublicChat(threadID: Long): PublicChat? { - if (threadID < 0) { return null } + fun getOpenGroupChat(threadID: Long): OpenGroupV2? { + if (threadID < 0) { + return null + } val database = databaseHelper.readableDatabase - return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor -> + return database.get(publicChat, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor -> + val json = cursor.getString(publicChat) + OpenGroupV2.fromJson(json) + } + } + + override fun getPublicChat(threadID: Long): PublicChat? { + if (threadID < 0) { + return null + } + val database = databaseHelper.readableDatabase + return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor -> val publicChatAsJSON = cursor.getString(publicChat) PublicChat.fromJSON(publicChatAsJSON) } } override fun setPublicChat(publicChat: PublicChat, threadID: Long) { - if (threadID < 0) { return } + if (threadID < 0) { + return + } val database = databaseHelper.writableDatabase val contentValues = ContentValues(2) contentValues.put(Companion.threadID, threadID) contentValues.put(Companion.publicChat, JsonUtil.toJson(publicChat.toJSON())) - database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) + database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString())) } override fun removePublicChat(threadID: Long) { - databaseHelper.writableDatabase.delete(publicChatTable, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) + databaseHelper.writableDatabase.delete(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt index c6b789f996..d38624b7b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/OpenGroupUtilities.kt @@ -5,6 +5,7 @@ import androidx.annotation.WorkerThread import org.greenrobot.eventbus.EventBus import org.session.libsession.messaging.opengroups.OpenGroup import org.session.libsession.messaging.opengroups.OpenGroupAPI +import org.session.libsession.messaging.opengroups.OpenGroupV2 import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.preferences.ProfileKeyUtil @@ -18,6 +19,18 @@ object OpenGroupUtilities { private const val TAG = "OpenGroupUtilities" + @JvmStatic + @WorkerThread + fun addGroup(context: Context, server: String, room: String, publicKey: String?): OpenGroupV2 { + val groupId = "$server.$room" + val threadID = GroupManager.getOpenGroupThreadID(groupId, context) + val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) + if (openGroup != null) return openGroup + + val application = ApplicationContext.getInstance(context) + val group = application.publicChatManager.addChat(server, room, publicKey) + } + @JvmStatic @WorkerThread @Throws(Exception::class) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt index 5bb564e72f..b68238eb73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt @@ -1,10 +1,13 @@ package org.thoughtcrime.securesms.loki.viewmodel -import androidx.lifecycle.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import org.session.libsession.messaging.opengroups.OpenGroupAPIV2 -import org.session.libsignal.utilities.logging.Log + +typealias DefaultGroups = List +typealias GroupState = State class DefaultGroupsViewModel : ViewModel() { @@ -12,28 +15,10 @@ class DefaultGroupsViewModel : ViewModel() { OpenGroupAPIV2.getDefaultRoomsIfNeeded() } - val defaultRooms = OpenGroupAPIV2.defaultRooms.asLiveData().distinctUntilChanged().switchMap { groups -> - liveData { - // load images etc - emit(State.Loading) - val images = groups.filterNot { it.imageID.isNullOrEmpty() }.map { group -> - val image = viewModelScope.async(Dispatchers.IO) { - try { - OpenGroupAPIV2.downloadOpenGroupProfilePicture(group.imageID!!) - } catch (e: Exception) { - Log.e("Loki", "Error getting group profile picture", e) - null - } - } - group.id to image - }.toMap() - val defaultGroups = groups.map { group -> - DefaultGroup(group.id, group.name, images[group.id]?.await()) - } - emit(State.Success(defaultGroups)) - } - } + val defaultRooms = OpenGroupAPIV2.defaultRooms.map { + State.Success(it) + }.onStart { + emit(State.Loading) + }.asLiveData() -} - -data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt index 88223b9099..94227d0e0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/State.kt @@ -1,6 +1,6 @@ package org.thoughtcrime.securesms.loki.viewmodel -sealed class State { +sealed class State { object Loading : State() data class Success(val value: T): State() data class Error(val error: Exception): State() diff --git a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml index b56e3e110d..a4b088aac1 100644 --- a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml @@ -1,6 +1,5 @@ + + + + + + + - - - - - -