From 8b92932b6dec8ad3da6353c65367e9ac1276e502 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 10 Oct 2019 10:53:17 +1100 Subject: [PATCH 01/10] Added database functionality. --- .../database/helpers/SQLCipherOpenHelper.java | 4 +++ .../securesms/loki/LokiGroupChatPoller.kt | 10 +++---- .../securesms/loki/LokiThreadDatabase.kt | 27 ++++++++++++++++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 51d45e6c2d..efbe89544a 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -131,6 +131,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiMessageDatabase.getCreateTableCommand()); db.execSQL(LokiThreadDatabase.getCreateFriendRequestTableCommand()); db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand()); + db.execSQL(LokiThreadDatabase.getCreateGroupChatMappingTableCommand()); db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); @@ -497,6 +498,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { if (oldVersion < lokiV3) { db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand()); + db.execSQL(LokiThreadDatabase.getCreateGroupChatMappingTableCommand()); + + // TODO: Map old public chat threads to new manager format } db.setTransactionSuccessful(); diff --git a/src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt index 294a9249e8..0d4435746a 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt @@ -161,7 +161,7 @@ class LokiGroupChatPoller(private val context: Context, private val group: LokiG finalize() } } - api.getMessages(group.serverID, group.server).success { messages -> + api.getMessages(group.channel, group.server).success { messages -> messages.forEach { message -> if (message.hexEncodedPublicKey != userHexEncodedPublicKey) { processIncomingMessage(message) @@ -170,12 +170,12 @@ class LokiGroupChatPoller(private val context: Context, private val group: LokiG } } }.fail { - Log.d("Loki", "Failed to get messages for group chat with ID: ${group.serverID} on server: ${group.server}.") + Log.d("Loki", "Failed to get messages for group chat with ID: ${group.channel} on server: ${group.server}.") } } private fun pollForDeletedMessages() { - api.getDeletedMessageServerIDs(group.serverID, group.server).success { deletedMessageServerIDs -> + api.getDeletedMessageServerIDs(group.channel, group.server).success { deletedMessageServerIDs -> val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context) val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { lokiMessageDatabase.getMessageID(it) } val smsMessageDatabase = DatabaseFactory.getSmsDatabase(context) @@ -185,12 +185,12 @@ class LokiGroupChatPoller(private val context: Context, private val group: LokiG mmsMessageDatabase.delete(it) } }.fail { - Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${group.serverID} on server: ${group.server}.") + Log.d("Loki", "Failed to get deleted messages for group chat with ID: ${group.channel} on server: ${group.server}.") } } private fun pollForModerators() { - api.getModerators(group.serverID, group.server) + api.getModerators(group.channel, group.server) } // endregion } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt index b291bdad10..1d1158caa3 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt @@ -7,6 +7,8 @@ import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.recipients.Recipient +import org.whispersystems.signalservice.internal.util.JsonUtil +import org.whispersystems.signalservice.loki.api.LokiGroupChat import org.whispersystems.signalservice.loki.messaging.LokiThreadDatabaseProtocol import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus @@ -17,11 +19,14 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa companion object { private val friendRequestTableName = "loki_thread_friend_request_database" private val sessionResetTableName = "loki_thread_session_reset_database" + private val groupChatMappingTableName = "loki_group_chat_mapping_database" private val threadID = "thread_id" private val friendRequestStatus = "friend_request_status" private val sessionResetStatus = "session_reset_status" + private val groupChatJSON = "group_chat_json" @JvmStatic val createFriendRequestTableCommand = "CREATE TABLE $friendRequestTableName ($threadID INTEGER PRIMARY KEY, $friendRequestStatus INTEGER DEFAULT 0);" @JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTableName ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);" + @JvmStatic val createGroupChatMappingTableCommand = "CREATE TABLE $groupChatMappingTableName ($threadID INTEGER PRIMARY KEY, $groupChatJSON TEXT);" } override fun getThreadID(hexEncodedPublicKey: String): Long { @@ -30,7 +35,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) } - override fun getThreadID(messageID: Long): Long { + fun getThreadID(messageID: Long): Long { return DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID) } @@ -84,4 +89,24 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa notifyConversationListListeners() notifyConversationListeners(threadID) } + + override fun getGroupChat(threadID: Long): LokiGroupChat? { + val database = databaseHelper.readableDatabase + return database.get(groupChatMappingTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor -> + val string = cursor.getString(groupChatJSON) + LokiGroupChat.fromJSON(string) + } + } + + override fun setGroupChat(groupChat: LokiGroupChat, threadID: Long) { + val database = databaseHelper.writableDatabase + val contentValues = ContentValues(2) + contentValues.put(Companion.threadID, threadID) + contentValues.put(Companion.groupChatJSON, JsonUtil.toJson(groupChat.toJSON())) + database.insertOrUpdate(groupChatMappingTableName, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) + } + + override fun removeGroupChat(threadID: Long) { + databaseHelper.writableDatabase.delete(groupChatMappingTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) + } } \ No newline at end of file From 13d42f542cc0c39bf901fe5294974902cf582981 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Thu, 10 Oct 2019 11:38:43 +1100 Subject: [PATCH 02/10] Added public chat manager. Replace hard coded public chat server with dynamic one. --- .../securesms/ApplicationContext.java | 69 ++++++----- .../securesms/CreateProfileActivity.java | 16 ++- .../securesms/components/QuoteView.java | 14 +-- .../conversation/ConversationFragment.java | 30 ++--- .../conversation/ConversationItem.java | 14 ++- .../database/helpers/SQLCipherOpenHelper.java | 4 +- .../securesms/jobs/PushGroupSendJob.java | 11 +- .../securesms/loki/DatabaseUtilities.kt | 4 + .../securesms/loki/DisplayNameActivity.kt | 11 +- .../securesms/loki/GeneralUtilities.kt | 4 +- .../securesms/loki/LokiAPIDatabase.kt | 12 ++ .../securesms/loki/LokiPublicChatManager.kt | 115 ++++++++++++++++++ .../securesms/loki/LokiThreadDatabase.kt | 38 +++++- .../securesms/sms/MessageSender.java | 4 +- 14 files changed, 267 insertions(+), 79 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index e2bde579ab..17fd512b39 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -25,6 +25,7 @@ import android.database.ContentObserver; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.multidex.MultiDexApplication; import com.crashlytics.android.Crashlytics; @@ -61,8 +62,9 @@ import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.loki.BackgroundPollWorker; import org.thoughtcrime.securesms.loki.LokiAPIDatabase; -import org.thoughtcrime.securesms.loki.LokiGroupChatPoller; +import org.thoughtcrime.securesms.loki.LokiPublicChatManager; import org.thoughtcrime.securesms.loki.LokiRSSFeedPoller; +import org.thoughtcrime.securesms.loki.LokiUserDatabase; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.providers.BlobProvider; @@ -98,6 +100,7 @@ import java.security.Security; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -132,9 +135,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc // Loki private LokiLongPoller lokiLongPoller = null; - private LokiGroupChatPoller lokiPublicChatPoller = null; private LokiRSSFeedPoller lokiNewsFeedPoller = null; private LokiRSSFeedPoller lokiMessengerUpdatesFeedPoller = null; + private LokiPublicChatManager lokiPublicChatManager = null; + private LokiGroupChatAPI lokiGroupChatAPI = null; public SignalCommunicationModule communicationModule; public MixpanelAPI mixpanel; @@ -147,7 +151,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc @Override public void onCreate() { super.onCreate(); - LokiGroupChatAPI.Companion.setDebugMode(BuildConfig.DEBUG); // Loki - Set debug mode if needed startKovenant(); Log.i(TAG, "onCreate()"); initializeSecurityProvider(); @@ -183,6 +186,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc mixpanel.trackMap(event, properties); return Unit.INSTANCE; }; + + lokiPublicChatManager = new LokiPublicChatManager(this); } @Override @@ -204,6 +209,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc MessageNotifier.setVisibleThread(-1); // Loki - Stop long polling if needed if (lokiLongPoller != null) { lokiLongPoller.stopIfNeeded(); } + if (lokiPublicChatManager != null) { lokiPublicChatManager.stopPollers(); } } @Override @@ -243,6 +249,22 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc return persistentLogger; } + public LokiPublicChatManager getLokiPublicChatManager() { + return lokiPublicChatManager; + } + + public @Nullable LokiGroupChatAPI getLokiGroupChatAPI() { + if (lokiGroupChatAPI == null && TextSecurePreferences.isPushRegistered(this)) { + String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this); + byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize(); + LokiAPIDatabase apiDatabase = DatabaseFactory.getLokiAPIDatabase(this); + LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(this); + lokiGroupChatAPI = new LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase); + } + + return lokiGroupChatAPI; + } + private void initializeSecurityProvider() { try { Class.forName("org.signal.aesgcmprovider.AesGcmCipher"); @@ -471,10 +493,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc if (lokiLongPoller != null) { lokiLongPoller.startIfNeeded(); } } - private LokiGroupChat lokiPublicChat() { - return new LokiGroupChat(LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer(), "Loki Public Chat", true); - } - private LokiRSSFeed lokiNewsFeed() { return new LokiRSSFeed("loki.network.feed", "https://loki.network/feed/", "Loki News", true); } @@ -484,11 +502,21 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc } public void createGroupChatsIfNeeded() { - LokiGroupChat publicChat = lokiPublicChat(); - boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publicChat.getId()); - if (!isChatSetUp || !publicChat.isDeletable()) { - GroupManager.GroupActionResult result = GroupManager.createGroup(publicChat.getId(), this, new HashSet<>(), null, publicChat.getDisplayName(), false); - TextSecurePreferences.markChatSetUp(this, publicChat.getId()); + List defaultChats = LokiGroupChat.Companion.defaultChats(BuildConfig.DEBUG); + for (LokiGroupChat chat : defaultChats) { + long threadID = GroupManager.getThreadId(chat.getId(), this); + String migrationKey = chat.getId() + "_migrated"; + boolean isChatMigrated = TextSecurePreferences.getBooleanPreference(this, migrationKey, false); + boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, chat.getId()); + if (!isChatSetUp || !chat.isDeletable()) { + lokiPublicChatManager.addChat(chat.getServer(), chat.getChannel(), chat.getDisplayName()); + TextSecurePreferences.markChatSetUp(this, chat.getId()); + TextSecurePreferences.setBooleanPreference(this, migrationKey, true); + } else if (threadID > -1 && !isChatMigrated) { + // Migrate the old public chats. + DatabaseFactory.getLokiThreadDatabase(this).setGroupChat(chat, threadID); + TextSecurePreferences.setBooleanPreference(this, migrationKey, true); + } } } @@ -505,20 +533,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc } } - private void createGroupChatPollersIfNeeded() { - // Only create the group chat pollers if their threads aren't deleted - LokiGroupChat publicChat = lokiPublicChat(); - long threadID = GroupManager.getThreadId(publicChat.getId(), this); - if (threadID >= 0 && lokiPublicChatPoller == null) { - lokiPublicChatPoller = new LokiGroupChatPoller(this, publicChat); - // Set up deletion listeners if needed - setUpThreadDeletionListeners(threadID, () -> { - if (lokiPublicChatPoller != null) lokiPublicChatPoller.stop(); - lokiPublicChatPoller = null; - }); - } - } - private void createRSSFeedPollersIfNeeded() { // Only create the RSS feed pollers if their threads aren't deleted LokiRSSFeed lokiNewsFeed = lokiNewsFeed(); @@ -559,8 +573,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc } public void startGroupChatPollersIfNeeded() { - createGroupChatPollersIfNeeded(); - if (lokiPublicChatPoller != null) lokiPublicChatPoller.startIfNeeded(); + lokiPublicChatManager.startPollersIfNeeded(); } public void startRSSFeedPollersIfNeeded() { diff --git a/src/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/org/thoughtcrime/securesms/CreateProfileActivity.java index 2d39b63fb7..cf73ae580f 100644 --- a/src/org/thoughtcrime/securesms/CreateProfileActivity.java +++ b/src/org/thoughtcrime/securesms/CreateProfileActivity.java @@ -61,6 +61,7 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.util.StreamDetails; +import org.whispersystems.signalservice.loki.api.LokiGroupChat; import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; import org.whispersystems.signalservice.loki.utilities.Analytics; @@ -68,6 +69,8 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.security.SecureRandom; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; import javax.inject.Inject; @@ -377,12 +380,13 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje Analytics.Companion.getShared().track("Display Name Updated"); TextSecurePreferences.setProfileName(context, name); - - String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context); - byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).getPrivateKey().serialize(); - LokiAPIDatabase apiDatabase = DatabaseFactory.getLokiAPIDatabase(context); - LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(context); - new LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase).setDisplayName(name, LokiGroupChatAPI.getPublicChatServer()); + LokiGroupChatAPI chatAPI = ApplicationContext.getInstance(context).getLokiGroupChatAPI(); + if (chatAPI != null) { + Set groupChatServers = DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChatServers(); + for (String server : groupChatServers) { + chatAPI.setDisplayName(name, server); + } + } // Loki - Original code // ======== diff --git a/src/org/thoughtcrime/securesms/components/QuoteView.java b/src/org/thoughtcrime/securesms/components/QuoteView.java index 1f2801ef9d..b667eb6484 100644 --- a/src/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/org/thoughtcrime/securesms/components/QuoteView.java @@ -21,6 +21,7 @@ import com.annimon.stream.Stream; import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.GlideRequests; @@ -31,6 +32,7 @@ import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.signalservice.loki.api.LokiGroupChat; import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; import java.util.List; @@ -194,18 +196,14 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener boolean isOwnNumber = Util.isOwnNumber(getContext(), author.getAddress()); String quoteeDisplayName = author.toShortString(); - if (quoteeDisplayName.equals(author.getAddress().toString())) { - quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(LokiGroupChatAPI.getPublicChatServer() + "." + LokiGroupChatAPI.getPublicChatServerID(), author.getAddress().toString()); - } // If we're in a group then try and use the display name in the group if (conversationRecipient.isGroupRecipient()) { - try { - String serverId = GroupUtil.getDecodedStringId(conversationRecipient.getAddress().serialize()); - String senderDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(serverId, author.getAddress().serialize()); + long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient); + LokiGroupChat chat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); + if (chat != null) { + String senderDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(chat.getId(), author.getAddress().serialize()); if (senderDisplayName != null) { quoteeDisplayName = senderDisplayName; } - } catch (Exception e) { - // Do nothing } } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 40c91565f5..fa1e4cb365 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -70,6 +70,7 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHol import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.database.Address; +import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; @@ -105,6 +106,7 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.internal.util.concurrent.SettableFuture; +import org.whispersystems.signalservice.loki.api.LokiGroupChat; import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; import java.io.IOException; @@ -408,14 +410,15 @@ public class ConversationFragment extends Fragment boolean isGroupChat = recipient.isGroupRecipient(); if (isGroupChat) { - boolean isLokiPublicChat = recipient.getName() != null && recipient.getName().equals("Loki Public Chat"); + LokiGroupChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); + boolean isPublicChat = groupChat != null; int selectedMessageCount = messageRecords.size(); boolean isSentByUser = ((MessageRecord)messageRecords.toArray()[0]).isOutgoing(); - menu.findItem(R.id.menu_context_copy_public_key).setVisible(isLokiPublicChat && selectedMessageCount == 1 && !isSentByUser); - menu.findItem(R.id.menu_context_reply).setVisible(isLokiPublicChat && selectedMessageCount == 1); + menu.findItem(R.id.menu_context_copy_public_key).setVisible(isPublicChat && selectedMessageCount == 1 && !isSentByUser); + menu.findItem(R.id.menu_context_reply).setVisible(isPublicChat && selectedMessageCount == 1); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); - boolean userCanModerate = LokiGroupChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer()); - boolean isDeleteOptionVisible = isLokiPublicChat && selectedMessageCount == 1 && (isSentByUser || userCanModerate); + boolean userCanModerate = groupChat != null && LokiGroupChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, groupChat.getChannel(), groupChat.getServer()); + boolean isDeleteOptionVisible = isPublicChat && selectedMessageCount == 1 && (isSentByUser || userCanModerate); menu.findItem(R.id.menu_context_delete_message).setVisible(isDeleteOptionVisible); } else { menu.findItem(R.id.menu_context_copy_public_key).setVisible(false); @@ -509,8 +512,8 @@ public class ConversationFragment extends Fragment builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount)); builder.setCancelable(true); - // Loki - The delete option is only visible to the user in a group chat if it's the Loki Public Chat - boolean isLokiPublicChat = this.recipient.isGroupRecipient(); + // Loki - The delete option is only visible to the user in a group chat + LokiGroupChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { @Override @@ -524,19 +527,16 @@ public class ConversationFragment extends Fragment for (MessageRecord messageRecord : messageRecords) { boolean isThreadDeleted; - if (isLokiPublicChat) { + if (groupChat != null) { final SettableFuture[] future = { new SettableFuture() }; - String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); - LokiAPIDatabase lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(getContext()); - LokiUserDatabase lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(getContext()); - byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(getContext()).getPrivateKey().serialize(); + LokiGroupChatAPI chatAPI = ApplicationContext.getInstance(getContext()).getLokiGroupChatAPI(); boolean isSentByUser = messageRecord.isOutgoing(); Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id); - if (serverID != null) { - new LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase) - .deleteMessage(serverID, LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer(), isSentByUser) + if (chatAPI != null && serverID != null) { + chatAPI + .deleteMessage(serverID, groupChat.getChannel(), groupChat.getServer(), isSentByUser) .success(l -> { @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture) future[0]; f.set(Unit.INSTANCE); diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java index 798d843ab0..f8fef22029 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.components.StickerView; import org.thoughtcrime.securesms.components.emoji.EmojiTextView; import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase; @@ -112,6 +113,7 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.loki.api.LokiGroupChat; import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; import java.util.Collections; @@ -934,13 +936,15 @@ public class ConversationItem extends LinearLayout if (!next.isPresent() || next.get().isUpdate() || !current.getRecipient().getAddress().equals(next.get().getRecipient().getAddress())) { contactPhoto.setVisibility(VISIBLE); - int visibility; - if (conversationRecipient.getName() != null && conversationRecipient.getName().equals("Loki Public Chat")) { - boolean isModerator = LokiGroupChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer()); + int visibility = View.GONE; + + // If we have a chat then use that to determine mod status + LokiGroupChat groupChat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(messageRecord.getThreadId()); + if (groupChat != null) { + boolean isModerator = LokiGroupChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), groupChat.getChannel(), groupChat.getServer()); visibility = isModerator ? View.VISIBLE : View.GONE; - } else { - visibility = View.GONE; } + moderatorIconImageView.setVisibility(visibility); } else { contactPhoto.setVisibility(GONE); diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index efbe89544a..c92b054706 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -71,7 +71,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV2 = 23; private static final int lokiV3 = 24; - private static final int DATABASE_VERSION = lokiV2; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes + private static final int DATABASE_VERSION = lokiV3; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -499,8 +499,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { if (oldVersion < lokiV3) { db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand()); db.execSQL(LokiThreadDatabase.getCreateGroupChatMappingTableCommand()); - - // TODO: Map old public chat threads to new manager format } db.setTransactionSuccessful(); diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index c7e6291eff..8793474558 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; @@ -44,6 +45,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; +import org.whispersystems.signalservice.loki.api.LokiGroupChat; import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; import java.io.IOException; @@ -285,7 +287,14 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { private @NonNull List
getGroupMessageRecipients(String groupId, long messageId) { ArrayList
result = new ArrayList<>(); - result.add(Address.fromSerialized(LokiGroupChatAPI.getPublicChatServer())); // Loki - All group messages should be directed to the Loki Public Chat for now + + // Loki - All group messages should be directed to their servers + long threadID = GroupManager.getThreadIdFromGroupId(groupId, context); + LokiGroupChat chat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(threadID); + if (chat != null) { + result.add(Address.fromSerialized(chat.getServer())); + } + return result; /* diff --git a/src/org/thoughtcrime/securesms/loki/DatabaseUtilities.kt b/src/org/thoughtcrime/securesms/loki/DatabaseUtilities.kt index 41613af87c..8f252feaaf 100644 --- a/src/org/thoughtcrime/securesms/loki/DatabaseUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/DatabaseUtilities.kt @@ -50,6 +50,10 @@ fun Cursor.getString(columnName: String): String { return getString(getColumnIndexOrThrow(columnName)) } +fun Cursor.getLong(columnName: String): Long { + return getLong(getColumnIndexOrThrow(columnName)) +} + fun Cursor.getBase64EncodedData(columnName: String): ByteArray { return Base64.decode(getString(columnName)) } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt index 3685a2ad92..0fbcc18b55 100644 --- a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt @@ -45,12 +45,11 @@ class DisplayNameActivity : BaseActionBarActivity() { application.setUpStorageAPIIfNeeded() startActivity(Intent(this, ConversationListActivity::class.java)) finish() - val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this) - val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).privateKey.serialize() - val apiDatabase = DatabaseFactory.getLokiAPIDatabase(this) - val userDatabase = DatabaseFactory.getLokiUserDatabase(this) - if (name != null) { - LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase).setDisplayName(name, LokiGroupChatAPI.publicChatServer) + + val chatAPI = ApplicationContext.getInstance(this).lokiGroupChatAPI + if (chatAPI != null && name != null) { + val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllGroupChatServers() + servers.forEach { chatAPI.setDisplayName(name, it) } } } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt b/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt index 9d53000c5d..3be416b0f8 100644 --- a/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt @@ -24,8 +24,8 @@ fun toPx(dp: Int, resources: Resources): Int { return (dp * scale).roundToInt() } -fun isGroupRecipient(recipient: String): Boolean { - return (LokiGroupChatAPI.publicChatServer == recipient) +fun isGroupRecipient(context: Context, recipient: String): Boolean { + return DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChats().values.map { it.server }.contains(recipient) } fun getFriendPublicKeys(context: Context, devicePublicKeys: Set): Set { diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt index 166cfe3aed..9ac5dc0ca0 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt @@ -137,6 +137,12 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.insertOrUpdate(lastMessageServerIDCache, row, "$lastMessageServerIDCacheIndex = ?", wrap(index)) } + fun removeLastMessageServerID(group: Long, server: String) { + val database = databaseHelper.writableDatabase + val index = "$server.$group" + database.delete(lastMessageServerIDCache,"$lastMessageServerIDCacheIndex = ?", wrap(index)) + } + override fun getLastDeletionServerID(group: Long, server: String): Long? { val database = databaseHelper.readableDatabase val index = "$server.$group" @@ -152,6 +158,12 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.insertOrUpdate(lastDeletionServerIDCache, row, "$lastDeletionServerIDCacheIndex = ?", wrap(index)) } + fun removeLastDeletionServerID(group: Long, server: String) { + val database = databaseHelper.writableDatabase + val index = "$server.$group" + database.delete(lastDeletionServerIDCache,"$lastDeletionServerIDCacheIndex = ?", wrap(index)) + } + override fun getPairingAuthorisations(hexEncodedPublicKey: String): List { val database = databaseHelper.readableDatabase return database.getAll(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) { cursor -> diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt new file mode 100644 index 0000000000..bec1b1f1f1 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt @@ -0,0 +1,115 @@ +package org.thoughtcrime.securesms.loki + +import android.content.Context +import android.database.ContentObserver +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.database.DatabaseContentProviders +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.groups.GroupManager +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.loki.api.LokiGroupChat +import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI +import java.util.HashSet + +class LokiPublicChatManager(private val context: Context) { + private var chats = mutableMapOf() + private val pollers = mutableMapOf() + private val observers = mutableMapOf() + private var isPolling = false + + public fun startPollersIfNeeded() { + refreshChatsAndPollers() + + for ((threadId, chat) in chats) { + val poller = pollers[threadId] ?: LokiGroupChatPoller(context, chat) + poller.startIfNeeded() + listenToThreadDeletion(threadId) + if (!pollers.containsKey(threadId)) { pollers[threadId] = poller } + } + isPolling = true + } + + public fun stopPollers() { + pollers.values.forEach { it.stop() } + isPolling = false + } + + public fun addChat(server: String, channel: Long): Promise { + val groupChatAPI = ApplicationContext.getInstance(context).lokiGroupChatAPI ?: return Promise.ofFail(IllegalStateException()) + return groupChatAPI.getAuthToken(server).bind { + groupChatAPI.getChannelInfo(channel, server) + }.map { + addChat(server, channel, it) + } + } + + public fun addChat(server: String, channel: Long, name: String): LokiGroupChat { + val chat = LokiGroupChat(channel, server, name, true) + var threadID = GroupManager.getThreadId(chat.id, context) + // Create the group if we don't have one + if (threadID < 0) { + val result = GroupManager.createGroup(chat.id, context, HashSet(), null, chat.displayName, false) + threadID = result.threadId + } + DatabaseFactory.getLokiThreadDatabase(context).setGroupChat(chat, threadID) + startPollersIfNeeded() + + // Set our name on the server + ApplicationContext.getInstance(context).lokiGroupChatAPI?.setDisplayName(server, TextSecurePreferences.getProfileName(context)) + + return chat + } + + private fun refreshChatsAndPollers() { + val chatsInDB = DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChats() + val removedChatThreadIds = chats.keys.filter { !chatsInDB.keys.contains(it) } + removedChatThreadIds.forEach { pollers.remove(it)?.stop() } + + // Only append to chats if we have a thread for the chat + chats = chatsInDB.filter { GroupManager.getThreadId(it.value.id, context) > -1 }.toMutableMap() + } + + private fun listenToThreadDeletion(threadID: Long) { + if (threadID < 0 || observers[threadID] != null) { return } + val observer = createDeletionObserver(threadID, Runnable { + val chat = chats[threadID] + + // Reset last message cache + if (chat != null) { + val apiDatabase = DatabaseFactory.getLokiAPIDatabase(context) + apiDatabase.removeLastDeletionServerID(chat.channel, chat.server) + apiDatabase.removeLastMessageServerID(chat.channel, chat.server) + } + + DatabaseFactory.getLokiThreadDatabase(context).removeGroupChat(threadID) + pollers.remove(threadID)?.stop() + observers.remove(threadID) + startPollersIfNeeded() + }) + observers[threadID] = observer + + context.applicationContext.contentResolver.registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadID), true, observer) + } + + private fun createDeletionObserver(threadID: Long, onDelete: Runnable): ContentObserver { + return object : ContentObserver(null) { + + override fun onChange(selfChange: Boolean) { + super.onChange(selfChange) + // Stop the poller if thread is deleted + try { + if (!DatabaseFactory.getThreadDatabase(context).hasThread(threadID)) { + onDelete.run() + context.applicationContext.contentResolver.unregisterContentObserver(this) + } + } catch (e: Exception) { + // TODO: Handle + } + } + } + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt index 1d1158caa3..a19acf580f 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki import android.content.ContentValues import android.content.Context +import android.database.Cursor import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.DatabaseFactory @@ -12,6 +13,7 @@ import org.whispersystems.signalservice.loki.api.LokiGroupChat import org.whispersystems.signalservice.loki.messaging.LokiThreadDatabaseProtocol import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus +import java.lang.Exception class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiThreadDatabaseProtocol { var delegate: LokiThreadDatabaseDelegate? = null @@ -19,11 +21,11 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa companion object { private val friendRequestTableName = "loki_thread_friend_request_database" private val sessionResetTableName = "loki_thread_session_reset_database" - private val groupChatMappingTableName = "loki_group_chat_mapping_database" - private val threadID = "thread_id" + public val groupChatMappingTableName = "loki_group_chat_mapping_database" + public val threadID = "thread_id" private val friendRequestStatus = "friend_request_status" private val sessionResetStatus = "session_reset_status" - private val groupChatJSON = "group_chat_json" + public val groupChatJSON = "group_chat_json" @JvmStatic val createFriendRequestTableCommand = "CREATE TABLE $friendRequestTableName ($threadID INTEGER PRIMARY KEY, $friendRequestStatus INTEGER DEFAULT 0);" @JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTableName ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);" @JvmStatic val createGroupChatMappingTableCommand = "CREATE TABLE $groupChatMappingTableName ($threadID INTEGER PRIMARY KEY, $groupChatJSON TEXT);" @@ -90,7 +92,35 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa notifyConversationListeners(threadID) } + fun getAllGroupChats(): Map { + val database = databaseHelper.readableDatabase + var cursor: Cursor? = null + + try { + val map = mutableMapOf() + cursor = database.rawQuery("select * from $groupChatMappingTableName", null) + while (cursor != null && cursor.moveToNext()) { + val threadID = cursor.getLong(Companion.threadID) + val string = cursor.getString(groupChatJSON) + val chat = LokiGroupChat.fromJSON(string) + if (chat != null) { map[threadID] = chat } + } + return map + } catch (e: Exception) { + + } finally { + cursor?.close() + } + + return mapOf() + } + + fun getAllGroupChatServers(): Set { + return getAllGroupChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) } + } + override fun getGroupChat(threadID: Long): LokiGroupChat? { + if (threadID < 0) { return null } val database = databaseHelper.readableDatabase return database.get(groupChatMappingTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor -> val string = cursor.getString(groupChatJSON) @@ -99,6 +129,8 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa } override fun setGroupChat(groupChat: LokiGroupChat, threadID: Long) { + if (threadID < 0) { return } + val database = databaseHelper.writableDatabase val contentValues = ContentValues(2) contentValues.put(Companion.threadID, threadID) diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index f43c973b35..6676dfcf5f 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -213,7 +213,7 @@ public class MessageSender { // Just send the message normally if it's a group message String recipientPublicKey = recipient.getAddress().serialize(); - if (GeneralUtilitiesKt.isGroupRecipient(recipientPublicKey)) { + if (GeneralUtilitiesKt.isGroupRecipient(context, recipientPublicKey)) { jobManager.add(new PushTextSendJob(messageId, recipient.getAddress())); return; } @@ -243,7 +243,7 @@ public class MessageSender { // Just send the message normally if it's a group message String recipientPublicKey = recipient.getAddress().serialize(); - if (GeneralUtilitiesKt.isGroupRecipient(recipientPublicKey)) { + if (GeneralUtilitiesKt.isGroupRecipient(context, recipientPublicKey)) { PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress()); return; } From b676c25930dce063c97a0ab8c0beb81f455e7354 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 11 Oct 2019 09:56:30 +1100 Subject: [PATCH 03/10] Add UI --- AndroidManifest.xml | 8 +- res/layout/fragment_add_public_chat.xml | 48 +++++++++++ res/values/strings.xml | 10 +++ res/xml/preferences.xml | 4 + .../ApplicationPreferencesActivity.java | 12 +++ .../securesms/loki/AddPublicChatActivity.kt | 81 +++++++++++++++++++ .../securesms/loki/LokiPublicChatManager.kt | 2 +- 7 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 res/layout/fragment_add_public_chat.xml create mode 100644 src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c30e4385da..165f1eb2df 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -477,8 +477,12 @@ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" /> + android:windowSoftInputMode="stateAlwaysVisible" + android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" /> + + diff --git a/res/layout/fragment_add_public_chat.xml b/res/layout/fragment_add_public_chat.xml new file mode 100644 index 0000000000..8f677a5784 --- /dev/null +++ b/res/layout/fragment_add_public_chat.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 59d313fa93..f1ba97c51a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1575,6 +1575,7 @@ Copied to clipboard Share Public Key Show QR Code + Add Public Chat Link Device Show Seed Your Seed @@ -1598,6 +1599,15 @@ Next Invalid public key Please enter the public key of the person you\'d like to message + + Add Public Chat + Server URL + Enter the full URL of the public server. E.g https://public-server.url + Add + Adding Server... + Invalid Url provided + Failed to connect to server + Added public chat server Accept Reject diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 99aa910c01..2260f55718 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -41,6 +41,10 @@ android:title="@string/activity_settings_show_qr_code_button_title" android:icon="@drawable/icon_qr_code"/> + + diff --git a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index 6799c9a3c6..97e646e136 100644 --- a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -40,8 +40,10 @@ import android.support.v7.preference.Preference; import android.widget.Toast; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; +import org.thoughtcrime.securesms.loki.AddPublicChatActivity; import org.thoughtcrime.securesms.loki.DeviceLinkingDialog; import org.thoughtcrime.securesms.loki.DeviceLinkingView; +import org.thoughtcrime.securesms.loki.NewConversationActivity; import org.thoughtcrime.securesms.loki.QRCodeDialog; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment; @@ -85,6 +87,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA private static final String PREFERENCE_CATEGORY_QR_CODE = "preference_category_qr_code"; private static final String PREFERENCE_CATEGORY_LINK_DEVICE = "preference_category_link_device"; private static final String PREFERENCE_CATEGORY_SEED = "preference_category_seed"; + private static final String PREFERENCE_CATEGORY_PUBLIC_CHAT = "preference_category_public_chat"; private final DynamicTheme dynamicTheme = new DynamicTheme(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); @@ -187,6 +190,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PUBLIC_KEY)); this.findPreference(PREFERENCE_CATEGORY_QR_CODE) .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_QR_CODE)); + this.findPreference(PREFERENCE_CATEGORY_PUBLIC_CHAT) + .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PUBLIC_CHAT)); + Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE); // Hide if this is a slave device linkDevicePreference.setVisible(isMasterDevice); @@ -256,6 +262,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA Drawable qrCode = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.icon_qr_code)); Drawable linkDevice = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.icon_link)); Drawable seed = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.icon_seedling)); + Drawable publicChat = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.ic_group_white_24dp)); int[] tintAttr = new int[]{R.attr.pref_icon_tint}; TypedArray typedArray = context.obtainStyledAttributes(tintAttr); @@ -273,6 +280,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA DrawableCompat.setTint(qrCode, color); DrawableCompat.setTint(linkDevice, color); DrawableCompat.setTint(seed, color); + DrawableCompat.setTint(publicChat, color); // this.findPreference(PREFERENCE_CATEGORY_SMS_MMS).setIcon(sms); this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS).setIcon(notifications); @@ -285,6 +293,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA this.findPreference(PREFERENCE_CATEGORY_QR_CODE).setIcon(qrCode); this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE).setIcon(linkDevice); this.findPreference(PREFERENCE_CATEGORY_SEED).setIcon(seed); + this.findPreference(PREFERENCE_CATEGORY_PUBLIC_CHAT).setIcon(publicChat); } private class CategoryClickListener implements Preference.OnPreferenceClickListener { @@ -369,6 +378,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA // Do nothing } break; + case PREFERENCE_CATEGORY_PUBLIC_CHAT: + startActivity(new Intent(getActivity(), AddPublicChatActivity.class)); + break; default: throw new AssertionError(); } diff --git a/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt new file mode 100644 index 0000000000..174396f2e0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt @@ -0,0 +1,81 @@ +package org.thoughtcrime.securesms.loki + +import android.Manifest +import android.content.Intent +import android.os.Bundle +import android.util.Patterns +import android.view.MenuItem +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import kotlinx.android.synthetic.main.activity_account_details.* +import kotlinx.android.synthetic.main.fragment_add_public_chat.* +import network.loki.messenger.R +import nl.komponents.kovenant.ui.failUi +import nl.komponents.kovenant.ui.successUi +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity +import org.thoughtcrime.securesms.conversation.ConversationActivity +import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.DynamicTheme +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI +import org.whispersystems.signalservice.loki.utilities.Analytics +import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation + +class AddPublicChatActivity : PassphraseRequiredActionBarActivity() { + private val dynamicTheme = DynamicTheme() + + override fun onPreCreate() { + dynamicTheme.onCreate(this) + } + + override fun onCreate(bundle: Bundle?, isReady: Boolean) { + supportActionBar!!.setTitle(R.string.fragment_add_public_chat_title) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + setContentView(R.layout.fragment_add_public_chat) + setButtonEnabled(true) + addButton.setOnClickListener { addPublicChatIfPossible() } + } + + public override fun onResume() { + super.onResume() + dynamicTheme.onResume(this) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + + private fun addPublicChatIfPossible() { + val inputMethodManager = getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(serverUrlEditText.windowToken, 0) + + val url = serverUrlEditText.text.toString().toLowerCase() + if (!Patterns.WEB_URL.matcher(url).matches()) { return Toast.makeText(this, R.string.fragment_add_public_chat_invalid_url_message, Toast.LENGTH_SHORT).show() } + + setButtonEnabled(false) + + ApplicationContext.getInstance(this).lokiPublicChatManager.addChat(url, 1).successUi { + Toast.makeText(this, R.string.fragment_add_public_chat_success_message, Toast.LENGTH_SHORT).show() + finish() + }.failUi { + setButtonEnabled(true) + Toast.makeText(this, R.string.fragment_add_public_chat_failed_connect_message, Toast.LENGTH_SHORT).show() + } + } + + private fun setButtonEnabled(enabled: Boolean) { + addButton.isEnabled = enabled + val text = if (enabled) R.string.fragment_add_public_chat_add_button_title else R.string.fragment_add_public_chat_adding_server_button_title + addButton.setText(text) + serverUrlEditText.isEnabled = enabled + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt index bec1b1f1f1..6ee7c68480 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt @@ -39,7 +39,7 @@ class LokiPublicChatManager(private val context: Context) { } public fun addChat(server: String, channel: Long): Promise { - val groupChatAPI = ApplicationContext.getInstance(context).lokiGroupChatAPI ?: return Promise.ofFail(IllegalStateException()) + val groupChatAPI = ApplicationContext.getInstance(context).lokiGroupChatAPI ?: return Promise.ofFail(IllegalStateException("LokiGroupChatAPI is not set!")) return groupChatAPI.getAuthToken(server).bind { groupChatAPI.getChannelInfo(channel, server) }.map { From 4657b7917951f782620e7f7a360e683f000e9e92 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 11 Oct 2019 12:37:45 +1100 Subject: [PATCH 04/10] UI improvements. --- .../thoughtcrime/securesms/ApplicationContext.java | 2 +- .../thoughtcrime/securesms/database/Address.java | 14 +++++++++++++- .../securesms/jobs/PushGroupSendJob.java | 4 +++- .../securesms/loki/AddPublicChatActivity.kt | 4 ++-- .../securesms/loki/LokiPublicChatManager.kt | 12 +++++++++--- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 17fd512b39..b55ee706df 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -254,7 +254,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc } public @Nullable LokiGroupChatAPI getLokiGroupChatAPI() { - if (lokiGroupChatAPI == null && TextSecurePreferences.isPushRegistered(this)) { + if (lokiGroupChatAPI == null && IdentityKeyUtil.hasIdentityKey(this)) { String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this); byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize(); LokiAPIDatabase apiDatabase = DatabaseFactory.getLokiAPIDatabase(this); diff --git a/src/org/thoughtcrime/securesms/database/Address.java b/src/org/thoughtcrime/securesms/database/Address.java index f70033991c..40d8a2ebd5 100644 --- a/src/org/thoughtcrime/securesms/database/Address.java +++ b/src/org/thoughtcrime/securesms/database/Address.java @@ -52,9 +52,17 @@ public class Address implements Parcelable, Comparable
{ private final String address; + // Loki - Special flag to indicate whether this address is meant to representing a public chat or not + private Boolean isPublicChat; + private Address(@NonNull String address) { + this(address, false); + } + + private Address(@NonNull String address, Boolean isPublicChat) { if (address == null) throw new AssertionError(address); this.address = address; + this.isPublicChat = isPublicChat; } public Address(Parcel in) { @@ -69,6 +77,10 @@ public class Address implements Parcelable, Comparable
{ return Address.fromSerialized(external); } + public static @NonNull Address fromPublicChatGroupID(@NonNull String serialized) { + return new Address(serialized, true); + } + public static @NonNull List
fromSerializedList(@NonNull String serialized, char delimiter) { String[] escapedAddresses = DelimiterUtil.split(serialized, delimiter); List
addresses = new LinkedList<>(); @@ -131,7 +143,7 @@ public class Address implements Parcelable, Comparable
{ } public @NonNull String toPhoneString() { - if (!isPhone()) { + if (!isPhone() && !isPublicChat) { if (isEmail()) throw new AssertionError("Not e164, is email"); if (isGroup()) throw new AssertionError("Not e164, is group"); throw new AssertionError("Not e164, unknown"); diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 8793474558..6885505cd8 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -292,7 +292,9 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { long threadID = GroupManager.getThreadIdFromGroupId(groupId, context); LokiGroupChat chat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(threadID); if (chat != null) { - result.add(Address.fromSerialized(chat.getServer())); + // We need to somehow maintain information that will allow the sender to map + // a Recipient to the correct public chat thread, and so this might be a bit hacky + result.add(Address.fromPublicChatGroupID(groupId)); } return result; diff --git a/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt index 174396f2e0..26f57edf38 100644 --- a/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt @@ -58,8 +58,8 @@ class AddPublicChatActivity : PassphraseRequiredActionBarActivity() { val inputMethodManager = getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow(serverUrlEditText.windowToken, 0) - val url = serverUrlEditText.text.toString().toLowerCase() - if (!Patterns.WEB_URL.matcher(url).matches()) { return Toast.makeText(this, R.string.fragment_add_public_chat_invalid_url_message, Toast.LENGTH_SHORT).show() } + val url = serverUrlEditText.text.toString().toLowerCase().replace("http://", "https://") + if (!Patterns.WEB_URL.matcher(url).matches() || !url.startsWith("https://")) { return Toast.makeText(this, R.string.fragment_add_public_chat_invalid_url_message, Toast.LENGTH_SHORT).show() } setButtonEnabled(false) diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt index 6ee7c68480..13e70b6a33 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki import android.content.Context import android.database.ContentObserver +import android.text.TextUtils import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map @@ -10,7 +11,9 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.groups.GroupManager +import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.loki.api.LokiGroupChat import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI import java.util.HashSet @@ -56,10 +59,13 @@ class LokiPublicChatManager(private val context: Context) { threadID = result.threadId } DatabaseFactory.getLokiThreadDatabase(context).setGroupChat(chat, threadID) - startPollersIfNeeded() - // Set our name on the server - ApplicationContext.getInstance(context).lokiGroupChatAPI?.setDisplayName(server, TextSecurePreferences.getProfileName(context)) + val displayName = TextSecurePreferences.getProfileName(context) + if (!TextUtils.isEmpty(displayName)) { + ApplicationContext.getInstance(context).lokiGroupChatAPI?.setDisplayName(server, displayName) + } + // Start polling + Util.runOnMain{ startPollersIfNeeded() } return chat } From a8c4fa22a3f21bd0de1646163503445a335e1e69 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 15 Oct 2019 13:39:17 +1100 Subject: [PATCH 05/10] Partially fix build --- .../securesms/ApplicationContext.java | 18 ++++++------ .../securesms/CreateProfileActivity.java | 6 ++-- .../securesms/components/QuoteView.java | 6 ++-- .../conversation/ConversationFragment.java | 12 ++++---- .../conversation/ConversationItem.java | 8 ++--- .../securesms/jobs/PushGroupSendJob.java | 6 ++-- .../securesms/loki/AddPublicChatActivity.kt | 12 -------- .../securesms/loki/DisplayNameActivity.kt | 6 ++-- .../securesms/loki/GeneralUtilities.kt | 3 +- .../securesms/loki/LokiAPIUtilities.kt | 4 +-- .../securesms/loki/LokiPublicChatManager.kt | 29 +++++++++---------- ...pChatPoller.kt => LokiPublicChatPoller.kt} | 16 +++++----- .../securesms/loki/LokiThreadDatabase.kt | 21 +++++++------- .../securesms/loki/MentionUtilities.kt | 15 +++++----- .../securesms/loki/UserSelectionView.kt | 17 ++++++++--- .../securesms/loki/UserSelectionViewCell.kt | 13 ++++++--- 16 files changed, 94 insertions(+), 98 deletions(-) rename src/org/thoughtcrime/securesms/loki/{LokiGroupChatPoller.kt => LokiPublicChatPoller.kt} (93%) diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index b55ee706df..582a88f59d 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -87,8 +87,8 @@ import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol; -import org.whispersystems.signalservice.loki.api.LokiGroupChat; -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import org.whispersystems.signalservice.loki.api.LokiLongPoller; import org.whispersystems.signalservice.loki.api.LokiP2PAPI; import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate; @@ -138,7 +138,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc private LokiRSSFeedPoller lokiNewsFeedPoller = null; private LokiRSSFeedPoller lokiMessengerUpdatesFeedPoller = null; private LokiPublicChatManager lokiPublicChatManager = null; - private LokiGroupChatAPI lokiGroupChatAPI = null; + private LokiPublicChatAPI lokiPublicChatAPI = null; public SignalCommunicationModule communicationModule; public MixpanelAPI mixpanel; @@ -253,16 +253,16 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc return lokiPublicChatManager; } - public @Nullable LokiGroupChatAPI getLokiGroupChatAPI() { - if (lokiGroupChatAPI == null && IdentityKeyUtil.hasIdentityKey(this)) { + public @Nullable LokiPublicChatAPI getLokiPublicChatAPI() { + if (lokiPublicChatAPI == null && IdentityKeyUtil.hasIdentityKey(this)) { String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this); byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).getPrivateKey().serialize(); LokiAPIDatabase apiDatabase = DatabaseFactory.getLokiAPIDatabase(this); LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(this); - lokiGroupChatAPI = new LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase); + lokiPublicChatAPI = new LokiPublicChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase); } - return lokiGroupChatAPI; + return lokiPublicChatAPI; } private void initializeSecurityProvider() { @@ -502,8 +502,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc } public void createGroupChatsIfNeeded() { - List defaultChats = LokiGroupChat.Companion.defaultChats(BuildConfig.DEBUG); - for (LokiGroupChat chat : defaultChats) { + List defaultChats = LokiPublicChat.Companion.defaultChats(BuildConfig.DEBUG); + for (LokiPublicChat chat : defaultChats) { long threadID = GroupManager.getThreadId(chat.getId(), this); String migrationKey = chat.getId() + "_migrated"; boolean isChatMigrated = TextSecurePreferences.getBooleanPreference(this, migrationKey, false); diff --git a/src/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/org/thoughtcrime/securesms/CreateProfileActivity.java index ce7c00cdf9..7febb79499 100644 --- a/src/org/thoughtcrime/securesms/CreateProfileActivity.java +++ b/src/org/thoughtcrime/securesms/CreateProfileActivity.java @@ -61,8 +61,8 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.util.StreamDetails; -import org.whispersystems.signalservice.loki.api.LokiGroupChat; -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import org.whispersystems.signalservice.loki.utilities.Analytics; import java.io.ByteArrayInputStream; @@ -383,7 +383,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje Analytics.Companion.getShared().track("Display Name Updated"); TextSecurePreferences.setProfileName(context, name); - LokiGroupChatAPI chatAPI = ApplicationContext.getInstance(context).getLokiGroupChatAPI(); + LokiPublicChatAPI chatAPI = ApplicationContext.getInstance(context).getLokiPublicChatAPI(); if (chatAPI != null) { Set groupChatServers = DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChatServers(); for (String server : groupChatServers) { diff --git a/src/org/thoughtcrime/securesms/components/QuoteView.java b/src/org/thoughtcrime/securesms/components/QuoteView.java index b667eb6484..a3ce8126d6 100644 --- a/src/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/org/thoughtcrime/securesms/components/QuoteView.java @@ -32,8 +32,8 @@ import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.signalservice.loki.api.LokiGroupChat; -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import java.util.List; @@ -200,7 +200,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener // If we're in a group then try and use the display name in the group if (conversationRecipient.isGroupRecipient()) { long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient); - LokiGroupChat chat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); + LokiPublicChat chat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); if (chat != null) { String senderDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(chat.getId(), author.getAddress().serialize()); if (senderDisplayName != null) { quoteeDisplayName = senderDisplayName; } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java index fa1e4cb365..a19beb5f0c 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -106,8 +106,8 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.internal.util.concurrent.SettableFuture; -import org.whispersystems.signalservice.loki.api.LokiGroupChat; -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import java.io.IOException; import java.io.InputStream; @@ -410,14 +410,14 @@ public class ConversationFragment extends Fragment boolean isGroupChat = recipient.isGroupRecipient(); if (isGroupChat) { - LokiGroupChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); + LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); boolean isPublicChat = groupChat != null; int selectedMessageCount = messageRecords.size(); boolean isSentByUser = ((MessageRecord)messageRecords.toArray()[0]).isOutgoing(); menu.findItem(R.id.menu_context_copy_public_key).setVisible(isPublicChat && selectedMessageCount == 1 && !isSentByUser); menu.findItem(R.id.menu_context_reply).setVisible(isPublicChat && selectedMessageCount == 1); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); - boolean userCanModerate = groupChat != null && LokiGroupChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, groupChat.getChannel(), groupChat.getServer()); + boolean userCanModerate = groupChat != null && LokiPublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, groupChat.getChannel(), groupChat.getServer()); boolean isDeleteOptionVisible = isPublicChat && selectedMessageCount == 1 && (isSentByUser || userCanModerate); menu.findItem(R.id.menu_context_delete_message).setVisible(isDeleteOptionVisible); } else { @@ -513,7 +513,7 @@ public class ConversationFragment extends Fragment builder.setCancelable(true); // Loki - The delete option is only visible to the user in a group chat - LokiGroupChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); + LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { @Override @@ -530,7 +530,7 @@ public class ConversationFragment extends Fragment if (groupChat != null) { final SettableFuture[] future = { new SettableFuture() }; - LokiGroupChatAPI chatAPI = ApplicationContext.getInstance(getContext()).getLokiGroupChatAPI(); + LokiPublicChatAPI chatAPI = ApplicationContext.getInstance(getContext()).getLokiPublicChatAPI(); boolean isSentByUser = messageRecord.isOutgoing(); Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id); diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java index 3867892ae8..c56b1d6af8 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -114,8 +114,8 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.loki.api.LokiGroupChat; -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import java.util.Collections; import java.util.HashSet; @@ -941,9 +941,9 @@ public class ConversationItem extends LinearLayout int visibility = View.GONE; // If we have a chat then use that to determine mod status - LokiGroupChat groupChat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(messageRecord.getThreadId()); + LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(messageRecord.getThreadId()); if (groupChat != null) { - boolean isModerator = LokiGroupChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), groupChat.getChannel(), groupChat.getServer()); + boolean isModerator = LokiPublicChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), groupChat.getChannel(), groupChat.getServer()); visibility = isModerator ? View.VISIBLE : View.GONE; } diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 6885505cd8..4532e619ca 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -45,8 +45,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; -import org.whispersystems.signalservice.loki.api.LokiGroupChat; -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import java.io.IOException; import java.util.ArrayList; @@ -290,7 +290,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { // Loki - All group messages should be directed to their servers long threadID = GroupManager.getThreadIdFromGroupId(groupId, context); - LokiGroupChat chat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(threadID); + LokiPublicChat chat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(threadID); if (chat != null) { // We need to somehow maintain information that will allow the sender to map // a Recipient to the correct public chat thread, and so this might be a bit hacky diff --git a/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt index 26f57edf38..9e034f32bc 100644 --- a/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt @@ -1,13 +1,10 @@ package org.thoughtcrime.securesms.loki -import android.Manifest -import android.content.Intent import android.os.Bundle import android.util.Patterns import android.view.MenuItem import android.view.inputmethod.InputMethodManager import android.widget.Toast -import kotlinx.android.synthetic.main.activity_account_details.* import kotlinx.android.synthetic.main.fragment_add_public_chat.* import network.loki.messenger.R import nl.komponents.kovenant.ui.failUi @@ -15,16 +12,7 @@ import nl.komponents.kovenant.ui.successUi import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.conversation.ConversationActivity -import org.thoughtcrime.securesms.database.Address -import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.database.ThreadDatabase -import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.DynamicTheme -import org.thoughtcrime.securesms.util.TextSecurePreferences -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI -import org.whispersystems.signalservice.loki.utilities.Analytics -import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation class AddPublicChatActivity : PassphraseRequiredActionBarActivity() { private val dynamicTheme = DynamicTheme() diff --git a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt index 9903881ca8..18bc717ba4 100644 --- a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt @@ -8,11 +8,9 @@ import network.loki.messenger.R import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.ConversationListActivity -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.api.crypto.ProfileCipher -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI import org.whispersystems.signalservice.loki.utilities.Analytics class DisplayNameActivity : BaseActionBarActivity() { @@ -46,9 +44,9 @@ class DisplayNameActivity : BaseActionBarActivity() { startActivity(Intent(this, ConversationListActivity::class.java)) finish() - val chatAPI = ApplicationContext.getInstance(this).lokiGroupChatAPI + val chatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI if (chatAPI != null && name != null) { - val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllGroupChatServers() + val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers() servers.forEach { chatAPI.setDisplayName(name, it) } } } diff --git a/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt b/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt index 3be416b0f8..6da575b9ce 100644 --- a/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt @@ -7,7 +7,6 @@ import android.support.annotation.ColorRes import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.recipients.Recipient -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus import kotlin.math.roundToInt @@ -25,7 +24,7 @@ fun toPx(dp: Int, resources: Resources): Int { } fun isGroupRecipient(context: Context, recipient: String): Boolean { - return DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChats().values.map { it.server }.contains(recipient) + return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().values.map { it.server }.contains(recipient) } fun getFriendPublicKeys(context: Context, devicePublicKeys: Set): Set { diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt index 3c110606c5..be5a8ecd10 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiAPIUtilities.kt @@ -9,7 +9,7 @@ import org.whispersystems.signalservice.loki.api.LokiAPI object LokiAPIUtilities { fun populateUserIDCacheIfNeeded(threadID: Long, context: Context) { - if (LokiAPI.userIDCache[threadID] != null) { return } + if (LokiAPI.userHexEncodedPublicKeyCache[threadID] != null) { return } val result = mutableSetOf() val messageDatabase = DatabaseFactory.getMmsSmsDatabase(context) val reader = messageDatabase.readerFor(messageDatabase.getConversation(threadID)) @@ -24,6 +24,6 @@ object LokiAPIUtilities { } reader.close() result.add(TextSecurePreferences.getLocalNumber(context)) - LokiAPI.userIDCache[threadID] = result + LokiAPI.userHexEncodedPublicKeyCache[threadID] = result } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt index 13e70b6a33..b8ae58f0bb 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt @@ -7,20 +7,17 @@ import nl.komponents.kovenant.Promise import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.DatabaseContentProviders import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.groups.GroupManager -import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util -import org.whispersystems.signalservice.loki.api.LokiGroupChat -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI -import java.util.HashSet +import org.whispersystems.signalservice.loki.api.LokiPublicChat +import java.util.* class LokiPublicChatManager(private val context: Context) { - private var chats = mutableMapOf() - private val pollers = mutableMapOf() + private var chats = mutableMapOf() + private val pollers = mutableMapOf() private val observers = mutableMapOf() private var isPolling = false @@ -28,7 +25,7 @@ class LokiPublicChatManager(private val context: Context) { refreshChatsAndPollers() for ((threadId, chat) in chats) { - val poller = pollers[threadId] ?: LokiGroupChatPoller(context, chat) + val poller = pollers[threadId] ?: LokiPublicChatPoller(context, chat) poller.startIfNeeded() listenToThreadDeletion(threadId) if (!pollers.containsKey(threadId)) { pollers[threadId] = poller } @@ -41,8 +38,8 @@ class LokiPublicChatManager(private val context: Context) { isPolling = false } - public fun addChat(server: String, channel: Long): Promise { - val groupChatAPI = ApplicationContext.getInstance(context).lokiGroupChatAPI ?: return Promise.ofFail(IllegalStateException("LokiGroupChatAPI is not set!")) + public fun addChat(server: String, channel: Long): Promise { + val groupChatAPI = ApplicationContext.getInstance(context).lokiPublicChatAPI ?: return Promise.ofFail(IllegalStateException("LokiPublicChatAPI is not set!")) return groupChatAPI.getAuthToken(server).bind { groupChatAPI.getChannelInfo(channel, server) }.map { @@ -50,19 +47,19 @@ class LokiPublicChatManager(private val context: Context) { } } - public fun addChat(server: String, channel: Long, name: String): LokiGroupChat { - val chat = LokiGroupChat(channel, server, name, true) + public fun addChat(server: String, channel: Long, name: String): LokiPublicChat { + val chat = LokiPublicChat(channel, server, name, true) var threadID = GroupManager.getThreadId(chat.id, context) // Create the group if we don't have one if (threadID < 0) { val result = GroupManager.createGroup(chat.id, context, HashSet(), null, chat.displayName, false) threadID = result.threadId } - DatabaseFactory.getLokiThreadDatabase(context).setGroupChat(chat, threadID) + DatabaseFactory.getLokiThreadDatabase(context).setPublicChat(chat, threadID) // Set our name on the server val displayName = TextSecurePreferences.getProfileName(context) if (!TextUtils.isEmpty(displayName)) { - ApplicationContext.getInstance(context).lokiGroupChatAPI?.setDisplayName(server, displayName) + ApplicationContext.getInstance(context).lokiPublicChatAPI?.setDisplayName(server, displayName) } // Start polling Util.runOnMain{ startPollersIfNeeded() } @@ -71,7 +68,7 @@ class LokiPublicChatManager(private val context: Context) { } private fun refreshChatsAndPollers() { - val chatsInDB = DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChats() + val chatsInDB = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats() val removedChatThreadIds = chats.keys.filter { !chatsInDB.keys.contains(it) } removedChatThreadIds.forEach { pollers.remove(it)?.stop() } @@ -91,7 +88,7 @@ class LokiPublicChatManager(private val context: Context) { apiDatabase.removeLastMessageServerID(chat.channel, chat.server) } - DatabaseFactory.getLokiThreadDatabase(context).removeGroupChat(threadID) + DatabaseFactory.getLokiThreadDatabase(context).removePublicChat(threadID) pollers.remove(threadID)?.stop() observers.remove(threadID) startPollersIfNeeded() diff --git a/src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt similarity index 93% rename from src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt rename to src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt index 0d4435746a..2530ae021c 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt @@ -21,23 +21,23 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.messages.SignalServiceGroup import org.whispersystems.signalservice.api.push.SignalServiceAddress -import org.whispersystems.signalservice.loki.api.LokiGroupChat -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI -import org.whispersystems.signalservice.loki.api.LokiGroupMessage +import org.whispersystems.signalservice.loki.api.LokiPublicChat +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI +import org.whispersystems.signalservice.loki.api.LokiPublicChatMessage -class LokiGroupChatPoller(private val context: Context, private val group: LokiGroupChat) { +class LokiPublicChatPoller(private val context: Context, private val group: LokiPublicChat) { private val handler = Handler() private var hasStarted = false // region Convenience private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context) - private val api: LokiGroupChatAPI + private val api: LokiPublicChatAPI get() = { val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize() val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context) val lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(context) - LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase) + LokiPublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase) }() // endregion @@ -94,7 +94,7 @@ class LokiGroupChatPoller(private val context: Context, private val group: LokiG // region Polling private fun pollForNewMessages() { - fun processIncomingMessage(message: LokiGroupMessage) { + fun processIncomingMessage(message: LokiPublicChatMessage) { val id = group.id.toByteArray() val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null) val quote: SignalServiceDataMessage.Quote? @@ -113,7 +113,7 @@ class LokiGroupChatPoller(private val context: Context, private val group: LokiG PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.of(message.serverID)) } } - fun processOutgoingMessage(message: LokiGroupMessage) { + fun processOutgoingMessage(message: LokiPublicChatMessage) { val messageServerID = message.serverID ?: return val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context) val isDuplicate = lokiMessageDatabase.getMessageID(messageServerID) != null diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt index a19acf580f..b7e604a587 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt @@ -9,11 +9,10 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.recipients.Recipient import org.whispersystems.signalservice.internal.util.JsonUtil -import org.whispersystems.signalservice.loki.api.LokiGroupChat +import org.whispersystems.signalservice.loki.api.LokiPublicChat import org.whispersystems.signalservice.loki.messaging.LokiThreadDatabaseProtocol import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus -import java.lang.Exception class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiThreadDatabaseProtocol { var delegate: LokiThreadDatabaseDelegate? = null @@ -92,17 +91,17 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa notifyConversationListeners(threadID) } - fun getAllGroupChats(): Map { + fun getAllPublicChats(): Map { val database = databaseHelper.readableDatabase var cursor: Cursor? = null try { - val map = mutableMapOf() + val map = mutableMapOf() cursor = database.rawQuery("select * from $groupChatMappingTableName", null) while (cursor != null && cursor.moveToNext()) { val threadID = cursor.getLong(Companion.threadID) val string = cursor.getString(groupChatJSON) - val chat = LokiGroupChat.fromJSON(string) + val chat = LokiPublicChat.fromJSON(string) if (chat != null) { map[threadID] = chat } } return map @@ -115,20 +114,20 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa return mapOf() } - fun getAllGroupChatServers(): Set { - return getAllGroupChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) } + fun getAllPublicChatServers(): Set { + return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) } } - override fun getGroupChat(threadID: Long): LokiGroupChat? { + override fun getPublicChat(threadID: Long): LokiPublicChat? { if (threadID < 0) { return null } val database = databaseHelper.readableDatabase return database.get(groupChatMappingTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor -> val string = cursor.getString(groupChatJSON) - LokiGroupChat.fromJSON(string) + LokiPublicChat.fromJSON(string) } } - override fun setGroupChat(groupChat: LokiGroupChat, threadID: Long) { + override fun setPublicChat(groupChat: LokiPublicChat, threadID: Long) { if (threadID < 0) { return } val database = databaseHelper.writableDatabase @@ -138,7 +137,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa database.insertOrUpdate(groupChatMappingTableName, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) } - override fun removeGroupChat(threadID: Long) { + override fun removePublicChat(threadID: Long) { databaseHelper.writableDatabase.delete(groupChatMappingTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/MentionUtilities.kt b/src/org/thoughtcrime/securesms/loki/MentionUtilities.kt index d9f0643cad..38e847553c 100644 --- a/src/org/thoughtcrime/securesms/loki/MentionUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/MentionUtilities.kt @@ -8,31 +8,32 @@ import android.util.Range import network.loki.messenger.R import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.util.TextSecurePreferences -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI import java.util.regex.Pattern object MentionUtilities { @JvmStatic - fun highlightMentions(text: CharSequence, isGroupThread: Boolean, context: Context): String { - return MentionUtilities.highlightMentions(text, false, isGroupThread, context).toString() // isOutgoingMessage is irrelevant + fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String { + return MentionUtilities.highlightMentions(text, false, threadID, context).toString() // isOutgoingMessage is irrelevant } @JvmStatic - fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, isGroupThread: Boolean, context: Context): SpannableString { + fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString { var text = text val pattern = Pattern.compile("@[0-9a-fA-F]*") var matcher = pattern.matcher(text) val mentions = mutableListOf>() var startIndex = 0 - if (matcher.find(startIndex) && isGroupThread) { + val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) + if (matcher.find(startIndex)) { while (true) { val userID = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @ val userDisplayName: String? = if (userID.toLowerCase() == TextSecurePreferences.getLocalNumber(context).toLowerCase()) { TextSecurePreferences.getProfileName(context) + } else if (publicChat != null) { + DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, userID) } else { - val publicChatID = LokiGroupChatAPI.publicChatServer + "." + LokiGroupChatAPI.publicChatServerID - DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChatID, userID) + "" // TODO: Implement } if (userDisplayName != null) { text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length) diff --git a/src/org/thoughtcrime/securesms/loki/UserSelectionView.kt b/src/org/thoughtcrime/securesms/loki/UserSelectionView.kt index ab5da3cf7e..ec4a88dcaa 100644 --- a/src/org/thoughtcrime/securesms/loki/UserSelectionView.kt +++ b/src/org/thoughtcrime/securesms/loki/UserSelectionView.kt @@ -13,7 +13,10 @@ import org.thoughtcrime.securesms.database.DatabaseFactory class UserSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) { private var users = listOf>() set(newValue) { field = newValue; userSelectionViewAdapter.users = newValue } - private var hasGroupContext = false + var publicChatServer: String? = null + set(newValue) { field = newValue; userSelectionViewAdapter.publicChatServer = publicChatServer } + var publicChatChannel: Long? = null + set(newValue) { field = newValue; userSelectionViewAdapter.publicChatChannel = publicChatChannel } var onUserSelected: ((Tuple2) -> Unit)? = null private val userSelectionViewAdapter by lazy { Adapter(context) } @@ -21,7 +24,8 @@ class UserSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: In private class Adapter(private val context: Context) : BaseAdapter() { var users = listOf>() set(newValue) { field = newValue; notifyDataSetChanged() } - var hasGroupContext = false + var publicChatServer: String? = null + var publicChatChannel: Long? = null override fun getCount(): Int { return users.count() @@ -39,7 +43,8 @@ class UserSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: In val cell = cellToBeReused as UserSelectionViewCell? ?: UserSelectionViewCell.inflate(LayoutInflater.from(context), parent) val user = getItem(position) cell.user = user - cell.hasGroupContext = hasGroupContext + cell.publicChatServer = publicChatServer + cell.publicChatChannel = publicChatChannel return cell } } @@ -56,7 +61,11 @@ class UserSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: In } fun show(users: List>, threadID: Long) { - hasGroupContext = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)!!.isGroupRecipient + val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) + if (publicChat != null) { + publicChatServer = publicChat.server + publicChatChannel = publicChat.channel + } this.users = users val layoutParams = this.layoutParams as ViewGroup.LayoutParams layoutParams.height = toPx(6 + Math.min(users.count(), 4) * 52, resources) diff --git a/src/org/thoughtcrime/securesms/loki/UserSelectionViewCell.kt b/src/org/thoughtcrime/securesms/loki/UserSelectionViewCell.kt index 739e55d098..30190d47f0 100644 --- a/src/org/thoughtcrime/securesms/loki/UserSelectionViewCell.kt +++ b/src/org/thoughtcrime/securesms/loki/UserSelectionViewCell.kt @@ -11,12 +11,13 @@ import android.widget.LinearLayout import kotlinx.android.synthetic.main.cell_user_selection_view.view.* import network.loki.messenger.R import nl.komponents.kovenant.combine.Tuple2 -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI class UserSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { var user = Tuple2("", "") set(newValue) { field = newValue; update() } - var hasGroupContext = false + var publicChatServer: String? = null + var publicChatChannel: Long? = null constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context) : this(context, null) @@ -42,7 +43,11 @@ class UserSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr private fun update() { displayNameTextView.text = user.second profilePictureImageView.update(user.first) - val isUserModerator = LokiGroupChatAPI.isUserModerator(user.first, LokiGroupChatAPI.publicChatServerID, LokiGroupChatAPI.publicChatServer) - moderatorIconImageView.visibility = if (isUserModerator && hasGroupContext) View.VISIBLE else View.GONE + if (publicChatServer != null && publicChatChannel != null) { + val isUserModerator = LokiPublicChatAPI.isUserModerator(user.first, publicChatChannel!!, publicChatServer!!) + moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE + } else { + moderatorIconImageView.visibility = View.GONE + } } } \ No newline at end of file From 65f95839d9fe68eb269f9512dee5f22b3654ec81 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 15 Oct 2019 13:51:18 +1100 Subject: [PATCH 06/10] Fix build --- src/org/thoughtcrime/securesms/ApplicationContext.java | 4 ++-- src/org/thoughtcrime/securesms/ConversationListItem.java | 2 +- src/org/thoughtcrime/securesms/CreateProfileActivity.java | 7 +------ src/org/thoughtcrime/securesms/components/QuoteView.java | 5 +---- .../securesms/conversation/ConversationActivity.java | 7 +++++-- .../securesms/conversation/ConversationFragment.java | 8 ++------ .../securesms/conversation/ConversationItem.java | 7 +++---- src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java | 3 +-- .../securesms/loki/MentionCandidateSelectionView.kt | 2 +- .../securesms/loki/MentionCandidateSelectionViewCell.kt | 2 +- 10 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 582a88f59d..e06c634635 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -502,7 +502,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc } public void createGroupChatsIfNeeded() { - List defaultChats = LokiPublicChat.Companion.defaultChats(BuildConfig.DEBUG); + List defaultChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG); for (LokiPublicChat chat : defaultChats) { long threadID = GroupManager.getThreadId(chat.getId(), this); String migrationKey = chat.getId() + "_migrated"; @@ -514,7 +514,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc TextSecurePreferences.setBooleanPreference(this, migrationKey, true); } else if (threadID > -1 && !isChatMigrated) { // Migrate the old public chats. - DatabaseFactory.getLokiThreadDatabase(this).setGroupChat(chat, threadID); + DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(chat, threadID); TextSecurePreferences.setBooleanPreference(this, migrationKey, true); } } diff --git a/src/org/thoughtcrime/securesms/ConversationListItem.java b/src/org/thoughtcrime/securesms/ConversationListItem.java index 4cd2f799c2..d2ca8aaa4c 100644 --- a/src/org/thoughtcrime/securesms/ConversationListItem.java +++ b/src/org/thoughtcrime/securesms/ConversationListItem.java @@ -273,7 +273,7 @@ public class ConversationListItem extends RelativeLayout private @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) { LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(threadId, getContext()); // TODO: Terrible place to do this, but okay for now - snippet = MentionUtilities.highlightMentions(snippet, this.recipient.isGroupRecipient(), getContext()); + snippet = MentionUtilities.highlightMentions(snippet, threadId, getContext()); return snippet.length() <= MAX_SNIPPET_LENGTH ? snippet : snippet.subSequence(0, MAX_SNIPPET_LENGTH); } diff --git a/src/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/org/thoughtcrime/securesms/CreateProfileActivity.java index 7febb79499..3680ec4859 100644 --- a/src/org/thoughtcrime/securesms/CreateProfileActivity.java +++ b/src/org/thoughtcrime/securesms/CreateProfileActivity.java @@ -35,15 +35,12 @@ import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider; import org.thoughtcrime.securesms.components.emoji.EmojiToggle; import org.thoughtcrime.securesms.components.emoji.MediaKeyboard; import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.loki.LokiAPIDatabase; -import org.thoughtcrime.securesms.loki.LokiUserDatabase; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.profiles.AvatarHelper; @@ -61,7 +58,6 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.util.StreamDetails; -import org.whispersystems.signalservice.loki.api.LokiPublicChat; import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import org.whispersystems.signalservice.loki.utilities.Analytics; @@ -69,7 +65,6 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.security.SecureRandom; -import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -385,7 +380,7 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje TextSecurePreferences.setProfileName(context, name); LokiPublicChatAPI chatAPI = ApplicationContext.getInstance(context).getLokiPublicChatAPI(); if (chatAPI != null) { - Set groupChatServers = DatabaseFactory.getLokiThreadDatabase(context).getAllGroupChatServers(); + Set groupChatServers = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChatServers(); for (String server : groupChatServers) { chatAPI.setDisplayName(name, server); } diff --git a/src/org/thoughtcrime/securesms/components/QuoteView.java b/src/org/thoughtcrime/securesms/components/QuoteView.java index a3ce8126d6..f868c56082 100644 --- a/src/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/org/thoughtcrime/securesms/components/QuoteView.java @@ -21,7 +21,6 @@ import com.annimon.stream.Stream; import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.attachments.Attachment; -import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.GlideRequests; @@ -29,11 +28,9 @@ import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; -import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.loki.api.LokiPublicChat; -import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import java.util.List; @@ -200,7 +197,7 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener // If we're in a group then try and use the display name in the group if (conversationRecipient.isGroupRecipient()) { long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient); - LokiPublicChat chat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); + LokiPublicChat chat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); if (chat != null) { String senderDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(chat.getId(), author.getAddress().serialize()); if (senderDisplayName != null) { quoteeDisplayName = senderDisplayName; } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index b728c58c42..c392596663 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -157,6 +157,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate; import org.thoughtcrime.securesms.loki.LokiAPIUtilities; +import org.thoughtcrime.securesms.loki.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.LokiThreadDatabaseDelegate; import org.thoughtcrime.securesms.loki.LokiUserDatabase; import org.thoughtcrime.securesms.loki.MentionCandidateSelectionView; @@ -2780,9 +2781,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } int lastCharacterIndex = text.length() - 1; char lastCharacter = text.charAt(lastCharacterIndex); + String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(ConversationActivity.this); + LokiThreadDatabase threadDatabase = DatabaseFactory.getLokiThreadDatabase(ConversationActivity.this); LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(ConversationActivity.this); if (lastCharacter == '@') { - List mentionCandidates = LokiAPI.Companion.getMentionCandidates("", threadId, userDatabase); + List mentionCandidates = LokiAPI.Companion.getMentionCandidates("", threadId, userHexEncodedPublicKey, threadDatabase, userDatabase); currentMentionStartIndex = lastCharacterIndex; mentionCandidateSelectionView.show(mentionCandidates, threadId); } else if (Character.isWhitespace(lastCharacter)) { @@ -2791,7 +2794,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } else { if (currentMentionStartIndex != -1) { String query = text.substring(currentMentionStartIndex + 1); // + 1 to get rid of the @ - List mentionCandidates = LokiAPI.Companion.getMentionCandidates(query, threadId, userDatabase); + List mentionCandidates = LokiAPI.Companion.getMentionCandidates(query, threadId, userHexEncodedPublicKey, threadDatabase, userDatabase); mentionCandidateSelectionView.show(mentionCandidates, threadId); } } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java index a19beb5f0c..0e2835ed2f 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -68,9 +68,7 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil; import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity; import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder; import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.database.Address; -import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; @@ -82,8 +80,6 @@ import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate; -import org.thoughtcrime.securesms.loki.LokiAPIDatabase; -import org.thoughtcrime.securesms.loki.LokiUserDatabase; import org.thoughtcrime.securesms.longmessage.LongMessageActivity; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.GlideApp; @@ -410,7 +406,7 @@ public class ConversationFragment extends Fragment boolean isGroupChat = recipient.isGroupRecipient(); if (isGroupChat) { - LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); + LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); boolean isPublicChat = groupChat != null; int selectedMessageCount = messageRecords.size(); boolean isSentByUser = ((MessageRecord)messageRecords.toArray()[0]).isOutgoing(); @@ -513,7 +509,7 @@ public class ConversationFragment extends Fragment builder.setCancelable(true); // Loki - The delete option is only visible to the user in a group chat - LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getGroupChat(threadId); + LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { @Override diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java index c56b1d6af8..bedeb16cfe 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -71,7 +71,6 @@ import org.thoughtcrime.securesms.components.StickerView; import org.thoughtcrime.securesms.components.emoji.EmojiTextView; import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.database.AttachmentDatabase; -import org.thoughtcrime.securesms.database.Database; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase; @@ -475,7 +474,7 @@ public class ConversationItem extends LinearLayout if (isCaptionlessMms(messageRecord)) { bodyText.setVisibility(View.GONE); } else { ; - Spannable text = MentionUtilities.highlightMentions(linkifyMessageBody(messageRecord.getDisplayBody(context), batchSelected.isEmpty()), messageRecord.isOutgoing(), isGroupThread, context); + Spannable text = MentionUtilities.highlightMentions(linkifyMessageBody(messageRecord.getDisplayBody(context), batchSelected.isEmpty()), messageRecord.isOutgoing(), messageRecord.getThreadId(), context); text = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), text, searchQuery); text = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), text, searchQuery); @@ -791,7 +790,7 @@ public class ConversationItem extends LinearLayout if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) { Quote quote = ((MediaMmsMessageRecord)current).getQuote(); //noinspection ConstantConditions - String quoteBody = MentionUtilities.highlightMentions(quote.getText(), isGroupThread, context); + String quoteBody = MentionUtilities.highlightMentions(quote.getText(), current.getThreadId(), context); quoteView.setQuote(glideRequests, quote.getId(), Recipient.from(context, quote.getAuthor(), true), quoteBody, quote.isOriginalMissing(), quote.getAttachment(), conversationRecipient); quoteView.setVisibility(View.VISIBLE); quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; @@ -941,7 +940,7 @@ public class ConversationItem extends LinearLayout int visibility = View.GONE; // If we have a chat then use that to determine mod status - LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(messageRecord.getThreadId()); + LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId()); if (groupChat != null) { boolean isModerator = LokiPublicChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), groupChat.getChannel(), groupChat.getServer()); visibility = isModerator ? View.VISIBLE : View.GONE; diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 4532e619ca..2f798fe3ee 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -46,7 +46,6 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; import org.whispersystems.signalservice.loki.api.LokiPublicChat; -import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import java.io.IOException; import java.util.ArrayList; @@ -290,7 +289,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { // Loki - All group messages should be directed to their servers long threadID = GroupManager.getThreadIdFromGroupId(groupId, context); - LokiPublicChat chat = DatabaseFactory.getLokiThreadDatabase(context).getGroupChat(threadID); + LokiPublicChat chat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID); if (chat != null) { // We need to somehow maintain information that will allow the sender to map // a Recipient to the correct public chat thread, and so this might be a bit hacky diff --git a/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt index 878c07cd93..fd01cb778e 100644 --- a/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt +++ b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt @@ -10,7 +10,7 @@ import android.widget.ListView import org.thoughtcrime.securesms.database.DatabaseFactory import org.whispersystems.signalservice.loki.messaging.Mention -class UserSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) { +class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) { private var mentionCandidates = listOf() set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue } var publicChatServer: String? = null diff --git a/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt index 36f571bb1a..c8bfcc1f81 100644 --- a/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt +++ b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt @@ -44,7 +44,7 @@ class MentionCandidateSelectionViewCell(context: Context, attrs: AttributeSet?, displayNameTextView.text = mentionCandidate.displayName profilePictureImageView.update(mentionCandidate.hexEncodedPublicKey) if (publicChatServer != null && publicChatChannel != null) { - val isUserModerator = LokiPublicChatAPI.isUserModerator(user.first, publicChatChannel!!, publicChatServer!!) + val isUserModerator = LokiPublicChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, publicChatChannel!!, publicChatServer!!) moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE } else { moderatorIconImageView.visibility = View.GONE From 4a613df52d1d919dfb2b7a41e7fe0b0ff0b3162a Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 15 Oct 2019 14:32:23 +1100 Subject: [PATCH 07/10] Clean --- AndroidManifest.xml | 8 ++-- ...network.loki.messenger_2019.10.11_14.38.li | Bin 630207 -> 0 bytes res/layout/fragment_add_public_chat.xml | 6 +-- res/values/strings.xml | 14 +++--- res/xml/preferences.xml | 8 +--- .../ApplicationPreferencesActivity.java | 11 ----- .../securesms/CreateProfileActivity.java | 6 +-- .../securesms/components/QuoteView.java | 6 +-- .../conversation/ConversationFragment.java | 20 ++++----- .../conversation/ConversationItem.java | 7 ++- .../securesms/database/Address.java | 2 +- .../securesms/jobs/PushGroupSendJob.java | 8 ++-- .../securesms/loki/AddPublicChatActivity.kt | 11 +++-- .../securesms/loki/DisplayNameActivity.kt | 7 ++- .../securesms/loki/GeneralUtilities.kt | 2 +- .../securesms/loki/LokiPublicChatManager.kt | 2 +- .../securesms/loki/LokiThreadDatabase.kt | 42 ++++++++---------- .../securesms/sms/MessageSender.java | 4 +- 18 files changed, 70 insertions(+), 94 deletions(-) delete mode 100644 captures/network.loki.messenger_2019.10.11_14.38.li diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 165f1eb2df..7b88684d51 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -477,12 +477,12 @@ android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" /> + android:windowSoftInputMode="stateAlwaysVisible" + android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" /> + android:windowSoftInputMode="stateAlwaysVisible" + android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" /> diff --git a/captures/network.loki.messenger_2019.10.11_14.38.li b/captures/network.loki.messenger_2019.10.11_14.38.li deleted file mode 100644 index 83119206336d5a2fc9eb626f5bde05d3a23eb5e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 630207 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

tg7K_1tI{P(LsR^m8ePOE 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 anvNn$7Q=0#W+e-BF@&|mqp zl`ssH(gQj1>7HQ$M9zww(fZoj3b-qg(i|YN1U|_-bl_y}R5JI?CdCm3D{lb|%H(_u zG@AgGfS82infQ-3JmaKw>?gWO z+sS3KRjnB~#67HZuj0oSRhx;pCo@RD2x>iLdn}6 z!plVPCpG&eS1W<><9FK|xqTYn@@T7aOwZ{Qgd25liSXu#&CQu}sk*3flGnaCRrN&v zN4{k@h9fD=l7H4{B>T3!8!z1;nv?5Ld)$}%4Y0vU>-xoKfQ zeI+171(A&opgA(ddhxL!ynAF-@-n{J-y)?@TU7Z%4w=BaI zt>j$6+PEAR^tiJHPP57A&B@46xg9hf3#Ehl$09?T$Z>>CIAS)-v9m<4a`xgyG6l(e z&JG?9($Fg>+!x-?Y(rX7Pbuy^hIpr>!S+Kfd>S0=j;`n&_orNGyn$GeuLGk>9BWiG zY&iwv%FQNuS?1`vU{W=J-d{b5jRLD(;@t+b8?raY0bGd*@zLiyA+i_9sK948)=JoS z*eE57+mqe_+gM+W6#)Kw0Ys4R|AWyR?iYIm1vet0EUEFn=cvR{Di}uaZ+teOHEUBD ztVw^fWV6EE+un>W=B4x8!ly*92MugA{j;0(_T1*?sFa!qIrocu3kn}@%Ia@d;cea; zs56nJuuw1~I~nj+Hssa=i@3+^r#{rmd&$3hs`YL|0}X9nV~c+upR{TzKKNxIC6<;5 zBZ+St`vm3_4c*RN_or6vAk6Z3wT$Hf!(TMYokt3&w&a9u3(oAYiW|LHQ|!J&q3A`I z){xhGr@!^dBG#FEO5A(`f+L=}Fm4(cnmnvnOGFFL^u?VOGTIIloR(H^!C_9QyKXNY z@_|WKa=jQw;ePxVaOB@z+tO<}edw@Sv}(JZR-np@nWo@srPIEqBUv$1FJG6|Cx5Jw zxyRFO>F$$az$&H?%wil|6}>Cv&!d5(8mEKT;onXNs4+D*O2uY1%tT5{yGF}a0c9W- zymtTr8*yvKjrzL&!Eo8$XW3^y>P^!V!8#ewpVPj+G`I7-)3PC!K700@+=Aa*V?NrxWgRSTADB3M1#tL+E=%+raBiYs+J$IUXBooFj8#M3lKaa`nqEl5$r!8YM z6=_f8WNdjF1X(cY9+|_Gk3rgM^R2%8_1%8cBiN=ee2Fw$wS&Il_a;kB*&{ATPr7Rx zzK^itcI3(Zv#Q+GS(xrqrg`;2HQ^hYgz6T*~uZbCnMdhO{Zn-@z{vVxp5lG_3=~Gau0vSa{NBO zDZD|~=jwBs7d1!ra-29gwhxk#hfhwHx4LX$w!CezszLCCPK@-XH1*0zHfdEZV%-2m zFiEYz&?tc;7`P8DDhVE=J~SO94gfv>li*-==ZDvxf_-9uKjW5-1#ElL5q!kG>$5jl zChqXI(QOy_l-M`lpL8j$Q~2R`QU3UV$yxZ>ym+diQ9jmZcf9iS&>97U*VjsY?-u!V z{IwsQpq}_afn@ggitN4A)EyL}WP18*j9vPX#hRY8QOPSW48G@C4UMI^?dGfcqWsL} zzGt!~s1*ML_d1LD@@nzfMA_(Yue79(N~x-vJp5{`^pwVYTX3tmUxD-bpxA$(fm0=5 zufx}OS;E>2bWc(zo)$eWuReA@^X_SbqYZnr}+e*5(_|M9$+sM?iHOhJ0LXqIWt!|EPaO5%$pt}d!_59JPs)_26 zc&npyw)kcJGfkpP2b@p&UkO;7!OUV3J*1jRo&yvjdOYoX?_0|Y(`gzj_Xj1j2fuW( z+*Wj6b6B+LgfvrP?36_0(JhT^{=WINJE-C%4ey+2Kf$QiQ10;L`Q_A)O5<*_;>W#4 zk3V0E?JeLB+SB!Q$K>y>V-VzTfGOek@GE!!k2Ampn?VEdxed1`b{R~ti!QdEmKdh2 zv7Pj|7-t`U@?&$s;eSP7FNd7u6*ErjfGO#WQ%zdFFF*1BaL`y{C2tM`gVGH=7u$F~ zD7M>c9h5y&6QB0~Veh@enoPUBQPfe#!LEo>RTPw}^o|8VK|sZTp{g|L&CrW8j)K&o zOCLdc2tw!(P-)VocY?G40YVWlK*Cv1eD^;8p6}axpX)pC8Lw+Zo#ZL^z3x?h>$lb- z=cT&rT{$##(agA>fMw34j5hr!*PHd1+;wxQA(*9p^E_?e=Ydb!_q6mzg=#anPjTqx zKEKvsaOK>mPd_%sk97E&T2!vmZ7vw{YtF>RNBWV6?8Y{W*&Q6TYvelFlY$SfvF?@c z##teyidXJZvRfs+9I_l=pW{1{r>LxFz$T9F#Q*(6>V8U1bpn)6E97kNh`&ShCY)@Ioq&-TaR5>3l6xBGtj8HHaoa}Ab00gv>nq}4Ct z+UE;{zlXj{pIeDP&e)T%mPQzFm7FTzo62@K?cRZ2=I3aCEv2ASRo=f$g{|!y^Vob+ zxbFKKsFDc+b?KMR)#JaIS*4{8*#2sps`8Be?}@IE+k~{6ofn0ED>#&Q(Ma?l-{mhI z6Q68vF4YJq?^0#Gx%58M;&rOP(N2{^uEdqqq2BfOgr5hcg)g2-b+l&A5ibzM)IGQs z@0lQWr{Zg8Uf$!1Un#;4_sajyvMu=C?~w;@z2M}T>nes#l$Ct8Z3ML9rgJ_#_`SJY{{8Q|tS^S-Tyc~asLuOMkb5{P$?{({{F&l#`a3BnQ!-6{Cq!TA!G8{D z_&tm3Ja4g5ROz8t9>I@2({5a}BF@%wb~e14<>FaaB+j}@YX~y-<5;XJ_)|d_YFcpw zPlw2suyY1rPv2&8^A`KQ^<^#LO1hM4_}c|@T5tv0A}y4;YYV;VjWR`lk3I48`%$Wr zcT^-~uKD1&G~0fwz+46->e4{hJM}NC&seV%1SK6!k8x~N`ZiBXR(t;KXkXqX_dbyh zob%<1Wsol3Q0J>_u3cNYV{WMY7Wcnj@n-P*e*U>Ux7)zpGrcGr()&e{YB7CS4kPr} z-0VFf!R!^WE3u{Y!5S+=3FJo0YIYnxz98$m!}{Io$|1XusuY@dxj};FuulD!sH426 z%Lvn7(H}?u)z-mO5!BeO7qx+RWiQ{#5UHIi+!LNxm%*(PB1s6)f9Sq3ac1mam9(pk z{2v`RZv1ZOeB*TV)D$*zW+i!3W3u+>#?nz?Ek0W@-h)DA#Ing`fge@;o=N;Am7edI z{&k$E!%;7IX2VYn?oOoHRW)mTK{t-6lwCjBwNMd{+4W|C^_QUx67f@7p?B1;gAZ4{ zP!8zKj&rleJ;_IZo)qdy;D&p6N_{^5a@zAG>D%9Iv!lel{cA?A3Gs+u2){(n=WuZ_ zIZ$>d&-Xpwcx(MHkr2F)gMP>~wwdo$$T z4_KhVLm$eh0yOr^y@5H7Vg;I;3L|X;9`q>n=Lo<9np{sm@+v}M~~ zXa-m89k34FfO>=Y2oOX;cnb+yq%#%LP|ye912(GM8s5ITyibp_D{0DxL{zPExg z!RZKO3BDf_6QFbm^n$egtfT18Rqnb5sXt^!)3#T$uOu)Y-axi2pb1(MgZvDzLvLq@ z$Sz(Qf=U1gO6R*%=2BDo5Z=QCaBBa2`SK+L+XP&&_>NqoJk*Insz(wT#Udh-Ct5-P z$O-TuKZJ=Hpry+%d>-N?M&chH`~GZ7p}3>ehYXI_=B{ z!BCy#IQ2LQnA`4E=z+unpAFW+WB^g0wZFrGt+UaB^9qYfHlh5}Nr*PT-z3G8)*qCC@^%Z|+4 z&f*G)F8EIB4_~mQl@#+rOpCPZLK+Lg{vC$yS$PE}Lo0Z`^UYsGKZb>6f)YQpJ5Kh- zJErd*4(L768}#0@B3+|OhNfA-5P1iVUi}&%kK2IG>nPL%#wz%8ybe5Hk(HeP7fE1R z{#oVCPy$}84o|rSC9&kvGP zFVxi53Y}993SiqC-0|$-u9y1b``9jSX6Kn-eIpS5>+4eGqXy?Mji37JA24orHs$GG ze>lgwh#kOyQ&&NT*4gkep4oHEZo`IrSWU^Q-;B%>f=yyOPQrJZO z1fR{>`~mZLvtEU{%+&Fi>TR=NZcJvN)kH-xt^z7ekh))Vb3kCA3DgjW!orfeqr}>z zd4N;K1>F7zwsyQ9e!s}>aM#-%?S&^z=cBtk*egx$cC94~8$Ud*ZG6h%{WHLK9cvCr zo&0VOBlIpoOCOeMYO8&Akq@xs$K#3H=GRLO^h-mt{7d9Ex7s=G4#3_5V4#rg-?`&` z)pX1}G^;8vW*%N8LZRliw3Ok_ zY=xFcFsj-4uvq*DD2BvRPiRWN8>B1%$|?SA$(L$NEnqr-UKr|+W`I7{TL9_wdf*yx zQz}>6_6_pG2!n*K-xqc`7rDvl>+N<6U z1JEt`_0%qQwE`jO^XGXL$}SALS|49yXRUk9wpR0;mXQ-3RX?O9d?4EbY6wELwG<>| z0qP#ItFZN&U?=)R`WWFpBXs#t&@&Bx9yqbGzJUYsP(Pry_-H7jlU#&-^naufeiMkQ zqIp6_mD0k{=uD71|)(2C_)lJ#0IiqHn}lTp|tH9Zu(KV`42bE>A+48ftmt-mk~^#G%3M31yn&k zZGA`;u8v0{#1M+$7kfv;EQG(}ngoOnPu|f*`)&a0T z#3i?efh5E3CGH0uc+Kca0sd}KXFAa)N5MLb2$c+rLd*E%A{)qzUxgOyYR$%dvhGl( zWWHH>$v2}42x(~A;;_q*E%MOWuEqO&e$;T+qjU|Uim*1dkwpvk?o@f*$<)df%xcxv zVjv)7NYB`DJ|*g%dB_3-OMCTE`EyGc-lmWQK|Em2zhO;w%Mc!FDgu0 zL@Ejc2{ic9XQ5sg0x2f!hZs45zmZD~y%anz7}zUP;&wYODr9(O9` z-R|#ImC|4}aJc@e=7df!wxApYokNP~@LVjj79zbh329{8?E=jWkad@y2oe)*2CjEhQOE};*q{cu&1rp` z{99>cI1+qzmCN-o;PP4;S^U}8v&fNP2o|_7)+39MyH$iGJpmMzC!d=6BS08I(XaqI z)!0JxT5Sa=-MALSM1R&H1bMM|yYkEQB|jxBW-CNty?=&S?b|l9%M3<5Ok`yw1;9#( zk(bk8jzYa_)oP+zb8CZ7%#QCfn7j%j5|1NW)9V3Ty+-vh-7tW-NW)X3b`=XwapPLC zq8D8TEi+oSfweW!BG!)m&;kQ6rnmhor{k>0VQpH`uHNKHU#O|FN*2(B;dG!-__B-a zlzT|uplPu*m*;$b0=E(*5CTA{>ugab$p&HVD@}c^L`W#)gYc*TzLpA9Blbn;Y@r}r zF<$_n{lhfhlB1-*n;t$aK{7c=on7`C9lGh(!Ue-(cR*C|RePHBliLaDYtRjnPJT$q zSv8Cwrv<>cnXVdM3#l& zGv33F4V;h;`K+dI+&#TK3KCN=4MwKf%8&in{nWN6%xh(RdoeVx1pA7`a%)YCPuA9J z5rg>~w7oqMULW(_G%fFPRtv7rRib+Yv`Jn|odu6XrDj!>i^fBJw@oxl^PQ%e+(?P8 z4w;Z$aeBw^zR$9FjLm*wLgTr#^!%$d~eUK7uRtrsOC&^^WN`kq-Qf~yAN8}-z@&_cI? zl%Gh*Equ{vW)qmTx0o1PNDIS0g(iIXmgqYAhH(THAJEkuZL4St_6?9yeu{6U=iTZC z2Jv`Gpl|@BO6@o|U_u83dce6p1p!b9c&p~NCRE-`vnJwYLC++wR$A6w7&eU8`w09G z@iv8xVQiqEe@~+0Ar9#))@UTrm*g4ZpFjonvoz_vH@IRlH;KJKUpov&_FZBehj*xhRMYm49&G|D2KaM8b!6Jw2<-<#P~_ z4rBTZV|NxoJ^fg)_hq+8#OUah!j!2$O255kcal9O{8On);O`egr}wwte}wnvI8QvQ z(yqBABov&Ia=uGeK5=6z+iioQo}>67z7)C9uq`q@P=LU`4t{Js9E7($mmdPf^C{x9 zL-k7Tn*Bn40~aF#Idv3lgCpXuz|`wXF3A4o!o&e!7uQwl0&Ag6bKOVL0rs**b%?r zri%}F&IsEpx5zhcu%7~_QVxlD)QpM!Q+OX%*uaDb-&Vxh<;A#setdJA2$sqgEFLa;I{;kgz)SiLMfEg-U z1`I_hxI3b&MazhpTlO+w2Uvs~2vN^p<)yjId;4B*wH4phkR5eQWCVXjv(&u#5Pf2# zaBj9#j#W9UDlq#yLkS?3o21zb4YYbUlU8=A*3O09>OJLvG^2s4^MtO74RGI&gR7MN zO^2j~7WsBW;rl%u?KTMV&hd@SK#i9s#^dqEf-USwk{q;pEx;56(; z;GV1j+3C;vU$fqXhF%4`L&}%R?4PmE!P135Fa!lY>cfa5 ziyS6$QFjP|B9d7a%nL&O!>hCD$5>rje&08ZluV=h%9d|?Kg~w6M%S`ZaM;Zk;Sw!| zwR0l47$XnMTuQ98r{=n44Jj&4WJ*@&K=t? z3qzR8A_f5LDTRz^Do{8O(W`?|#Hb559#(Yo0-w zs1cZuwM}-6W3_fn`=;R#;*TTtyIh|DystWF^Kv@Iz^PG>7*8pG$T~L-|1SxT^~!h& zODNz``%X6WOs}%xk707kpfq=0s5P#P7vwG(UkP|Y2gGGajE@ko&g=Ab2;^0YLdg`q z6wHGdIo)Mmd&nr0Su*O{aZBLvH2dc-c?( zEc}9KN%SsNs(Hp#)Dfxk=Yd9c8n_lM?rp`AQF}R#9n(^znr_cqC^fgh5-6N-;Baoa zicb978#F7rJw%7a;bAw~Q$x-}fQa<~ZZon7NUVidBm$?ce%F4Eqx(4=t|ef4(NIRU z?FO27T*Q~Ep7sq@)x=-xGp&!#Nfc!U9E-wz_wu}})e5@WS|41d2@u#IV7Vd_$i(b*}%+0GzsHPKGrMw?p#&N@A`V>ZokuC0hd?RmOI#lR7{C=qM2*}fv8u&# zatbo%%w@h`Ez}TTiShRlngIy(un4iZni9n;5|v47$yk}n10aCVeD_JUb|i?FK4LNu z9W@SNscPF&7=pV2Jb)UkR#v8y9~frF!`gm97n?6>r%Q&qQm<5dssABrR$~{vlb3z0 zdhRnR>5m%;q0E+Z-C40!B~9~ie9@t$|0zdJY)TVKIpfKpf^S}Gja1G>Wl7{6(myG- zqwT2rQjs?I6ve&9W$DUt34b8_8+$KHVE&yazmrSD;o8u=DjSUp? zTZfD7MNJl9XkRSBa!{_-`=}^&R(HavOSqr|=2Ai$58K)Zo zj^T2rx=5Xo=K>2rWqQx&`>(D;pP7~`c4p8w4EmKUhnfTm75f7mRdm*j#Yt(^tRrRq zlo7SGR2|kU3fAixbZPK6-X3R-r6Q*af?^6nA`)vxf?Xth-u}^`g!>TW)t6R&PpVIz z6mxlJL_57WWCN;$3gQM3@4tB1soARJDOL?msP;YoRmXRz?zut$ienYc9>}d{*@|@U zWj8~-<>{M&CVm9=dd&qk6hUa`)r0e2*XH09=|$ zeDeZxXC?3ANqvWbp82fRn>~5_oVnoMKqQZY;H+gE@feYH+Xld({>dEH0P^26DdRb896%7gYQeZv;x|t4Ls2bpDjAlLgt-ck#iwh+o<`xWX zghVx*IT#?V1@viIvfmI$_Gu(u8}{B9kud|8sOMFA8R)zz!JIj{)UOSmzT9-*mJ7p?U57ULc7B63_rIvo>w+btqZhhAs;%zm=jQOOp;jgA*S&R2II2t%@ zb@jtLp|@rH^@qREKEJb?q9~sPCw2qe_M$2hoR}u+H|?G!Kg1p)m?X6M;Dq|C#&Tabm}7@t(1ah;j5-!{!-ggm<7~h7}#7;9yumG z6{%fdB;ea%2Kl`}0JgE8oe=mr;oNYq*Dsfo8DJ3e6POzrYmwFr_1ua%GTU(=NvtqY zBBx-uyow0C?NCS-^8H-RCp%IA2nB-k#bYtk()8M1P)V&GmN{}QQ#Td>qK0Jp_YD3L z2_i=H>6)W)7a+vCz-M1u9FZdn+t<#WdJ3(pY}?Ag=Dxzk6E}NH3O2dWT^dp&C*>9j zwR;NyJ~C}(9e2}h053LNTci!x{0PDclS8nb8;M}~Sq|r-jm{||$nPOTO3s7KRmXGS zZ1W?qyB~+s?cNp8Zt~Ews|;(F#kqBeO2bTssn$c-cLGTeanoj0W%5ek3y4s<&Ajs) z2hMT(fFBHIK+Nqp;OF{?%T6v5x@U+X3F=)-uEvWdu}dqJ8&uvp*%Nq4=up>gEgrRr zvfo!HR$a5D<{Ii3A!5cBOx499PNEqXRB-j`Pe4nBwz*sM>>OMG0%AgyTO%U9CR+<7 zp$Js`ehq0Tgm|#z!d6f%Z@?Qf$8fnKfs?uj;UpwZ)@7q&kAS~&7a6dD#4pp8Fz)ID zMtB1tFNP;zvOTt6$=}dr;39q=NdF-SOp7c9@W_I~!^5G@6ou#?$J%46%1~#*bAB9L zXMadM)xp6L9dd-&$zm}Qfr7Xoj|4N19EPEC}?_B6Oee)yaX# zG6ji1$H86qL3+qC0BFcKa47xwbi_$d5Bg!{bn56JkOZV0CB%r(y$?Zg%f&L-$lRtvEv2aV17;4)_OKH55ZT?@mHg z36g2RRwsg=%#K+tC*zP4A-I|djXIj8XWvI4^-*ZDkU{Ac1hXo_9&M0(avRvoM|D-twKb5l zQ>?SVwj%RROxhJTunx6_0eTz|_DOYb^g;Z<0X2l}99!J5oA>jK2yU?5;M5R#LlxE^ z&4br=>{tirv3{@{VxPPamkwg>W-sGJhtCJ{;VqQGOSY~Qx_2U88Sxc9l}>d$C%;pA3J5Yfw1yHqG2YnhQ{CPrv?Njt>33i+0bsZ+x zxf6U|w7tE`gXk$FEh$>lu7a(SLqrI>MnHBJfk$Gg2yltuq@YfT3v{xpyq`RBbb@&({ zYi)Wiuq!%)@80 zaCN7-2E;p;8hH+O0et8iy{(`~lq@d14uC!c=V6hE4}a)Yg}Q1Oz}U$SsC=;MX_knB z8FOGbCo)q*vc*jsH%LZZ9tOH|-l*Rk6@}kJy-fq4V=*VrVlNk&RG+J==9jg{mf3(Y zl}Y?9PbP8|AlRr?52~W!-$y007;&#LYhClrQ_6SlSVG!?=ik~J14zf!M0to^AO%|BWZy?sJMY3c?^s%fr>cV9gGA>UcDf9*o=etHt#;n}l z-|zP({)J@LVZz_)gLP(+%AlB$PtE@uiWC9U;c8hdhm;Wi_|!2u2rC zj9gIfqt=HmvO+Q3dx&Hk&R8VepM7=vr%TiXvbkCc&Cdkp<(=ySSddI2G}p35lC9(- zFbZnTSn?JDGBFIQ0VL#tp!lEki@O;dY!*o2y+IPJdP?$8A$%qy!AY$WhL_5)CLB9v z>_au%UUaQS9O9zS-1#hxu8H%2*vbP6&w|s|-_-@BKWE@ScTLVY*z-30JYwmyUPN!8)IST;msa8q)m*fg3<67WF!Nbl zDSH{pSkY$OXQ}l6I8KPmwfYX4a3Hu9NSh_YzXrKL!mym4bGB9uRmIiS5D(+9$D$w^ z7u|E-95UyQAgmU2-vlXpBM85QV*B{+zC8+%hyC;#c){3JldgLwZUI;oKPf^}sOq{D zLcfL2)K%KR?UB$e$QDnUl~PJ4jORxVIsqiA3cP`*%|azXh-?9pa~$fCZX}kU9Xt(W zjB|r9>2}Bluc3^sGFgVEF&4q1_YmJX1W7-D%0u#NfruDDm%$0Zf^d#}UR1%HyH)G> zdkoMUa3W3w@_Y(QhqS#hT{I-DG6|G8` zq$r?o+Z`{^jfgX`w#dV8+>oEXCY|PCoVmLEI(jfaFPD-B*->%GVBy{1bI!7=!H-6o zY-0>v-awjJr@zw*=o@c9b>(;Vog+#PAjh%qC3}6ocZvV&_yaP%4FPFsSEtk6mmB{a zmb=MB&IZ2*6~fdFb?8Wbq}TZ~xP1B~`o*&tYi8-khBDlHvic{v?xVXJ>~sC^@5u2* z3kkj{@UJN}@4C3EKHiZ-`c>*hC4}#Z<4BTY+3w3fGWD zQ1j(vAXuqS*9KV%q+B|$#&b7EhPFbRy1LydbJck;8?&{viyvZ$m!;ser9kgtgd9{0 z`EP`+fhI#rmdg?RfLblz%`WT|lA=fAaY-rG$Ej0Z2j81VoZ)q`J=Mnl#xL(JJEx}| z*M6_+JIW_>xmYH5j<{RJDF~raZ1%yAIP4V*rPY{+Tu**DAPYUX#aO=rB@6g|eRoM1 zL=(Y2^uz0<0FHyE=$AQ}8GssLQRF#ztrb~~fk}Ob$jj(6%PV8ImVe}t>c-11Vp88I zb0!EwXv{?;xDVy@*@Wa=Fml!rL$5a|`$EHyvE&{nqq1!(JYPzWe@@RI;R;Rx8wDe&B^zg3ECt5P zhJYbt`I>vLl`&+a#!Dwc0s%6R0yyLt48DhAkYPj`0QD2YxLmr`e-sDaXFr$$u~j4; zdXPifVUOK0_7fzNzi}rV9=^*oQm&Yg3Jg(*Q%N1y$obT~Qg?BAa8Dq>E$Z?iyP24- zCGw7CrjJjrjel^K3{^N{{a~%>jR0uu%k3h8y03);>YOjIGchrBd+!c`y6p7p9M9Dv z`Uxfzm-w@F`DE!WeTAlGcOaQ9&KsgP=Xer68h(E4k*faUBc;LQQuKUR+8yy{k?W}f z0x>bQTkXQZ2LLjois{(SwC-YJdck(W0h=){M836kBEFVx7?^tB@xI|Rh7$0 zkGnXNcJ*7wt$foYE4cc#@?SXI3)gUKCKMj_hEY}VH}LReW&is=vi9{&KibusCy4DL zZ}OCS_4dH^gI~T#WeS+PWmBY1>v!K#$v3G`$)E5!WP(p;pS~KM#;iWR>Z)5yqTu}7 zd4)prl#DEuk(ZE^-5sJ=u&{XODQP`8Wvc3sN&E<#GXB&WbEmQiT{q|HbRo+!)l*;s zf2Y}`S5=mIFB9~M^NjO+2-hb( zI^0%M{QVylXNv0x3%p^0jQmuAKTETkNn>UB+tu0ZADa;@SGT%QS4mx{{HIwp2bh>X zrmKF01!KA(vaqay{NV=UuQtE@!hx*H%0EB;S%QBrDPSmpe}wy=gZ)u^+oZ)^@=LW7OC1#Ow09a zVp>ke?gP6HoIH2-bNI2xCY}RVU4qG14TaM`<(gDjcJ@St%XT|mHh8I6925^&y{2-f&aRO`DZ!*5zPNF z^YxE#{SONlLkav3`5ynrZeFEMHL|QPnxB{x@3|~;H@3a38OZmQt~eZQYLgwc{){Kr zg=gWLir&rO!q+)LK_w4RcMSQgo8t!z^6+IH>K25UR!xz_9wVJolFp0$dTq%D?eoiP z2}=3VWnJtvx}-Z>543n2p`3%ej1BaAyGi>QOGnp;#C=I(e~l*(hF!qSU0^2Z7L@ll znwLbVRA{E6GpsJ4#4N=lR0J#wv&HrAxQfl%_RiY!66_whMwG{VFGqPK^5>e%rA92= zUEolUx{&3xIHSkq>823s<#IXH$NVBG1h=tf@o6u^dDI`pS^GJSFgG~U0vnuv+&Xb4 z+0xwIPAr2P-J+5a@ow3u-bLpO(S1tu=_5sA^Gca)VwB43!G(E(kLZM0u3#duB{f;U zEj1Yp{~5MXCv@DuzeF6-kI~}zyhORh_{N2eyB2-#zF`Gzrt?)}3d+lq!e(XV@~qQ4 z=80YeJ16>WDeF~Kef?`ZX-QdyPhZB_RN;uvW8ou}zKW|Eax`{__+yCk+b^oZ7hgsj zogH>*e~u&Aim#G%ou~Vq;%Y|bON+zIbLKYuwQ`^(&-ybF!kRg*%;mhcoQl)rryj3m zI#qt@cPduMWlLfF8pD;IsW9w2JcZ-#PaL8owhgYBYbq4Ht#{z7u(wGcEZ3N@?7=uK zezPmLUYM_bB6>45SAL0J<~44d)I5Jc{}JvN#3H_^Dq9T6AEweOoJTl$WY)es?Hv8m zR%50ToT1&KXMSI+-d)xb}Y>#k67=*rZw03FVKx>g|FIK*qC1Xbi8ppoN){dB=#ps#Yt_? z)9)_WP0i>|i#i)7hBfZe!#Rfao+7t!-oeqLIb-FDVmIh3jR`-*1Z0I@G4_Q;Djy%^ z>FcCr=U%6*sVE;4YfUbEU1Hq-LnWS!nGVqz@TJ|Y`kr6sHP*-7+>}srJDHjBzyb^< zklvxTxLsxFUKj5|nt$^uh%$0gGU@ZQN8(gh?);!qY$z=dPwn!>C=yzIv{0vheserM zCYU6lR3x&{VHd3%En4Qf)ZSnEFfepYR8cmpb;yr4H!IrJ^qC8{;y? z5i5l==wnjC9sMCHqUCYr`ANxcq(f!2q%7pq=ue1DUv&3UZxYJmXg6sfiF$wZiEOr| zD<>-Kw^$kD78)#A5og3@N%;I(bDt&Fyw)F!vgDDdpt}L=x=ou85?v&sqnuInI$zHV z)`0}%vcDqXoIIb{){uLbh+({uq^LMPtSKZ?0JnIN-TCI7lEfVng`;)b(zQ{sZRiMy zoP}+>Mh!LJm7~p^5dAH2yuRmwMEJKv_AGre<6o2FUvDXNMt>9f6`mLGL^D3q9J)P0 zE+{B|+hM44^n1;hkXBe3k}7=}Q?~vpmBwYE{&U5jm5j*6a=UH)E-2V$@!apkkxk3z z79z~d%+ycD@^AHb29>KDX5}o92+iTX(I!ExY@@Y8OsV3pZ;d`$H50w85wo1y?mHE2 z)7sTaeH6#d`rnW!HK~I$FR_VWKN|IFucJxMnWVd?SXozW?FXVjIm~THd-EVyT+$eM zPI5Ea5$iMcak+ZagqHL=qmxTx+JbepJh_Zls`9xfJ^bhD_I?^tSt0Du=z@ZRdS&@_ zhqQPDnFFb0UwhxfBZ{3AN1QA}>M{zqmhM&UaC5Ybk3W5#_yqkY^AIX}rQTTj?MUB| zHP3_`wK2H!m`LmZvFQlC^AjwKgi;y9?~ibv;71y?(q&3%_moG@Tvs8y{MPNiz8RJpl>8lZB1i+TJk zJN}IVX3>)@LF1arW-R!Ra@HyzQQ}H=$MbS)8rQ4lzq<^ZnHSmH7xK<*-+ah;+3*V$ zIBSxGeHky_BBqmTfNO|tpP}dbrs09K%EgSlMjUyN#zhLquCK38I0YK%Lu-+0&UU|& zOeCl>9<*@Uiv=zJ(4E$P zc+s5$dH#=*uH=!4GU}8Gn)pcF%%bU9b~bdLyx3mh$xs6FC6!OH@Uq8#ez=znw_>YQ zB*0f)obc@c#^kTyR&l4fu&Lp#=n=~!M@A`n$UXb(jn;VNI@#`|A2~Q(c2ItxyGpiZ zqR~*PFqN2U;PUfQMXUGZM5swwEs2JA@Ct6pGpjKYL#Syk{YLej&4=k5C7TLuW)~5O zvC+=k(yzcfxG0G2muIu%6Z2z3#~HR;@8pcY5*Mrp?}J;uw%aOR<|wz7#^iKS?$5j| z?`T{T;cCov4c+jJY*t}BmN{GNGJ% zTUU2wMM{Hjk3&7yRSa2Gr&Yt$0ml!CZwR!cKXV^@xh5h;C|yoktRg8)! zb{QKs&J;FW5AT>&(e%d1xDqVOVU0v70Mfj$9WQ4MH3cnjCeC4fpYM}rDQ%T=(S3-g6%`b!tAr7Sx>;Mi~;w|0m}XUKU_F$(uWqNdn+2?M$Wv9 zP1w_3?J8)&ZOHCVzE-w7dvZH}O`gKVIDbM`Pj$ui+jF;leOq%&1cj35vs2^Ou0%6YMCaiq!f328!=rXcTk7wC)VbdgXOEg+Va0# zMyNMqms>e(%shPMOvGqO?+seBqhdIJysM*W0sud-j@h=QmJBGJ4u1&Jxz)GeWf=ze5`r>*5#-A=Fe2f zY>X_GI1MfKo#K|Mca`WQyErfHtpV9Z3vBU$s{bg3Sz7-lm@qKEar%~(PVO*freSrC zJeu(4ouSqi2G#RrbXnh-vS#0ETR{;}bP`M&Imz&o^bU_+O$hF>t8Tc|S@niQ%A4Ox zx_g-7U7u(Buyvl@*JzPI&v16zym*UHsX6}|9 z8KOP+T3)3th3)XDvs;MFk=apV`+4o!T^UM%xU%8=Z0=Q^N=R51zue(sQBjnR=(FOX zg_1Q-Fg{Ge)$GQK;qeqUP3!l>h{)7zHcOq>?%9iXG(~(b2eOAwZO+D+XSzF<1l~Pt z7?Hnzmn)9;OqNh2AhXEQ!IjmS$tFXK3QkaZ(6*jl;B*kK$hz>A`8D1Rm$1Lv#~K!- za)PDIj5J~?K_5RD(4Ii2Rc_?$T)|tbifk4g*4p8-lxh-EXSR^5ORVXb+kgu&8Jq$C zF0C&o;zo&@fktq4Y<2lga2aWdcsHXW?ug=Mo4nh!x3Tf>%duTMvhjqqVNaTt1?5MH z`~;7p{15b{*E4OQl{@rO@)^TGvV=6C3o)0;c8+BI99 zITh628v8pLC2=B&TaP6@*=Ku3yq4#=FWy-!qvsAt`H;J+Mn7+6Z8t9aCKa{T<*W%A zxS+&h2aI(@FInrs#f8=Ii9vUzIvw%HVBP^4gTKhG8XV*^kYLQ-s($%=6x2kN`C2{K| z&I3Vrb;~Zxz*}5*Ij|HDtnHq z@)MZ)yp`V6LN~8gEqlgL^-OW2L^VTw!t!&y;CI_S-u5jDe#E$}Z{Aw&Y1SwSQ8A~p zI|Ix<5>hRdf+)Z9S z)ok1}yt$m`mntS;+&Vv)&kzPFMD6b&-p=Dn5Yv*>jlhT6Oi!{lOADgLV3kf^ z19vI9Q6czX@IcCna&G-SBZt-!l7!UuU6&w^@|o&ByO8OxlgwfzE`rY1e4RNW2GySC zQ|}TUd7V9y;7++#7kmM2IAdn}uC2zme3WuuIN+uVn3;WFXM#Q~2gjiVMC6uky02lB zqCjJ{&rx);3Ov7WB04A{toNa!?=;cNTcC+&HM#Qmgx!Huv8e=``)%*x$qWkDpGB1i zHH+W^VE5EFc0ML2mo3ka*H2_;hmEB6k$Uuy!4h6=RPgUgwMDpyUWe+8Qbyk4BSQ%=Sp`3|6mOz_ zr>VJ*qdr8mJ5JS~5VB2ZZV096RC&>B_P9>hOIe9WxLof$$jlJ0Ooaz&`K`%6VB0I* zC>xVWHTHzbD0a*o_PP1$x76zBP)=Iw@1g8>I&BPpHB0NCws!@c{;8WQt9Xqie_{F= z3<(~3AS_{bp;663l3QklJj82v;ZCYynnEY#vM?hJa^=Nf>YY?{TLvXY8#Y-EM_nIw zt!X#&aND_9J=$4cW?SSm;h6i5|6<}p0mP6U{JZz-dJsEufH~c?jl4(nlU8jnYl&C; zh;n;;*IjMkqfZ$I6Y z;bT8kA3XVaE;zWyNe9P>DE2&aCTM!H*B;XSWF$jqdlhvp?ZYjEF3Dtz?Y(IDVmYBO zKGpqO66HC|Jj(;VS?ro;K%m7>0hq(~&0_YKIF$ajZk5@E#v|W*p*V-_xg*n_-X4(i zI4`7})h`H$tY3`<@B~w)&@l&=`5tXl&SOf|I-lWQ zPnJT}oR(xki;57$m%j31TGPhd?Wx0VKe%GmyL^6UK8DA;IrEv1^75Fp!QLSev)nNU z4M9XIPKs-0(uN2b^jS8a(cg*sC}do@X& zrjc}~%Smo5^B%5c#EFxscpW~J)AsKLyZwGDgYLnniL0vRmWMCiG#Mf-x1&D9m{Yw( z*mE$lIcBe}3Ly4=U;i5soA*|c?tx9tQ~dqv+7iMx_WX>9<;uqprT)fR+`FS?4Aqm-*j5mq)f*h)dh(TMLK`4URs zY{QBexxEm&6~*7hvp;E>4Ax8_almH1$?FP{U&q|@Ox$fBazncrrmFJcl~Qs9EX3__ z!Ni5BNmki2u{Nhzr02R}hv4eP!97%?Hu+&8QM(JpneaIqW@Gx2NBV|DxM;FrS?8Ro zlNOJ$@N>_fTW!O9#M?#zg>Ryo#6wgj^lbfyd1vOzSJj_e`(Z6jR#lOZWL`mx_`o6& z*O;1Ypkm_+Mth=VyPEUcK}4D^E{7n2i;CjfVpPk#y%}bd|8RKEDw*rbS1M_Q8Sr*pVPHmv&j zjw4c599tgaHC_@UO2~^1p4#c+z-J&9kt6n&cRX!$p+ql78RFo$AoC zL!OASgkyIUSF~b8eY4}IoQty~ZKz&S&V1$6e)~gJ zDAHoqKq{UcLtk20-Q&Gje}eJ)+pwCR?(M{6^`7T5&n%5D6S8wXT|9_*+^J36U6?a`cMXhj;2-CASpXit+M88hP>C z@jI4^8P%1v!zaD@886-5*hY?{;;N-0dT)y=@;Dg?H9Lpc#e zyp)ij)Y(d*jSOI0`vxA(rVTAqsZrhN>Lq8R*xc2{YB>QruRRRyUV5Qs0<}n+ZRGH2 zyeHWgk$)yR&DbuKn|E_2Lvn@gDp-!&xXf4{gHx_v4bK+0zvX|I%nqX1yzi6|MvrW* z)+ong=s%K$5Gh*od@rOv1`9M+Fn}qI?W0L)`}(}HiR!S7nkdsVoJmtFa43%V-3mP= z>e(F~X|MA0vsR%G%SgE=%9hP|VwFZ(+itds5230X_*Go-ZS(nd>FeUFUmNwUES27v2+sW@g)4X4xCdIkJdBE>8Y$9!g-KybBQyUA z`aadGX;iPWmv^mva)CXk@$2oWqn#NaMEjcStwe5`pcFUuTT{C!!Wl!e8xPHXBEIA1 zhsB*Ye6NPlN{a9f1@X&?tw9S%Wp$=K(2B&??kaOlpLY?6biL{V!-TrPc(R=7Y28C4 z9CzZE{^wD3VuYYp@#3j~5U>+E)1I;YJzrLAM3R40N3ZmR4uv>fW{3eUyVqf*(o@x` zfn7xmqahY(;dv&}#?*^6<`*)xO27(ji00$Ns14=RZEoyzyJBfHuGq2hr%#h&bwP^o zR9b|N%b%~TiVaSay_t$qN#7^(9NV&1P-bNDETyT;Pii%G13PV<{7B1Is>zH2i+*+W zGwN>(tnw81TRg3nD4T=FysmFD>dE(_fBy6?wF{M<-PF-;JLyb8x8}FYD129AK#TpN z>FWSC<-B(8GOQVJw-p#^H8orRjAr49&{uN73auA-RmF6~s>J0^4u`dREu0`z!8+sJ z>YaiG?jT$KSbfmZR!oX8eq;XaD6s|LedKT$lv)*2a^&VrXAMLIR!1J9L}j~fTbDL% zzp<;@WOxD%a3Q~5QD3$0Qa#427gxsaY2_#-B+}aLNgo#$f;6Q%pIxw4$H-b^vM(%j z@TYb`@+`ZVPO-P^Mki9z@H!c|2+#!I-tc26pMx*)am`OovdYQOuko$&Rm9MUq*#XT zI%(1H&iwFjZySARaKODZDwfuow}VC*N`HKgsfw{9z1hiw&>6&ZkcX8{#v1;5VMH_0 zjxWoTb+<}3v$2#c=G@b6EjOnm(CeV!WR_oPB9by-@<+YSSww+eIm(-7Y3gJzCbktH zJ3REIrONzu2Aky(6gXoLIJCaw&vg+MAH5y*Ub7&7xY?X<)~H3aw3D4IvyFc(D z4)qHKB&EnSe*lRXA8R2hhsl#6^8F`}u=Zop81hf+zaZY!|J+xM7Mj>q diff --git a/res/layout/fragment_add_public_chat.xml b/res/layout/fragment_add_public_chat.xml index 8f677a5784..f6ad5a9887 100644 --- a/res/layout/fragment_add_public_chat.xml +++ b/res/layout/fragment_add_public_chat.xml @@ -13,12 +13,12 @@ android:orientation="vertical"> + app:labeledEditText_label="@string/fragment_add_public_chat_url_edit_text_label"/> + app:cpb_textIdle="@string/fragment_add_public_chat_add_button_title_1" /> diff --git a/res/values/strings.xml b/res/values/strings.xml index 5cd6bf7d23..3a1a2303d6 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1575,7 +1575,6 @@ Copied to clipboard Share Public Key Show QR Code - Add Public Chat Link Device Show Seed Your Seed @@ -1601,13 +1600,12 @@ Please enter the public key of the person you\'d like to message Add Public Chat - Server URL - Enter the full URL of the public server. E.g https://public-server.url - Add - Adding Server... - Invalid Url provided - Failed to connect to server - Added public chat server + URL + Enter the URL of the public chat you\'d like to join. The Loki Public Chat URL is https://chat.lokinet.org. + Add + Adding Server... + Invalid URL + Couldn\'t Connect Accept Reject diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 2260f55718..21598d3421 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -41,13 +41,9 @@ android:title="@string/activity_settings_show_qr_code_button_title" android:icon="@drawable/icon_qr_code"/> - - + android:title="@string/activity_settings_link_device_button_title" + android:icon="@drawable/icon_link"/> groupChatServers = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChatServers(); for (String server : groupChatServers) { - chatAPI.setDisplayName(name, server); + publicChatAPI.setDisplayName(name, server); } } diff --git a/src/org/thoughtcrime/securesms/components/QuoteView.java b/src/org/thoughtcrime/securesms/components/QuoteView.java index f868c56082..1c2c705197 100644 --- a/src/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/org/thoughtcrime/securesms/components/QuoteView.java @@ -197,9 +197,9 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener // If we're in a group then try and use the display name in the group if (conversationRecipient.isGroupRecipient()) { long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient); - LokiPublicChat chat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); - if (chat != null) { - String senderDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(chat.getId(), author.getAddress().serialize()); + LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); + if (publicChat != null) { + String senderDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(publicChat.getId(), author.getAddress().serialize()); if (senderDisplayName != null) { quoteeDisplayName = senderDisplayName; } } } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 0e2835ed2f..87b03e5118 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -406,14 +406,14 @@ public class ConversationFragment extends Fragment boolean isGroupChat = recipient.isGroupRecipient(); if (isGroupChat) { - LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); - boolean isPublicChat = groupChat != null; + LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); + boolean isPublicChat = publicChat != null; int selectedMessageCount = messageRecords.size(); boolean isSentByUser = ((MessageRecord)messageRecords.toArray()[0]).isOutgoing(); menu.findItem(R.id.menu_context_copy_public_key).setVisible(isPublicChat && selectedMessageCount == 1 && !isSentByUser); menu.findItem(R.id.menu_context_reply).setVisible(isPublicChat && selectedMessageCount == 1); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); - boolean userCanModerate = groupChat != null && LokiPublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, groupChat.getChannel(), groupChat.getServer()); + boolean userCanModerate = isPublicChat && LokiPublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer()); boolean isDeleteOptionVisible = isPublicChat && selectedMessageCount == 1 && (isSentByUser || userCanModerate); menu.findItem(R.id.menu_context_delete_message).setVisible(isDeleteOptionVisible); } else { @@ -508,8 +508,8 @@ public class ConversationFragment extends Fragment builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount)); builder.setCancelable(true); - // Loki - The delete option is only visible to the user in a group chat - LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); + // Loki - The delete option is only visible to the user in a public chat + LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { @Override @@ -523,16 +523,16 @@ public class ConversationFragment extends Fragment for (MessageRecord messageRecord : messageRecords) { boolean isThreadDeleted; - if (groupChat != null) { + if (publicChat != null) { final SettableFuture[] future = { new SettableFuture() }; - LokiPublicChatAPI chatAPI = ApplicationContext.getInstance(getContext()).getLokiPublicChatAPI(); + LokiPublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getLokiPublicChatAPI(); boolean isSentByUser = messageRecord.isOutgoing(); Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id); - if (chatAPI != null && serverID != null) { - chatAPI - .deleteMessage(serverID, groupChat.getChannel(), groupChat.getServer(), isSentByUser) + if (publicChatAPI != null && serverID != null) { + publicChatAPI + .deleteMessage(serverID, publicChat.getChannel(), publicChat.getServer(), isSentByUser) .success(l -> { @SuppressWarnings("unchecked") SettableFuture f = (SettableFuture) future[0]; f.set(Unit.INSTANCE); diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java index bedeb16cfe..57fbe13391 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -939,10 +939,9 @@ public class ConversationItem extends LinearLayout contactPhoto.setVisibility(VISIBLE); int visibility = View.GONE; - // If we have a chat then use that to determine mod status - LokiPublicChat groupChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId()); - if (groupChat != null) { - boolean isModerator = LokiPublicChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), groupChat.getChannel(), groupChat.getServer()); + LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(messageRecord.getThreadId()); + if (publicChat != null) { + boolean isModerator = LokiPublicChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), publicChat.getChannel(), publicChat.getServer()); visibility = isModerator ? View.VISIBLE : View.GONE; } diff --git a/src/org/thoughtcrime/securesms/database/Address.java b/src/org/thoughtcrime/securesms/database/Address.java index 40d8a2ebd5..ed20c6fbbb 100644 --- a/src/org/thoughtcrime/securesms/database/Address.java +++ b/src/org/thoughtcrime/securesms/database/Address.java @@ -52,7 +52,7 @@ public class Address implements Parcelable, Comparable

{ private final String address; - // Loki - Special flag to indicate whether this address is meant to representing a public chat or not + // Loki - Special flag to indicate whether this address represents a public chat or not private Boolean isPublicChat; private Address(@NonNull String address) { diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 2f798fe3ee..be4dd9ed4e 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -287,12 +287,12 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { private @NonNull List
getGroupMessageRecipients(String groupId, long messageId) { ArrayList
result = new ArrayList<>(); - // Loki - All group messages should be directed to their servers + // Loki - All group messages should be directed to their respective servers long threadID = GroupManager.getThreadIdFromGroupId(groupId, context); - LokiPublicChat chat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID); - if (chat != null) { + LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID); + if (publicChat != null) { // We need to somehow maintain information that will allow the sender to map - // a Recipient to the correct public chat thread, and so this might be a bit hacky + // a recipient to the correct public chat thread, and so this might be a bit hacky result.add(Address.fromPublicChatGroupID(groupId)); } diff --git a/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt index 9e034f32bc..bab0f78c97 100644 --- a/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt @@ -44,26 +44,25 @@ class AddPublicChatActivity : PassphraseRequiredActionBarActivity() { private fun addPublicChatIfPossible() { val inputMethodManager = getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.hideSoftInputFromWindow(serverUrlEditText.windowToken, 0) + inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0) - val url = serverUrlEditText.text.toString().toLowerCase().replace("http://", "https://") + val url = urlEditText.text.toString().toLowerCase().replace("http://", "https://") if (!Patterns.WEB_URL.matcher(url).matches() || !url.startsWith("https://")) { return Toast.makeText(this, R.string.fragment_add_public_chat_invalid_url_message, Toast.LENGTH_SHORT).show() } setButtonEnabled(false) ApplicationContext.getInstance(this).lokiPublicChatManager.addChat(url, 1).successUi { - Toast.makeText(this, R.string.fragment_add_public_chat_success_message, Toast.LENGTH_SHORT).show() finish() }.failUi { setButtonEnabled(true) - Toast.makeText(this, R.string.fragment_add_public_chat_failed_connect_message, Toast.LENGTH_SHORT).show() + Toast.makeText(this, R.string.fragment_add_public_chat_connection_failed_message, Toast.LENGTH_SHORT).show() } } private fun setButtonEnabled(enabled: Boolean) { addButton.isEnabled = enabled - val text = if (enabled) R.string.fragment_add_public_chat_add_button_title else R.string.fragment_add_public_chat_adding_server_button_title + val text = if (enabled) R.string.fragment_add_public_chat_add_button_title_1 else R.string.fragment_add_public_chat_add_button_title_2 addButton.setText(text) - serverUrlEditText.isEnabled = enabled + urlEditText.isEnabled = enabled } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt index 18bc717ba4..31d31a611e 100644 --- a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt @@ -43,11 +43,10 @@ class DisplayNameActivity : BaseActionBarActivity() { application.setUpStorageAPIIfNeeded() startActivity(Intent(this, ConversationListActivity::class.java)) finish() - - val chatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI - if (chatAPI != null && name != null) { + val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI + if (publicChatAPI != null) { val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers() - servers.forEach { chatAPI.setDisplayName(name, it) } + servers.forEach { publicChatAPI.setDisplayName(name, it) } } } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt b/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt index 6da575b9ce..856b038b9a 100644 --- a/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/GeneralUtilities.kt @@ -23,7 +23,7 @@ fun toPx(dp: Int, resources: Resources): Int { return (dp * scale).roundToInt() } -fun isGroupRecipient(context: Context, recipient: String): Boolean { +fun isPublicChat(context: Context, recipient: String): Boolean { return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().values.map { it.server }.contains(recipient) } diff --git a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt index b8ae58f0bb..01da3532ad 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt @@ -59,7 +59,7 @@ class LokiPublicChatManager(private val context: Context) { // Set our name on the server val displayName = TextSecurePreferences.getProfileName(context) if (!TextUtils.isEmpty(displayName)) { - ApplicationContext.getInstance(context).lokiPublicChatAPI?.setDisplayName(server, displayName) + ApplicationContext.getInstance(context).lokiPublicChatAPI?.setDisplayName(displayName, server) } // Start polling Util.runOnMain{ startPollersIfNeeded() } diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt index b7e604a587..931241168d 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt @@ -20,14 +20,14 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa companion object { private val friendRequestTableName = "loki_thread_friend_request_database" private val sessionResetTableName = "loki_thread_session_reset_database" - public val groupChatMappingTableName = "loki_group_chat_mapping_database" + public val publicChatTableName = "loki_public_chat_database" public val threadID = "thread_id" private val friendRequestStatus = "friend_request_status" private val sessionResetStatus = "session_reset_status" - public val groupChatJSON = "group_chat_json" + public val publicChat = "public_chat" @JvmStatic val createFriendRequestTableCommand = "CREATE TABLE $friendRequestTableName ($threadID INTEGER PRIMARY KEY, $friendRequestStatus INTEGER DEFAULT 0);" @JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTableName ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);" - @JvmStatic val createGroupChatMappingTableCommand = "CREATE TABLE $groupChatMappingTableName ($threadID INTEGER PRIMARY KEY, $groupChatJSON TEXT);" + @JvmStatic val createGroupChatMappingTableCommand = "CREATE TABLE $publicChatTableName ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);" } override fun getThreadID(hexEncodedPublicKey: String): Long { @@ -94,50 +94,46 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa fun getAllPublicChats(): Map { val database = databaseHelper.readableDatabase var cursor: Cursor? = null - + val result = mutableMapOf() try { - val map = mutableMapOf() - cursor = database.rawQuery("select * from $groupChatMappingTableName", null) + cursor = database.rawQuery("select * from $publicChatTableName", null) while (cursor != null && cursor.moveToNext()) { - val threadID = cursor.getLong(Companion.threadID) - val string = cursor.getString(groupChatJSON) - val chat = LokiPublicChat.fromJSON(string) - if (chat != null) { map[threadID] = chat } + val threadID = cursor.getLong(threadID) + val string = cursor.getString(publicChat) + val publicChat = LokiPublicChat.fromJSON(string) + if (publicChat != null) { result[threadID] = publicChat } } - return map } catch (e: Exception) { - + // Do nothing } finally { cursor?.close() } - - return mapOf() + return result } fun getAllPublicChatServers(): Set { - return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) } + return getAllPublicChats().values.fold(setOf()) { set, chat -> set.plus(chat.server) } } override fun getPublicChat(threadID: Long): LokiPublicChat? { if (threadID < 0) { return null } val database = databaseHelper.readableDatabase - return database.get(groupChatMappingTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor -> - val string = cursor.getString(groupChatJSON) - LokiPublicChat.fromJSON(string) + return database.get(publicChatTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor -> + val publicChatAsJSON = cursor.getString(publicChat) + LokiPublicChat.fromJSON(publicChatAsJSON) } } - override fun setPublicChat(groupChat: LokiPublicChat, threadID: Long) { + override fun setPublicChat(publicChat: LokiPublicChat, threadID: Long) { if (threadID < 0) { return } - val database = databaseHelper.writableDatabase val contentValues = ContentValues(2) contentValues.put(Companion.threadID, threadID) - contentValues.put(Companion.groupChatJSON, JsonUtil.toJson(groupChat.toJSON())) - database.insertOrUpdate(groupChatMappingTableName, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) + contentValues.put(Companion.publicChat, JsonUtil.toJson(publicChat.toJSON())) + database.insertOrUpdate(publicChatTableName, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) } override fun removePublicChat(threadID: Long) { - databaseHelper.writableDatabase.delete(groupChatMappingTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) + databaseHelper.writableDatabase.delete(publicChatTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index 6676dfcf5f..d6cb29279d 100644 --- a/src/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/src/org/thoughtcrime/securesms/sms/MessageSender.java @@ -213,7 +213,7 @@ public class MessageSender { // Just send the message normally if it's a group message String recipientPublicKey = recipient.getAddress().serialize(); - if (GeneralUtilitiesKt.isGroupRecipient(context, recipientPublicKey)) { + if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey)) { jobManager.add(new PushTextSendJob(messageId, recipient.getAddress())); return; } @@ -243,7 +243,7 @@ public class MessageSender { // Just send the message normally if it's a group message String recipientPublicKey = recipient.getAddress().serialize(); - if (GeneralUtilitiesKt.isGroupRecipient(context, recipientPublicKey)) { + if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey)) { PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress()); return; } From cbad88558683ed009e18c77e9e6e1bd52a48f9d9 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 15 Oct 2019 16:06:38 +1100 Subject: [PATCH 08/10] Add missing display name update & message fetch --- ..._chat.xml => activity_add_public_chat.xml} | 0 .../securesms/ApplicationContext.java | 30 +++++++--------- .../ApplicationPreferencesActivity.java | 1 - .../securesms/ConversationListActivity.java | 4 +-- .../securesms/CreateProfileActivity.java | 4 +-- .../securesms/components/QuoteView.java | 18 +++++----- .../database/helpers/SQLCipherOpenHelper.java | 4 +-- .../securesms/loki/AddPublicChatActivity.kt | 34 +++++++++++-------- .../securesms/loki/DisplayNameActivity.kt | 4 +++ .../securesms/loki/LokiThreadDatabase.kt | 2 +- 10 files changed, 54 insertions(+), 47 deletions(-) rename res/layout/{fragment_add_public_chat.xml => activity_add_public_chat.xml} (100%) diff --git a/res/layout/fragment_add_public_chat.xml b/res/layout/activity_add_public_chat.xml similarity index 100% rename from res/layout/fragment_add_public_chat.xml rename to res/layout/activity_add_public_chat.xml diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index e06c634635..6384e42d47 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -186,7 +186,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc mixpanel.trackMap(event, properties); return Unit.INSTANCE; }; - + // Loki - Set up public chat manager lokiPublicChatManager = new LokiPublicChatManager(this); } @@ -198,6 +198,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc KeyCachingService.onAppForegrounded(this); // Loki - Start long polling if needed startLongPollingIfNeeded(); + lokiPublicChatManager.startPollersIfNeeded(); setUpStorageAPIIfNeeded(); } @@ -261,7 +262,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(this); lokiPublicChatAPI = new LokiPublicChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase); } - return lokiPublicChatAPI; } @@ -501,20 +501,20 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc return new LokiRSSFeed("loki.network.messenger-updates.feed", "https://loki.network/category/messenger-updates/feed", "Loki Messenger Updates", false); } - public void createGroupChatsIfNeeded() { - List defaultChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG); - for (LokiPublicChat chat : defaultChats) { - long threadID = GroupManager.getThreadId(chat.getId(), this); - String migrationKey = chat.getId() + "_migrated"; + public void createDefaultPublicChatsIfNeeded() { + List defaultPublicChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG); + for (LokiPublicChat publiChat : defaultPublicChats) { + long threadID = GroupManager.getThreadId(publiChat.getId(), this); + String migrationKey = publiChat.getId() + "_migrated"; boolean isChatMigrated = TextSecurePreferences.getBooleanPreference(this, migrationKey, false); - boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, chat.getId()); - if (!isChatSetUp || !chat.isDeletable()) { - lokiPublicChatManager.addChat(chat.getServer(), chat.getChannel(), chat.getDisplayName()); - TextSecurePreferences.markChatSetUp(this, chat.getId()); + boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publiChat.getId()); + if (!isChatSetUp || !publiChat.isDeletable()) { + lokiPublicChatManager.addChat(publiChat.getServer(), publiChat.getChannel(), publiChat.getDisplayName()); + TextSecurePreferences.markChatSetUp(this, publiChat.getId()); TextSecurePreferences.setBooleanPreference(this, migrationKey, true); } else if (threadID > -1 && !isChatMigrated) { - // Migrate the old public chats. - DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(chat, threadID); + // Migrate the old public chats + DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(publiChat, threadID); TextSecurePreferences.setBooleanPreference(this, migrationKey, true); } } @@ -572,10 +572,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc this.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadID), true, observer); } - public void startGroupChatPollersIfNeeded() { - lokiPublicChatManager.startPollersIfNeeded(); - } - public void startRSSFeedPollersIfNeeded() { createRSSFeedPollersIfNeeded(); if (lokiNewsFeedPoller != null) lokiNewsFeedPoller.startIfNeeded(); diff --git a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index 998832642b..f9b4cd3b15 100644 --- a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -187,7 +187,6 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PUBLIC_KEY)); this.findPreference(PREFERENCE_CATEGORY_QR_CODE) .setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_QR_CODE)); - Preference linkDevicePreference = this.findPreference(PREFERENCE_CATEGORY_LINK_DEVICE); // Hide if this is a slave device linkDevicePreference.setVisible(isMasterDevice); diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index b817a2b16e..d28e2bfcbd 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -82,9 +82,9 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit dynamicLanguage.onCreate(this); if (TextSecurePreferences.getLocalNumber(this) != null) { ApplicationContext application = ApplicationContext.getInstance(this); - application.createGroupChatsIfNeeded(); + application.createDefaultPublicChatsIfNeeded(); application.createRSSFeedsIfNeeded(); - application.startGroupChatPollersIfNeeded(); + application.getLokiPublicChatManager().startPollersIfNeeded(); application.startRSSFeedPollersIfNeeded(); } } diff --git a/src/org/thoughtcrime/securesms/CreateProfileActivity.java b/src/org/thoughtcrime/securesms/CreateProfileActivity.java index d8e46ae67e..975a9b443f 100644 --- a/src/org/thoughtcrime/securesms/CreateProfileActivity.java +++ b/src/org/thoughtcrime/securesms/CreateProfileActivity.java @@ -380,8 +380,8 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje TextSecurePreferences.setProfileName(context, name); LokiPublicChatAPI publicChatAPI = ApplicationContext.getInstance(context).getLokiPublicChatAPI(); if (publicChatAPI != null) { - Set groupChatServers = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChatServers(); - for (String server : groupChatServers) { + Set servers = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChatServers(); + for (String server : servers) { publicChatAPI.setDisplayName(name, server); } } diff --git a/src/org/thoughtcrime/securesms/components/QuoteView.java b/src/org/thoughtcrime/securesms/components/QuoteView.java index 1c2c705197..cfb6a1bc3b 100644 --- a/src/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/org/thoughtcrime/securesms/components/QuoteView.java @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.loki.api.LokiPublicChat; @@ -194,14 +195,15 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener String quoteeDisplayName = author.toShortString(); - // If we're in a group then try and use the display name in the group - if (conversationRecipient.isGroupRecipient()) { - long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient); - LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId); - if (publicChat != null) { - String senderDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(publicChat.getId(), author.getAddress().serialize()); - if (senderDisplayName != null) { quoteeDisplayName = senderDisplayName; } - } + long threadID = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient); + String senderHexEncodedPublicKey = author.getAddress().serialize(); + LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID); + if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) { + quoteeDisplayName = TextSecurePreferences.getProfileName(getContext()); + } else if (publicChat != null) { + quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(publicChat.getId(), senderHexEncodedPublicKey); + } else { + quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getDisplayName(senderHexEncodedPublicKey); } authorView.setText(isOwnNumber ? getContext().getString(R.string.QuoteView_you) : quoteeDisplayName); diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index c92b054706..28122e956c 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -131,7 +131,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiMessageDatabase.getCreateTableCommand()); db.execSQL(LokiThreadDatabase.getCreateFriendRequestTableCommand()); db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand()); - db.execSQL(LokiThreadDatabase.getCreateGroupChatMappingTableCommand()); + db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); @@ -498,7 +498,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { if (oldVersion < lokiV3) { db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand()); - db.execSQL(LokiThreadDatabase.getCreateGroupChatMappingTableCommand()); + db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); } db.setTransactionSuccessful(); diff --git a/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt index bab0f78c97..ec075001cd 100644 --- a/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt @@ -5,7 +5,7 @@ import android.util.Patterns import android.view.MenuItem import android.view.inputmethod.InputMethodManager import android.widget.Toast -import kotlinx.android.synthetic.main.fragment_add_public_chat.* +import kotlinx.android.synthetic.main.activity_add_public_chat.* import network.loki.messenger.R import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.util.DynamicTheme +import org.thoughtcrime.securesms.util.TextSecurePreferences class AddPublicChatActivity : PassphraseRequiredActionBarActivity() { private val dynamicTheme = DynamicTheme() @@ -24,8 +25,8 @@ class AddPublicChatActivity : PassphraseRequiredActionBarActivity() { override fun onCreate(bundle: Bundle?, isReady: Boolean) { supportActionBar!!.setTitle(R.string.fragment_add_public_chat_title) supportActionBar!!.setDisplayHomeAsUpEnabled(true) - setContentView(R.layout.fragment_add_public_chat) - setButtonEnabled(true) + setContentView(R.layout.activity_add_public_chat) + updateUI(false) addButton.setOnClickListener { addPublicChatIfPossible() } } @@ -45,24 +46,29 @@ class AddPublicChatActivity : PassphraseRequiredActionBarActivity() { private fun addPublicChatIfPossible() { val inputMethodManager = getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0) - val url = urlEditText.text.toString().toLowerCase().replace("http://", "https://") - if (!Patterns.WEB_URL.matcher(url).matches() || !url.startsWith("https://")) { return Toast.makeText(this, R.string.fragment_add_public_chat_invalid_url_message, Toast.LENGTH_SHORT).show() } - - setButtonEnabled(false) - - ApplicationContext.getInstance(this).lokiPublicChatManager.addChat(url, 1).successUi { + if (!Patterns.WEB_URL.matcher(url).matches() || !url.startsWith("https://")) { + return Toast.makeText(this, R.string.fragment_add_public_chat_invalid_url_message, Toast.LENGTH_SHORT).show() + } + updateUI(true) + val application = ApplicationContext.getInstance(this) + val channel: Long = 1 + val displayName = TextSecurePreferences.getProfileName(this) + val lokiPublicChatAPI = application.lokiPublicChatAPI!! + application.lokiPublicChatManager.addChat(url, channel).successUi { + lokiPublicChatAPI.getMessages(channel, url) + lokiPublicChatAPI.setDisplayName(displayName, url) finish() }.failUi { - setButtonEnabled(true) + updateUI(false) Toast.makeText(this, R.string.fragment_add_public_chat_connection_failed_message, Toast.LENGTH_SHORT).show() } } - private fun setButtonEnabled(enabled: Boolean) { - addButton.isEnabled = enabled - val text = if (enabled) R.string.fragment_add_public_chat_add_button_title_1 else R.string.fragment_add_public_chat_add_button_title_2 + private fun updateUI(isConnecting: Boolean) { + addButton.isEnabled = !isConnecting + val text = if (isConnecting) R.string.fragment_add_public_chat_add_button_title_2 else R.string.fragment_add_public_chat_add_button_title_1 addButton.setText(text) - urlEditText.isEnabled = enabled + urlEditText.isEnabled = !isConnecting } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt index 31d31a611e..bb7f97504e 100644 --- a/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/DisplayNameActivity.kt @@ -45,6 +45,10 @@ class DisplayNameActivity : BaseActionBarActivity() { finish() val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI if (publicChatAPI != null) { + application.createDefaultPublicChatsIfNeeded() + application.createRSSFeedsIfNeeded() + application.lokiPublicChatManager.startPollersIfNeeded() + application.startRSSFeedPollersIfNeeded() val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers() servers.forEach { publicChatAPI.setDisplayName(name, it) } } diff --git a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt index 931241168d..4c177bc108 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt @@ -27,7 +27,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa public val publicChat = "public_chat" @JvmStatic val createFriendRequestTableCommand = "CREATE TABLE $friendRequestTableName ($threadID INTEGER PRIMARY KEY, $friendRequestStatus INTEGER DEFAULT 0);" @JvmStatic val createSessionResetTableCommand = "CREATE TABLE $sessionResetTableName ($threadID INTEGER PRIMARY KEY, $sessionResetStatus INTEGER DEFAULT 0);" - @JvmStatic val createGroupChatMappingTableCommand = "CREATE TABLE $publicChatTableName ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);" + @JvmStatic val createPublicChatTableCommand = "CREATE TABLE $publicChatTableName ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);" } override fun getThreadID(hexEncodedPublicKey: String): Long { From c2d4f4b58dfaaa93d8e19dba435dd3b3cb2d710c Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 15 Oct 2019 16:07:34 +1100 Subject: [PATCH 09/10] Fix mentions bug --- .../securesms/conversation/ConversationActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index c392596663..b5e55c6020 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -2775,7 +2775,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } catch (Exception exception) { mentions.clear(); // TODO: Dirty workaround for ConcurrentModificationException } - } else if (text.length() > 0) { + } + if (text.length() > 0) { if (currentMentionStartIndex > text.length()) { resetMentions(); // Should never occur } From a5b543c43c2cfbc0e50f401b4652bb20ac82730d Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 15 Oct 2019 16:19:00 +1100 Subject: [PATCH 10/10] Move add public chat button to home screen --- res/menu/text_secure_normal.xml | 12 +++++++--- res/values/strings.xml | 2 ++ .../securesms/ConversationListActivity.java | 24 +++++++++++-------- .../conversation/ConversationActivity.java | 12 ++++------ 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/res/menu/text_secure_normal.xml b/res/menu/text_secure_normal.xml index cb90a43076..875626f0f1 100644 --- a/res/menu/text_secure_normal.xml +++ b/res/menu/text_secure_normal.xml @@ -1,7 +1,13 @@ - + - + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 3a1a2303d6..51065f8cc1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1639,5 +1639,7 @@ Loki Messenger needs camera access to scan QR codes. Copy public key + + Add Public Chat diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index d28e2bfcbd..d51d1b3ce9 100644 --- a/src/org/thoughtcrime/securesms/ConversationListActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java @@ -29,6 +29,7 @@ import android.support.annotation.NonNull; import android.support.v7.widget.Toolbar; import android.support.v7.widget.TooltipCompat; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -44,6 +45,7 @@ import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.lock.RegistrationLockDialog; +import org.thoughtcrime.securesms.loki.AddPublicChatActivity; import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MessageNotifier; @@ -127,18 +129,15 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit @Override public boolean onPrepareOptionsMenu(Menu menu) { - return false; - /* MenuInflater inflater = this.getMenuInflater(); menu.clear(); inflater.inflate(R.menu.text_secure_normal, menu); - menu.findItem(R.id.menu_clear_passphrase).setVisible(!TextSecurePreferences.isPasswordDisabled(this)); +// menu.findItem(R.id.menu_clear_passphrase).setVisible(!TextSecurePreferences.isPasswordDisabled(this)); super.onPrepareOptionsMenu(menu); return true; - */ } private void initializeSearchListener() { @@ -235,12 +234,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit super.onOptionsItemSelected(item); switch (item.getItemId()) { - case R.id.menu_new_group: createGroup(); return true; - case R.id.menu_settings: handleDisplaySettings(); return true; - case R.id.menu_clear_passphrase: handleClearPassphrase(); return true; - case R.id.menu_mark_all_read: handleMarkAllRead(); return true; - case R.id.menu_invite: handleInvite(); return true; - case R.id.menu_help: handleHelp(); return true; +// case R.id.menu_new_group: createGroup(); return true; +// case R.id.menu_settings: handleDisplaySettings(); return true; +// case R.id.menu_clear_passphrase: handleClearPassphrase(); return true; +// case R.id.menu_mark_all_read: handleMarkAllRead(); return true; +// case R.id.menu_invite: handleInvite(); return true; +// case R.id.menu_help: handleHelp(); return true; + case R.id.menu_conversation_list_add_public_chat_option: addNewPublicChat(); return true; } return false; @@ -321,4 +321,8 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit Toast.makeText(this, R.string.ConversationListActivity_there_is_no_browser_installed_on_your_device, Toast.LENGTH_LONG).show(); } } + + private void addNewPublicChat() { + startActivity(new Intent(this, AddPublicChatActivity.class)); + } } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index b5e55c6020..39412b48ee 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -2766,15 +2766,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (isBackspace) { currentMentionStartIndex = -1; mentionCandidateSelectionView.hide(); - try { - for (Mention mention : mentions) { - if (!text.contains(mention.getDisplayName())) { - mentions.remove(mention); - } + ArrayList mentionsToRemove = new ArrayList<>(); + for (Mention mention : mentions) { + if (!text.contains(mention.getDisplayName())) { + mentionsToRemove.add(mention); } - } catch (Exception exception) { - mentions.clear(); // TODO: Dirty workaround for ConcurrentModificationException } + mentions.removeAll(mentionsToRemove); } if (text.length() > 0) { if (currentMentionStartIndex > text.length()) {