diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c30e4385da..7b88684d51 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -479,6 +479,10 @@ + + diff --git a/res/layout/activity_add_public_chat.xml b/res/layout/activity_add_public_chat.xml new file mode 100644 index 0000000000..f6ad5a9887 --- /dev/null +++ b/res/layout/activity_add_public_chat.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + \ No newline at end of file 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 8ee2da75e5..51065f8cc1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1598,6 +1598,14 @@ Next Invalid public key Please enter the public key of the person you\'d like to message + + Add Public Chat + 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 @@ -1631,5 +1639,7 @@ Loki Messenger needs camera access to scan QR codes. Copy public key + + Add Public Chat diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 99aa910c01..21598d3421 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -42,8 +42,8 @@ android:icon="@drawable/icon_qr_code"/> + android:title="@string/activity_settings_link_device_button_title" + android:icon="@drawable/icon_link"/> (), null, publicChat.getDisplayName(), false); - TextSecurePreferences.markChatSetUp(this, publicChat.getId()); + 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, 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(publiChat, 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(); @@ -558,11 +572,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc this.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadID), true, observer); } - public void startGroupChatPollersIfNeeded() { - createGroupChatPollersIfNeeded(); - if (lokiPublicChatPoller != null) lokiPublicChatPoller.startIfNeeded(); - } - public void startRSSFeedPollersIfNeeded() { createRSSFeedPollersIfNeeded(); if (lokiNewsFeedPoller != null) lokiNewsFeedPoller.startIfNeeded(); diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java index b817a2b16e..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; @@ -82,9 +84,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(); } } @@ -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/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 cae89065d3..975a9b443f 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,13 +58,14 @@ 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.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import org.whispersystems.signalservice.loki.utilities.Analytics; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.security.SecureRandom; +import java.util.Set; import java.util.concurrent.ExecutionException; import javax.inject.Inject; @@ -380,12 +378,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()); + LokiPublicChatAPI publicChatAPI = ApplicationContext.getInstance(context).getLokiPublicChatAPI(); + if (publicChatAPI != null) { + Set servers = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChatServers(); + for (String server : servers) { + publicChatAPI.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..cfb6a1bc3b 100644 --- a/src/org/thoughtcrime/securesms/components/QuoteView.java +++ b/src/org/thoughtcrime/securesms/components/QuoteView.java @@ -28,10 +28,10 @@ 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.TextSecurePreferences; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; import java.util.List; @@ -194,19 +194,16 @@ 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()); - if (senderDisplayName != null) { quoteeDisplayName = senderDisplayName; } - } catch (Exception e) { - // Do nothing - } + 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/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index b728c58c42..39412b48ee 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; @@ -2765,24 +2766,25 @@ 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 } - } else if (text.length() > 0) { + mentions.removeAll(mentionsToRemove); + } + if (text.length() > 0) { if (currentMentionStartIndex > text.length()) { resetMentions(); // Should never occur } 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 +2793,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 40c91565f5..87b03e5118 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -68,7 +68,6 @@ 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.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsSmsDatabase; @@ -81,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; @@ -105,7 +102,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.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import java.io.IOException; import java.io.InputStream; @@ -408,14 +406,15 @@ public class ConversationFragment extends Fragment boolean isGroupChat = recipient.isGroupRecipient(); if (isGroupChat) { - boolean isLokiPublicChat = recipient.getName() != null && recipient.getName().equals("Loki Public Chat"); + 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(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 = 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 { menu.findItem(R.id.menu_context_copy_public_key).setVisible(false); @@ -509,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 if it's the Loki Public Chat - boolean isLokiPublicChat = this.recipient.isGroupRecipient(); + // 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 @@ -524,19 +523,16 @@ public class ConversationFragment extends Fragment for (MessageRecord messageRecord : messageRecords) { boolean isThreadDeleted; - if (isLokiPublicChat) { + if (publicChat != 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(); + LokiPublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getLokiPublicChatAPI(); 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 (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 1d239c6adf..57fbe13391 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -113,7 +113,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.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI; import java.util.Collections; import java.util.HashSet; @@ -473,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); @@ -789,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; @@ -936,13 +937,14 @@ 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; + + 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; - } else { - visibility = View.GONE; } + moderatorIconImageView.setVisibility(visibility); } else { contactPhoto.setVisibility(GONE); diff --git a/src/org/thoughtcrime/securesms/database/Address.java b/src/org/thoughtcrime/securesms/database/Address.java index f70033991c..ed20c6fbbb 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 represents 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/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 51d45e6c2d..28122e956c 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; @@ -131,6 +131,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiMessageDatabase.getCreateTableCommand()); db.execSQL(LokiThreadDatabase.getCreateFriendRequestTableCommand()); db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand()); + db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); @@ -497,6 +498,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { if (oldVersion < lokiV3) { db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand()); + db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); } db.setTransactionSuccessful(); diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index c7e6291eff..be4dd9ed4e 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,7 +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.LokiGroupChatAPI; +import org.whispersystems.signalservice.loki.api.LokiPublicChat; import java.io.IOException; import java.util.ArrayList; @@ -285,7 +286,16 @@ 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 respective servers + long threadID = GroupManager.getThreadIdFromGroupId(groupId, context); + 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 + 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 new file mode 100644 index 0000000000..ec075001cd --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/AddPublicChatActivity.kt @@ -0,0 +1,74 @@ +package org.thoughtcrime.securesms.loki + +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_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.util.DynamicTheme +import org.thoughtcrime.securesms.util.TextSecurePreferences + +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.activity_add_public_chat) + updateUI(false) + 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(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() + } + 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 { + updateUI(false) + Toast.makeText(this, R.string.fragment_add_public_chat_connection_failed_message, Toast.LENGTH_SHORT).show() + } + } + + 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 = !isConnecting + } +} \ No newline at end of file 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 1db9912be5..bb7f97504e 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() { @@ -45,10 +43,14 @@ 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) - LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase).setDisplayName(name, LokiGroupChatAPI.publicChatServer) + 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) } + } } } \ 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..856b038b9a 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 @@ -24,8 +23,8 @@ fun toPx(dp: Int, resources: Resources): Int { return (dp * scale).roundToInt() } -fun isGroupRecipient(recipient: String): Boolean { - return (LokiGroupChatAPI.publicChatServer == recipient) +fun isPublicChat(context: Context, recipient: String): Boolean { + 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/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..01da3532ad --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/LokiPublicChatManager.kt @@ -0,0 +1,118 @@ +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 +import org.thoughtcrime.securesms.ApplicationContext +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.thoughtcrime.securesms.util.Util +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 val observers = mutableMapOf() + private var isPolling = false + + public fun startPollersIfNeeded() { + refreshChatsAndPollers() + + for ((threadId, chat) in chats) { + val poller = pollers[threadId] ?: LokiPublicChatPoller(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).lokiPublicChatAPI ?: return Promise.ofFail(IllegalStateException("LokiPublicChatAPI is not set!")) + return groupChatAPI.getAuthToken(server).bind { + groupChatAPI.getChannelInfo(channel, server) + }.map { + addChat(server, channel, it) + } + } + + 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).setPublicChat(chat, threadID) + // Set our name on the server + val displayName = TextSecurePreferences.getProfileName(context) + if (!TextUtils.isEmpty(displayName)) { + ApplicationContext.getInstance(context).lokiPublicChatAPI?.setDisplayName(displayName, server) + } + // Start polling + Util.runOnMain{ startPollersIfNeeded() } + + return chat + } + + private fun refreshChatsAndPollers() { + val chatsInDB = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats() + 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).removePublicChat(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/LokiGroupChatPoller.kt b/src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt similarity index 89% rename from src/org/thoughtcrime/securesms/loki/LokiGroupChatPoller.kt rename to src/org/thoughtcrime/securesms/loki/LokiPublicChatPoller.kt index 294a9249e8..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 @@ -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..4c177bc108 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiThreadDatabase.kt @@ -2,11 +2,14 @@ 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 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.LokiPublicChat import org.whispersystems.signalservice.loki.messaging.LokiThreadDatabaseProtocol import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus @@ -17,11 +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" - private val threadID = "thread_id" + 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 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 createPublicChatTableCommand = "CREATE TABLE $publicChatTableName ($threadID INTEGER PRIMARY KEY, $publicChat TEXT);" } override fun getThreadID(hexEncodedPublicKey: String): Long { @@ -30,7 +36,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 +90,50 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa notifyConversationListListeners() notifyConversationListeners(threadID) } + + fun getAllPublicChats(): Map { + val database = databaseHelper.readableDatabase + var cursor: Cursor? = null + val result = mutableMapOf() + try { + cursor = database.rawQuery("select * from $publicChatTableName", null) + while (cursor != null && cursor.moveToNext()) { + val threadID = cursor.getLong(threadID) + val string = cursor.getString(publicChat) + val publicChat = LokiPublicChat.fromJSON(string) + if (publicChat != null) { result[threadID] = publicChat } + } + } catch (e: Exception) { + // Do nothing + } finally { + cursor?.close() + } + return result + } + + fun getAllPublicChatServers(): Set { + 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(publicChatTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor -> + val publicChatAsJSON = cursor.getString(publicChat) + LokiPublicChat.fromJSON(publicChatAsJSON) + } + } + + 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.publicChat, JsonUtil.toJson(publicChat.toJSON())) + database.insertOrUpdate(publicChatTableName, contentValues, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) + } + + override fun removePublicChat(threadID: Long) { + databaseHelper.writableDatabase.delete(publicChatTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) + } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt index 9465d2cfa5..fd01cb778e 100644 --- a/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt +++ b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionView.kt @@ -13,7 +13,10 @@ import org.whispersystems.signalservice.loki.messaging.Mention class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) { private var mentionCandidates = listOf() set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue } - private var hasGroupContext = false + var publicChatServer: String? = null + set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.publicChatServer = publicChatServer } + var publicChatChannel: Long? = null + set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.publicChatChannel = publicChatChannel } var onMentionCandidateSelected: ((Mention) -> Unit)? = null private val mentionCandidateSelectionViewAdapter by lazy { Adapter(context) } @@ -21,7 +24,8 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS private class Adapter(private val context: Context) : BaseAdapter() { var mentionCandidates = listOf() set(newValue) { field = newValue; notifyDataSetChanged() } - var hasGroupContext = false + var publicChatServer: String? = null + var publicChatChannel: Long? = null override fun getCount(): Int { return mentionCandidates.count() @@ -39,7 +43,8 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS val cell = cellToBeReused as MentionCandidateSelectionViewCell? ?: MentionCandidateSelectionViewCell.inflate(LayoutInflater.from(context), parent) val mentionCandidate = getItem(position) cell.mentionCandidate = mentionCandidate - cell.hasGroupContext = hasGroupContext + cell.publicChatServer = publicChatServer + cell.publicChatChannel = publicChatChannel return cell } } @@ -56,7 +61,11 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS } fun show(mentionCandidates: 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.mentionCandidates = mentionCandidates val layoutParams = this.layoutParams as ViewGroup.LayoutParams layoutParams.height = toPx(6 + Math.min(mentionCandidates.count(), 4) * 52, resources) diff --git a/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt index 8656948d59..c8bfcc1f81 100644 --- a/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt +++ b/src/org/thoughtcrime/securesms/loki/MentionCandidateSelectionViewCell.kt @@ -10,13 +10,14 @@ import android.view.ViewOutlineProvider import android.widget.LinearLayout import kotlinx.android.synthetic.main.cell_mention_candidate_selection_view.view.* import network.loki.messenger.R -import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI +import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI import org.whispersystems.signalservice.loki.messaging.Mention class MentionCandidateSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { var mentionCandidate = Mention("", "") 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 MentionCandidateSelectionViewCell(context: Context, attrs: AttributeSet?, private fun update() { displayNameTextView.text = mentionCandidate.displayName profilePictureImageView.update(mentionCandidate.hexEncodedPublicKey) - val isUserModerator = LokiGroupChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, LokiGroupChatAPI.publicChatServerID, LokiGroupChatAPI.publicChatServer) - moderatorIconImageView.visibility = if (isUserModerator && hasGroupContext) View.VISIBLE else View.GONE + if (publicChatServer != null && publicChatChannel != null) { + val isUserModerator = LokiPublicChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, publicChatChannel!!, publicChatServer!!) + moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE + } else { + moderatorIconImageView.visibility = View.GONE + } } } \ 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 187694d3ff..82ded332cb 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 hexEncodedPublicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @ val userDisplayName: String? = if (hexEncodedPublicKey.toLowerCase() == TextSecurePreferences.getLocalNumber(context).toLowerCase()) { TextSecurePreferences.getProfileName(context) + } else if (publicChat != null) { + DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, hexEncodedPublicKey) } else { - val publicChatID = LokiGroupChatAPI.publicChatServer + "." + LokiGroupChatAPI.publicChatServerID - DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChatID, hexEncodedPublicKey) + DatabaseFactory.getLokiUserDatabase(context).getDisplayName(hexEncodedPublicKey) } if (userDisplayName != null) { text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length) diff --git a/src/org/thoughtcrime/securesms/sms/MessageSender.java b/src/org/thoughtcrime/securesms/sms/MessageSender.java index f43c973b35..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(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(recipientPublicKey)) { + if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey)) { PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress()); return; }