Merge pull request #35 from loki-project/custom-server

Custom Public Chats
This commit is contained in:
gmbnt 2019-10-16 09:19:20 +11:00 committed by GitHub
commit e8e539c425
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 536 additions and 159 deletions

View File

@ -479,6 +479,10 @@
<activity android:name="org.thoughtcrime.securesms.loki.NewConversationActivity" <activity android:name="org.thoughtcrime.securesms.loki.NewConversationActivity"
android:windowSoftInputMode="stateAlwaysVisible" android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" /> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name="org.thoughtcrime.securesms.loki.AddPublicChatActivity"
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<!-- Loki --> <!-- Loki -->
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/> <service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.LabeledEditText
android:id="@+id/urlEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:labeledEditText_background="@color/loki_darkest_gray"
app:labeledEditText_label="@string/fragment_add_public_chat_url_edit_text_label"/>
<TextView
android:id="@+id/explanationTextView"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/fragment_add_public_chat_explanation" />
<com.dd.CircularProgressButton
android:id="@+id/addButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="32dp"
android:layout_gravity="center_horizontal"
android:background="@color/signal_primary"
android:textColor="@color/white"
app:cpb_colorIndicator="@color/white"
app:cpb_colorProgress="@color/textsecure_primary"
app:cpb_cornerRadius="4dp"
app:cpb_selectorIdle="@drawable/progress_button_state"
app:cpb_textIdle="@string/fragment_add_public_chat_add_button_title_1" />
</LinearLayout>
</ScrollView>

View File

@ -1,7 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/text_secure_normal__menu_new_group" <item android:title="@string/activity_conversation_list_add_public_chat_button_title"
android:id="@+id/menu_conversation_list_add_public_chat_option"
android:icon="@drawable/ic_group_white_24dp"
app:showAsAction="always" />
<!-- <item android:title="@string/text_secure_normal__menu_new_group"
android:id="@+id/menu_new_group" /> android:id="@+id/menu_new_group" />
<item android:title="@string/text_secure_normal__menu_clear_passphrase" <item android:title="@string/text_secure_normal__menu_clear_passphrase"
@ -17,6 +23,6 @@
android:id="@+id/menu_settings" /> android:id="@+id/menu_settings" />
<item android:title="@string/text_secure_normal__help" <item android:title="@string/text_secure_normal__help"
android:id="@+id/menu_help"/> android:id="@+id/menu_help"/> -->
</menu> </menu>

View File

@ -1598,6 +1598,14 @@
<string name="fragment_new_conversation_next_button_title">Next</string> <string name="fragment_new_conversation_next_button_title">Next</string>
<string name="fragment_new_conversation_invalid_public_key_message">Invalid public key</string> <string name="fragment_new_conversation_invalid_public_key_message">Invalid public key</string>
<string name="fragment_new_conversation_note_to_self_not_supported_message">Please enter the public key of the person you\'d like to message</string> <string name="fragment_new_conversation_note_to_self_not_supported_message">Please enter the public key of the person you\'d like to message</string>
<!-- Add public chat activity -->
<string name="fragment_add_public_chat_title">Add Public Chat</string>
<string name="fragment_add_public_chat_url_edit_text_label">URL</string>
<string name="fragment_add_public_chat_explanation">Enter the URL of the public chat you\'d like to join. The Loki Public Chat URL is https://chat.lokinet.org.</string>
<string name="fragment_add_public_chat_add_button_title_1">Add</string>
<string name="fragment_add_public_chat_add_button_title_2">Adding Server...</string>
<string name="fragment_add_public_chat_invalid_url_message">Invalid URL</string>
<string name="fragment_add_public_chat_connection_failed_message">Couldn\'t Connect</string>
<!-- Friend request view --> <!-- Friend request view -->
<string name="view_friend_request_accept_button_title">Accept</string> <string name="view_friend_request_accept_button_title">Accept</string>
<string name="view_friend_request_reject_button_title">Reject</string> <string name="view_friend_request_reject_button_title">Reject</string>
@ -1631,5 +1639,7 @@
<string name="fragment_scan_qr_code_camera_permission_dialog_message">Loki Messenger needs camera access to scan QR codes.</string> <string name="fragment_scan_qr_code_camera_permission_dialog_message">Loki Messenger needs camera access to scan QR codes.</string>
<!-- Conversation activity --> <!-- Conversation activity -->
<string name="activity_conversation_copy_public_key_button_title">Copy public key</string> <string name="activity_conversation_copy_public_key_button_title">Copy public key</string>
<!-- Conversation list activity -->
<string name="activity_conversation_list_add_public_chat_button_title">Add Public Chat</string>
</resources> </resources>

View File

@ -42,8 +42,8 @@
android:icon="@drawable/icon_qr_code"/> android:icon="@drawable/icon_qr_code"/>
<Preference android:key="preference_category_link_device" <Preference android:key="preference_category_link_device"
android:title="@string/activity_settings_link_device_button_title" android:title="@string/activity_settings_link_device_button_title"
android:icon="@drawable/icon_link"/> android:icon="@drawable/icon_link"/>
<Preference android:key="preference_category_seed" <Preference android:key="preference_category_seed"
android:title="@string/activity_settings_show_seed_button_title" android:title="@string/activity_settings_show_seed_button_title"

View File

@ -25,6 +25,7 @@ import android.database.ContentObserver;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.multidex.MultiDexApplication; import android.support.multidex.MultiDexApplication;
import com.crashlytics.android.Crashlytics; 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.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.loki.BackgroundPollWorker; import org.thoughtcrime.securesms.loki.BackgroundPollWorker;
import org.thoughtcrime.securesms.loki.LokiAPIDatabase; 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.LokiRSSFeedPoller;
import org.thoughtcrime.securesms.loki.LokiUserDatabase;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
@ -85,8 +87,8 @@ import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol; import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol;
import org.whispersystems.signalservice.loki.api.LokiGroupChat; import org.whispersystems.signalservice.loki.api.LokiPublicChat;
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI;
import org.whispersystems.signalservice.loki.api.LokiLongPoller; import org.whispersystems.signalservice.loki.api.LokiLongPoller;
import org.whispersystems.signalservice.loki.api.LokiP2PAPI; import org.whispersystems.signalservice.loki.api.LokiP2PAPI;
import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate; import org.whispersystems.signalservice.loki.api.LokiP2PAPIDelegate;
@ -98,6 +100,7 @@ import java.security.Security;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -132,9 +135,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
// Loki // Loki
private LokiLongPoller lokiLongPoller = null; private LokiLongPoller lokiLongPoller = null;
private LokiGroupChatPoller lokiPublicChatPoller = null;
private LokiRSSFeedPoller lokiNewsFeedPoller = null; private LokiRSSFeedPoller lokiNewsFeedPoller = null;
private LokiRSSFeedPoller lokiMessengerUpdatesFeedPoller = null; private LokiRSSFeedPoller lokiMessengerUpdatesFeedPoller = null;
private LokiPublicChatManager lokiPublicChatManager = null;
private LokiPublicChatAPI lokiPublicChatAPI = null;
public SignalCommunicationModule communicationModule; public SignalCommunicationModule communicationModule;
public MixpanelAPI mixpanel; public MixpanelAPI mixpanel;
@ -147,7 +151,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
LokiGroupChatAPI.Companion.setDebugMode(BuildConfig.DEBUG); // Loki - Set debug mode if needed
startKovenant(); startKovenant();
Log.i(TAG, "onCreate()"); Log.i(TAG, "onCreate()");
initializeSecurityProvider(); initializeSecurityProvider();
@ -183,6 +186,8 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
mixpanel.trackMap(event, properties); mixpanel.trackMap(event, properties);
return Unit.INSTANCE; return Unit.INSTANCE;
}; };
// Loki - Set up public chat manager
lokiPublicChatManager = new LokiPublicChatManager(this);
} }
@Override @Override
@ -193,6 +198,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
KeyCachingService.onAppForegrounded(this); KeyCachingService.onAppForegrounded(this);
// Loki - Start long polling if needed // Loki - Start long polling if needed
startLongPollingIfNeeded(); startLongPollingIfNeeded();
lokiPublicChatManager.startPollersIfNeeded();
setUpStorageAPIIfNeeded(); setUpStorageAPIIfNeeded();
} }
@ -204,6 +210,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
MessageNotifier.setVisibleThread(-1); MessageNotifier.setVisibleThread(-1);
// Loki - Stop long polling if needed // Loki - Stop long polling if needed
if (lokiLongPoller != null) { lokiLongPoller.stopIfNeeded(); } if (lokiLongPoller != null) { lokiLongPoller.stopIfNeeded(); }
if (lokiPublicChatManager != null) { lokiPublicChatManager.stopPollers(); }
} }
@Override @Override
@ -243,6 +250,21 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
return persistentLogger; return persistentLogger;
} }
public LokiPublicChatManager getLokiPublicChatManager() {
return lokiPublicChatManager;
}
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);
lokiPublicChatAPI = new LokiPublicChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase);
}
return lokiPublicChatAPI;
}
private void initializeSecurityProvider() { private void initializeSecurityProvider() {
try { try {
Class.forName("org.signal.aesgcmprovider.AesGcmCipher"); Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
@ -471,10 +493,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
if (lokiLongPoller != null) { lokiLongPoller.startIfNeeded(); } if (lokiLongPoller != null) { lokiLongPoller.startIfNeeded(); }
} }
private LokiGroupChat lokiPublicChat() {
return new LokiGroupChat(LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer(), "Loki Public Chat", true);
}
private LokiRSSFeed lokiNewsFeed() { private LokiRSSFeed lokiNewsFeed() {
return new LokiRSSFeed("loki.network.feed", "https://loki.network/feed/", "Loki News", true); return new LokiRSSFeed("loki.network.feed", "https://loki.network/feed/", "Loki News", true);
} }
@ -483,12 +501,22 @@ 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); return new LokiRSSFeed("loki.network.messenger-updates.feed", "https://loki.network/category/messenger-updates/feed", "Loki Messenger Updates", false);
} }
public void createGroupChatsIfNeeded() { public void createDefaultPublicChatsIfNeeded() {
LokiGroupChat publicChat = lokiPublicChat(); List<LokiPublicChat> defaultPublicChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG);
boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publicChat.getId()); for (LokiPublicChat publiChat : defaultPublicChats) {
if (!isChatSetUp || !publicChat.isDeletable()) { long threadID = GroupManager.getThreadId(publiChat.getId(), this);
GroupManager.GroupActionResult result = GroupManager.createGroup(publicChat.getId(), this, new HashSet<>(), null, publicChat.getDisplayName(), false); String migrationKey = publiChat.getId() + "_migrated";
TextSecurePreferences.markChatSetUp(this, publicChat.getId()); 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() { private void createRSSFeedPollersIfNeeded() {
// Only create the RSS feed pollers if their threads aren't deleted // Only create the RSS feed pollers if their threads aren't deleted
LokiRSSFeed lokiNewsFeed = lokiNewsFeed(); LokiRSSFeed lokiNewsFeed = lokiNewsFeed();
@ -558,11 +572,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
this.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadID), true, observer); this.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(threadID), true, observer);
} }
public void startGroupChatPollersIfNeeded() {
createGroupChatPollersIfNeeded();
if (lokiPublicChatPoller != null) lokiPublicChatPoller.startIfNeeded();
}
public void startRSSFeedPollersIfNeeded() { public void startRSSFeedPollersIfNeeded() {
createRSSFeedPollersIfNeeded(); createRSSFeedPollersIfNeeded();
if (lokiNewsFeedPoller != null) lokiNewsFeedPoller.startIfNeeded(); if (lokiNewsFeedPoller != null) lokiNewsFeedPoller.startIfNeeded();

View File

@ -29,6 +29,7 @@ import android.support.annotation.NonNull;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.support.v7.widget.TooltipCompat; import android.support.v7.widget.TooltipCompat;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; 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.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
import org.thoughtcrime.securesms.lock.RegistrationLockDialog; import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
import org.thoughtcrime.securesms.loki.AddPublicChatActivity;
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable; import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
@ -82,9 +84,9 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
dynamicLanguage.onCreate(this); dynamicLanguage.onCreate(this);
if (TextSecurePreferences.getLocalNumber(this) != null) { if (TextSecurePreferences.getLocalNumber(this) != null) {
ApplicationContext application = ApplicationContext.getInstance(this); ApplicationContext application = ApplicationContext.getInstance(this);
application.createGroupChatsIfNeeded(); application.createDefaultPublicChatsIfNeeded();
application.createRSSFeedsIfNeeded(); application.createRSSFeedsIfNeeded();
application.startGroupChatPollersIfNeeded(); application.getLokiPublicChatManager().startPollersIfNeeded();
application.startRSSFeedPollersIfNeeded(); application.startRSSFeedPollersIfNeeded();
} }
} }
@ -127,18 +129,15 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
return false;
/*
MenuInflater inflater = this.getMenuInflater(); MenuInflater inflater = this.getMenuInflater();
menu.clear(); menu.clear();
inflater.inflate(R.menu.text_secure_normal, menu); 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); super.onPrepareOptionsMenu(menu);
return true; return true;
*/
} }
private void initializeSearchListener() { private void initializeSearchListener() {
@ -235,12 +234,13 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
super.onOptionsItemSelected(item); super.onOptionsItemSelected(item);
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.menu_new_group: createGroup(); return true; // case R.id.menu_new_group: createGroup(); return true;
case R.id.menu_settings: handleDisplaySettings(); return true; // case R.id.menu_settings: handleDisplaySettings(); return true;
case R.id.menu_clear_passphrase: handleClearPassphrase(); 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_mark_all_read: handleMarkAllRead(); return true;
case R.id.menu_invite: handleInvite(); return true; // case R.id.menu_invite: handleInvite(); return true;
case R.id.menu_help: handleHelp(); 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; 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(); 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));
}
} }

View File

@ -273,7 +273,7 @@ public class ConversationListItem extends RelativeLayout
private @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) { private @NonNull CharSequence getTrimmedSnippet(@NonNull CharSequence snippet) {
LokiAPIUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(threadId, getContext()); // TODO: Terrible place to do this, but okay for now 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); return snippet.length() <= MAX_SNIPPET_LENGTH ? snippet : snippet.subSequence(0, MAX_SNIPPET_LENGTH);
} }

View File

@ -35,15 +35,12 @@ import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle; import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard; import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceProfileKeyUpdateJob;
import org.thoughtcrime.securesms.logging.Log; 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.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.AvatarHelper; 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.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.util.StreamDetails; 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 org.whispersystems.signalservice.loki.utilities.Analytics;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import javax.inject.Inject; import javax.inject.Inject;
@ -380,12 +378,13 @@ public class CreateProfileActivity extends BaseActionBarActivity implements Inje
Analytics.Companion.getShared().track("Display Name Updated"); Analytics.Companion.getShared().track("Display Name Updated");
TextSecurePreferences.setProfileName(context, name); TextSecurePreferences.setProfileName(context, name);
LokiPublicChatAPI publicChatAPI = ApplicationContext.getInstance(context).getLokiPublicChatAPI();
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context); if (publicChatAPI != null) {
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).getPrivateKey().serialize(); Set<String> servers = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChatServers();
LokiAPIDatabase apiDatabase = DatabaseFactory.getLokiAPIDatabase(context); for (String server : servers) {
LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(context); publicChatAPI.setDisplayName(name, server);
new LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase).setDisplayName(name, LokiGroupChatAPI.getPublicChatServer()); }
}
// Loki - Original code // Loki - Original code
// ======== // ========

View File

@ -28,10 +28,10 @@ import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; 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.ThemeUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI; import org.whispersystems.signalservice.loki.api.LokiPublicChat;
import java.util.List; import java.util.List;
@ -194,19 +194,16 @@ public class QuoteView extends FrameLayout implements RecipientModifiedListener
boolean isOwnNumber = Util.isOwnNumber(getContext(), author.getAddress()); boolean isOwnNumber = Util.isOwnNumber(getContext(), author.getAddress());
String quoteeDisplayName = author.toShortString(); 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 long threadID = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(conversationRecipient);
if (conversationRecipient.isGroupRecipient()) { String senderHexEncodedPublicKey = author.getAddress().serialize();
try { LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadID);
String serverId = GroupUtil.getDecodedStringId(conversationRecipient.getAddress().serialize()); if (senderHexEncodedPublicKey.equalsIgnoreCase(TextSecurePreferences.getLocalNumber(getContext()))) {
String senderDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(serverId, author.getAddress().serialize()); quoteeDisplayName = TextSecurePreferences.getProfileName(getContext());
if (senderDisplayName != null) { quoteeDisplayName = senderDisplayName; } } else if (publicChat != null) {
} catch (Exception e) { quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getServerDisplayName(publicChat.getId(), senderHexEncodedPublicKey);
// Do nothing } else {
} quoteeDisplayName = DatabaseFactory.getLokiUserDatabase(getContext()).getDisplayName(senderHexEncodedPublicKey);
} }
authorView.setText(isOwnNumber ? getContext().getString(R.string.QuoteView_you) : quoteeDisplayName); authorView.setText(isOwnNumber ? getContext().getString(R.string.QuoteView_you) : quoteeDisplayName);

View File

@ -157,6 +157,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate; import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate;
import org.thoughtcrime.securesms.loki.LokiAPIUtilities; import org.thoughtcrime.securesms.loki.LokiAPIUtilities;
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.LokiThreadDatabaseDelegate; import org.thoughtcrime.securesms.loki.LokiThreadDatabaseDelegate;
import org.thoughtcrime.securesms.loki.LokiUserDatabase; import org.thoughtcrime.securesms.loki.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.MentionCandidateSelectionView; import org.thoughtcrime.securesms.loki.MentionCandidateSelectionView;
@ -2765,24 +2766,25 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (isBackspace) { if (isBackspace) {
currentMentionStartIndex = -1; currentMentionStartIndex = -1;
mentionCandidateSelectionView.hide(); mentionCandidateSelectionView.hide();
try { ArrayList<Mention> mentionsToRemove = new ArrayList<>();
for (Mention mention : mentions) { for (Mention mention : mentions) {
if (!text.contains(mention.getDisplayName())) { if (!text.contains(mention.getDisplayName())) {
mentions.remove(mention); 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()) { if (currentMentionStartIndex > text.length()) {
resetMentions(); // Should never occur resetMentions(); // Should never occur
} }
int lastCharacterIndex = text.length() - 1; int lastCharacterIndex = text.length() - 1;
char lastCharacter = text.charAt(lastCharacterIndex); char lastCharacter = text.charAt(lastCharacterIndex);
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(ConversationActivity.this);
LokiThreadDatabase threadDatabase = DatabaseFactory.getLokiThreadDatabase(ConversationActivity.this);
LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(ConversationActivity.this); LokiUserDatabase userDatabase = DatabaseFactory.getLokiUserDatabase(ConversationActivity.this);
if (lastCharacter == '@') { if (lastCharacter == '@') {
List<Mention> mentionCandidates = LokiAPI.Companion.getMentionCandidates("", threadId, userDatabase); List<Mention> mentionCandidates = LokiAPI.Companion.getMentionCandidates("", threadId, userHexEncodedPublicKey, threadDatabase, userDatabase);
currentMentionStartIndex = lastCharacterIndex; currentMentionStartIndex = lastCharacterIndex;
mentionCandidateSelectionView.show(mentionCandidates, threadId); mentionCandidateSelectionView.show(mentionCandidates, threadId);
} else if (Character.isWhitespace(lastCharacter)) { } else if (Character.isWhitespace(lastCharacter)) {
@ -2791,7 +2793,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} else { } else {
if (currentMentionStartIndex != -1) { if (currentMentionStartIndex != -1) {
String query = text.substring(currentMentionStartIndex + 1); // + 1 to get rid of the @ String query = text.substring(currentMentionStartIndex + 1); // + 1 to get rid of the @
List<Mention> mentionCandidates = LokiAPI.Companion.getMentionCandidates(query, threadId, userDatabase); List<Mention> mentionCandidates = LokiAPI.Companion.getMentionCandidates(query, threadId, userHexEncodedPublicKey, threadDatabase, userDatabase);
mentionCandidateSelectionView.show(mentionCandidates, threadId); mentionCandidateSelectionView.show(mentionCandidates, threadId);
} }
} }

View File

@ -68,7 +68,6 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity; import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder; import org.thoughtcrime.securesms.conversation.ConversationAdapter.HeaderViewHolder;
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener; import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase; 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.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.loki.FriendRequestViewDelegate; 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.longmessage.LongMessageActivity;
import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.GlideApp; 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.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.util.concurrent.SettableFuture; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -408,14 +406,15 @@ public class ConversationFragment extends Fragment
boolean isGroupChat = recipient.isGroupRecipient(); boolean isGroupChat = recipient.isGroupRecipient();
if (isGroupChat) { 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(); int selectedMessageCount = messageRecords.size();
boolean isSentByUser = ((MessageRecord)messageRecords.toArray()[0]).isOutgoing(); 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_copy_public_key).setVisible(isPublicChat && selectedMessageCount == 1 && !isSentByUser);
menu.findItem(R.id.menu_context_reply).setVisible(isLokiPublicChat && selectedMessageCount == 1); menu.findItem(R.id.menu_context_reply).setVisible(isPublicChat && selectedMessageCount == 1);
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext());
boolean userCanModerate = LokiGroupChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer()); boolean userCanModerate = isPublicChat && LokiPublicChatAPI.Companion.isUserModerator(userHexEncodedPublicKey, publicChat.getChannel(), publicChat.getServer());
boolean isDeleteOptionVisible = isLokiPublicChat && selectedMessageCount == 1 && (isSentByUser || userCanModerate); boolean isDeleteOptionVisible = isPublicChat && selectedMessageCount == 1 && (isSentByUser || userCanModerate);
menu.findItem(R.id.menu_context_delete_message).setVisible(isDeleteOptionVisible); menu.findItem(R.id.menu_context_delete_message).setVisible(isDeleteOptionVisible);
} else { } else {
menu.findItem(R.id.menu_context_copy_public_key).setVisible(false); 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.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
builder.setCancelable(true); builder.setCancelable(true);
// Loki - The delete option is only visible to the user in a group chat if it's the Loki Public Chat // Loki - The delete option is only visible to the user in a public chat
boolean isLokiPublicChat = this.recipient.isGroupRecipient(); LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(getContext()).getPublicChat(threadId);
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
@Override @Override
@ -524,19 +523,16 @@ public class ConversationFragment extends Fragment
for (MessageRecord messageRecord : messageRecords) { for (MessageRecord messageRecord : messageRecords) {
boolean isThreadDeleted; boolean isThreadDeleted;
if (isLokiPublicChat) { if (publicChat != null) {
final SettableFuture<?>[] future = { new SettableFuture<Unit>() }; final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(getContext()); LokiPublicChatAPI publicChatAPI = ApplicationContext.getInstance(getContext()).getLokiPublicChatAPI();
LokiAPIDatabase lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(getContext());
LokiUserDatabase lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(getContext());
byte[] userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(getContext()).getPrivateKey().serialize();
boolean isSentByUser = messageRecord.isOutgoing(); boolean isSentByUser = messageRecord.isOutgoing();
Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id); Long serverID = DatabaseFactory.getLokiMessageDatabase(getContext()).getServerID(messageRecord.id);
if (serverID != null) { if (publicChatAPI != null && serverID != null) {
new LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase) publicChatAPI
.deleteMessage(serverID, LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer(), isSentByUser) .deleteMessage(serverID, publicChat.getChannel(), publicChat.getServer(), isSentByUser)
.success(l -> { .success(l -> {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>) future[0]; @SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>) future[0];
f.set(Unit.INSTANCE); f.set(Unit.INSTANCE);

View File

@ -113,7 +113,8 @@ import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub; import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional; 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.Collections;
import java.util.HashSet; import java.util.HashSet;
@ -473,7 +474,7 @@ public class ConversationItem extends LinearLayout
if (isCaptionlessMms(messageRecord)) { if (isCaptionlessMms(messageRecord)) {
bodyText.setVisibility(View.GONE); bodyText.setVisibility(View.GONE);
} else { ; } 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 BackgroundColorSpan(Color.YELLOW), text, searchQuery);
text = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), 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) { if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) {
Quote quote = ((MediaMmsMessageRecord)current).getQuote(); Quote quote = ((MediaMmsMessageRecord)current).getQuote();
//noinspection ConstantConditions //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.setQuote(glideRequests, quote.getId(), Recipient.from(context, quote.getAuthor(), true), quoteBody, quote.isOriginalMissing(), quote.getAttachment(), conversationRecipient);
quoteView.setVisibility(View.VISIBLE); quoteView.setVisibility(View.VISIBLE);
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; 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())) { if (!next.isPresent() || next.get().isUpdate() || !current.getRecipient().getAddress().equals(next.get().getRecipient().getAddress())) {
contactPhoto.setVisibility(VISIBLE); contactPhoto.setVisibility(VISIBLE);
int visibility; int visibility = View.GONE;
if (conversationRecipient.getName() != null && conversationRecipient.getName().equals("Loki Public Chat")) {
boolean isModerator = LokiGroupChatAPI.Companion.isUserModerator(current.getRecipient().getAddress().toString(), LokiGroupChatAPI.getPublicChatServerID(), LokiGroupChatAPI.getPublicChatServer()); 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; visibility = isModerator ? View.VISIBLE : View.GONE;
} else {
visibility = View.GONE;
} }
moderatorIconImageView.setVisibility(visibility); moderatorIconImageView.setVisibility(visibility);
} else { } else {
contactPhoto.setVisibility(GONE); contactPhoto.setVisibility(GONE);

View File

@ -52,9 +52,17 @@ public class Address implements Parcelable, Comparable<Address> {
private final String address; 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) { private Address(@NonNull String address) {
this(address, false);
}
private Address(@NonNull String address, Boolean isPublicChat) {
if (address == null) throw new AssertionError(address); if (address == null) throw new AssertionError(address);
this.address = address; this.address = address;
this.isPublicChat = isPublicChat;
} }
public Address(Parcel in) { public Address(Parcel in) {
@ -69,6 +77,10 @@ public class Address implements Parcelable, Comparable<Address> {
return Address.fromSerialized(external); return Address.fromSerialized(external);
} }
public static @NonNull Address fromPublicChatGroupID(@NonNull String serialized) {
return new Address(serialized, true);
}
public static @NonNull List<Address> fromSerializedList(@NonNull String serialized, char delimiter) { public static @NonNull List<Address> fromSerializedList(@NonNull String serialized, char delimiter) {
String[] escapedAddresses = DelimiterUtil.split(serialized, delimiter); String[] escapedAddresses = DelimiterUtil.split(serialized, delimiter);
List<Address> addresses = new LinkedList<>(); List<Address> addresses = new LinkedList<>();
@ -131,7 +143,7 @@ public class Address implements Parcelable, Comparable<Address> {
} }
public @NonNull String toPhoneString() { public @NonNull String toPhoneString() {
if (!isPhone()) { if (!isPhone() && !isPublicChat) {
if (isEmail()) throw new AssertionError("Not e164, is email"); if (isEmail()) throw new AssertionError("Not e164, is email");
if (isGroup()) throw new AssertionError("Not e164, is group"); if (isGroup()) throw new AssertionError("Not e164, is group");
throw new AssertionError("Not e164, unknown"); throw new AssertionError("Not e164, unknown");

View File

@ -71,7 +71,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV2 = 23; private static final int lokiV2 = 23;
private static final int lokiV3 = 24; 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 static final String DATABASE_NAME = "signal.db";
private final Context context; private final Context context;
@ -131,6 +131,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiMessageDatabase.getCreateTableCommand()); db.execSQL(LokiMessageDatabase.getCreateTableCommand());
db.execSQL(LokiThreadDatabase.getCreateFriendRequestTableCommand()); db.execSQL(LokiThreadDatabase.getCreateFriendRequestTableCommand());
db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand()); db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand());
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand());
db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand());
@ -497,6 +498,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
if (oldVersion < lokiV3) { if (oldVersion < lokiV3) {
db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand()); db.execSQL(LokiAPIDatabase.getCreatePairingAuthorisationTableCommand());
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
} }
db.setTransactionSuccessful(); db.setTransactionSuccessful();

View File

@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager; 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.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; 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.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -285,7 +286,16 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
private @NonNull List<Address> getGroupMessageRecipients(String groupId, long messageId) { private @NonNull List<Address> getGroupMessageRecipients(String groupId, long messageId) {
ArrayList<Address> result = new ArrayList<>(); ArrayList<Address> 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; return result;
/* /*

View File

@ -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
}
}

View File

@ -50,6 +50,10 @@ fun Cursor.getString(columnName: String): String {
return getString(getColumnIndexOrThrow(columnName)) return getString(getColumnIndexOrThrow(columnName))
} }
fun Cursor.getLong(columnName: String): Long {
return getLong(getColumnIndexOrThrow(columnName))
}
fun Cursor.getBase64EncodedData(columnName: String): ByteArray { fun Cursor.getBase64EncodedData(columnName: String): ByteArray {
return Base64.decode(getString(columnName)) return Base64.decode(getString(columnName))
} }

View File

@ -8,11 +8,9 @@ import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.BaseActionBarActivity
import org.thoughtcrime.securesms.ConversationListActivity import org.thoughtcrime.securesms.ConversationListActivity
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.crypto.ProfileCipher import org.whispersystems.signalservice.api.crypto.ProfileCipher
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI
import org.whispersystems.signalservice.loki.utilities.Analytics import org.whispersystems.signalservice.loki.utilities.Analytics
class DisplayNameActivity : BaseActionBarActivity() { class DisplayNameActivity : BaseActionBarActivity() {
@ -45,10 +43,14 @@ class DisplayNameActivity : BaseActionBarActivity() {
application.setUpStorageAPIIfNeeded() application.setUpStorageAPIIfNeeded()
startActivity(Intent(this, ConversationListActivity::class.java)) startActivity(Intent(this, ConversationListActivity::class.java))
finish() finish()
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this) val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(this).privateKey.serialize() if (publicChatAPI != null) {
val apiDatabase = DatabaseFactory.getLokiAPIDatabase(this) application.createDefaultPublicChatsIfNeeded()
val userDatabase = DatabaseFactory.getLokiUserDatabase(this) application.createRSSFeedsIfNeeded()
LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, apiDatabase, userDatabase).setDisplayName(name, LokiGroupChatAPI.publicChatServer) application.lokiPublicChatManager.startPollersIfNeeded()
application.startRSSFeedPollersIfNeeded()
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
servers.forEach { publicChatAPI.setDisplayName(name, it) }
}
} }
} }

View File

@ -7,7 +7,6 @@ import android.support.annotation.ColorRes
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -24,8 +23,8 @@ fun toPx(dp: Int, resources: Resources): Int {
return (dp * scale).roundToInt() return (dp * scale).roundToInt()
} }
fun isGroupRecipient(recipient: String): Boolean { fun isPublicChat(context: Context, recipient: String): Boolean {
return (LokiGroupChatAPI.publicChatServer == recipient) return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().values.map { it.server }.contains(recipient)
} }
fun getFriendPublicKeys(context: Context, devicePublicKeys: Set<String>): Set<String> { fun getFriendPublicKeys(context: Context, devicePublicKeys: Set<String>): Set<String> {

View File

@ -137,6 +137,12 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(lastMessageServerIDCache, row, "$lastMessageServerIDCacheIndex = ?", wrap(index)) 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? { override fun getLastDeletionServerID(group: Long, server: String): Long? {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
val index = "$server.$group" val index = "$server.$group"
@ -152,6 +158,12 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.insertOrUpdate(lastDeletionServerIDCache, row, "$lastDeletionServerIDCacheIndex = ?", wrap(index)) 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<PairingAuthorisation> { override fun getPairingAuthorisations(hexEncodedPublicKey: String): List<PairingAuthorisation> {
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
return database.getAll(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) { cursor -> return database.getAll(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey )) { cursor ->

View File

@ -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<Long, LokiPublicChat>()
private val pollers = mutableMapOf<Long, LokiPublicChatPoller>()
private val observers = mutableMapOf<Long, ContentObserver>()
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<LokiPublicChat, Exception> {
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
}
}
}
}
}

View File

@ -21,23 +21,23 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.messages.SignalServiceGroup import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.api.LokiGroupChat import org.whispersystems.signalservice.loki.api.LokiPublicChat
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI
import org.whispersystems.signalservice.loki.api.LokiGroupMessage 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 val handler = Handler()
private var hasStarted = false private var hasStarted = false
// region Convenience // region Convenience
private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context) private val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
private val api: LokiGroupChatAPI private val api: LokiPublicChatAPI
get() = { get() = {
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize() val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context) val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context)
val lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(context) val lokiUserDatabase = DatabaseFactory.getLokiUserDatabase(context)
LokiGroupChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase) LokiPublicChatAPI(userHexEncodedPublicKey, userPrivateKey, lokiAPIDatabase, lokiUserDatabase)
}() }()
// endregion // endregion
@ -94,7 +94,7 @@ class LokiGroupChatPoller(private val context: Context, private val group: LokiG
// region Polling // region Polling
private fun pollForNewMessages() { private fun pollForNewMessages() {
fun processIncomingMessage(message: LokiGroupMessage) { fun processIncomingMessage(message: LokiPublicChatMessage) {
val id = group.id.toByteArray() val id = group.id.toByteArray()
val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null) val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null)
val quote: SignalServiceDataMessage.Quote? 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)) 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 messageServerID = message.serverID ?: return
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context) val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
val isDuplicate = lokiMessageDatabase.getMessageID(messageServerID) != null val isDuplicate = lokiMessageDatabase.getMessageID(messageServerID) != null
@ -161,7 +161,7 @@ class LokiGroupChatPoller(private val context: Context, private val group: LokiG
finalize() finalize()
} }
} }
api.getMessages(group.serverID, group.server).success { messages -> api.getMessages(group.channel, group.server).success { messages ->
messages.forEach { message -> messages.forEach { message ->
if (message.hexEncodedPublicKey != userHexEncodedPublicKey) { if (message.hexEncodedPublicKey != userHexEncodedPublicKey) {
processIncomingMessage(message) processIncomingMessage(message)
@ -170,12 +170,12 @@ class LokiGroupChatPoller(private val context: Context, private val group: LokiG
} }
} }
}.fail { }.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() { 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 lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { lokiMessageDatabase.getMessageID(it) } val deletedMessageIDs = deletedMessageServerIDs.mapNotNull { lokiMessageDatabase.getMessageID(it) }
val smsMessageDatabase = DatabaseFactory.getSmsDatabase(context) val smsMessageDatabase = DatabaseFactory.getSmsDatabase(context)
@ -185,12 +185,12 @@ class LokiGroupChatPoller(private val context: Context, private val group: LokiG
mmsMessageDatabase.delete(it) mmsMessageDatabase.delete(it)
} }
}.fail { }.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() { private fun pollForModerators() {
api.getModerators(group.serverID, group.server) api.getModerators(group.channel, group.server)
} }
// endregion // endregion
} }

View File

@ -2,11 +2,14 @@ package org.thoughtcrime.securesms.loki
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.Cursor
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.recipients.Recipient 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.LokiThreadDatabaseProtocol
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus import org.whispersystems.signalservice.loki.messaging.LokiThreadSessionResetStatus
@ -17,11 +20,14 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
companion object { companion object {
private val friendRequestTableName = "loki_thread_friend_request_database" private val friendRequestTableName = "loki_thread_friend_request_database"
private val sessionResetTableName = "loki_thread_session_reset_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 friendRequestStatus = "friend_request_status"
private val sessionResetStatus = "session_reset_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 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 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 { override fun getThreadID(hexEncodedPublicKey: String): Long {
@ -30,7 +36,7 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
} }
override fun getThreadID(messageID: Long): Long { fun getThreadID(messageID: Long): Long {
return DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID) return DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageID)
} }
@ -84,4 +90,50 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
notifyConversationListListeners() notifyConversationListListeners()
notifyConversationListeners(threadID) notifyConversationListeners(threadID)
} }
fun getAllPublicChats(): Map<Long, LokiPublicChat> {
val database = databaseHelper.readableDatabase
var cursor: Cursor? = null
val result = mutableMapOf<Long, LokiPublicChat>()
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<String> {
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() ))
}
} }

View File

@ -13,7 +13,10 @@ import org.whispersystems.signalservice.loki.messaging.Mention
class MentionCandidateSelectionView(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<Mention>() private var mentionCandidates = listOf<Mention>()
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue } 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 var onMentionCandidateSelected: ((Mention) -> Unit)? = null
private val mentionCandidateSelectionViewAdapter by lazy { Adapter(context) } 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() { private class Adapter(private val context: Context) : BaseAdapter() {
var mentionCandidates = listOf<Mention>() var mentionCandidates = listOf<Mention>()
set(newValue) { field = newValue; notifyDataSetChanged() } set(newValue) { field = newValue; notifyDataSetChanged() }
var hasGroupContext = false var publicChatServer: String? = null
var publicChatChannel: Long? = null
override fun getCount(): Int { override fun getCount(): Int {
return mentionCandidates.count() 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 cell = cellToBeReused as MentionCandidateSelectionViewCell? ?: MentionCandidateSelectionViewCell.inflate(LayoutInflater.from(context), parent)
val mentionCandidate = getItem(position) val mentionCandidate = getItem(position)
cell.mentionCandidate = mentionCandidate cell.mentionCandidate = mentionCandidate
cell.hasGroupContext = hasGroupContext cell.publicChatServer = publicChatServer
cell.publicChatChannel = publicChatChannel
return cell return cell
} }
} }
@ -56,7 +61,11 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
} }
fun show(mentionCandidates: List<Mention>, threadID: Long) { fun show(mentionCandidates: List<Mention>, 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 this.mentionCandidates = mentionCandidates
val layoutParams = this.layoutParams as ViewGroup.LayoutParams val layoutParams = this.layoutParams as ViewGroup.LayoutParams
layoutParams.height = toPx(6 + Math.min(mentionCandidates.count(), 4) * 52, resources) layoutParams.height = toPx(6 + Math.min(mentionCandidates.count(), 4) * 52, resources)

View File

@ -10,13 +10,14 @@ import android.view.ViewOutlineProvider
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.android.synthetic.main.cell_mention_candidate_selection_view.view.* import kotlinx.android.synthetic.main.cell_mention_candidate_selection_view.view.*
import network.loki.messenger.R 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 import org.whispersystems.signalservice.loki.messaging.Mention
class MentionCandidateSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { class MentionCandidateSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
var mentionCandidate = Mention("", "") var mentionCandidate = Mention("", "")
set(newValue) { field = newValue; update() } 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, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context) : this(context, null) constructor(context: Context) : this(context, null)
@ -42,7 +43,11 @@ class MentionCandidateSelectionViewCell(context: Context, attrs: AttributeSet?,
private fun update() { private fun update() {
displayNameTextView.text = mentionCandidate.displayName displayNameTextView.text = mentionCandidate.displayName
profilePictureImageView.update(mentionCandidate.hexEncodedPublicKey) profilePictureImageView.update(mentionCandidate.hexEncodedPublicKey)
val isUserModerator = LokiGroupChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, LokiGroupChatAPI.publicChatServerID, LokiGroupChatAPI.publicChatServer) if (publicChatServer != null && publicChatChannel != null) {
moderatorIconImageView.visibility = if (isUserModerator && hasGroupContext) View.VISIBLE else View.GONE val isUserModerator = LokiPublicChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, publicChatChannel!!, publicChatServer!!)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
} else {
moderatorIconImageView.visibility = View.GONE
}
} }
} }

View File

@ -8,31 +8,32 @@ import android.util.Range
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.api.LokiGroupChatAPI
import java.util.regex.Pattern import java.util.regex.Pattern
object MentionUtilities { object MentionUtilities {
@JvmStatic @JvmStatic
fun highlightMentions(text: CharSequence, isGroupThread: Boolean, context: Context): String { fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String {
return MentionUtilities.highlightMentions(text, false, isGroupThread, context).toString() // isOutgoingMessage is irrelevant return MentionUtilities.highlightMentions(text, false, threadID, context).toString() // isOutgoingMessage is irrelevant
} }
@JvmStatic @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 var text = text
val pattern = Pattern.compile("@[0-9a-fA-F]*") val pattern = Pattern.compile("@[0-9a-fA-F]*")
var matcher = pattern.matcher(text) var matcher = pattern.matcher(text)
val mentions = mutableListOf<Range<Int>>() val mentions = mutableListOf<Range<Int>>()
var startIndex = 0 var startIndex = 0
if (matcher.find(startIndex) && isGroupThread) { val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
if (matcher.find(startIndex)) {
while (true) { while (true) {
val hexEncodedPublicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @ 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()) { val userDisplayName: String? = if (hexEncodedPublicKey.toLowerCase() == TextSecurePreferences.getLocalNumber(context).toLowerCase()) {
TextSecurePreferences.getProfileName(context) TextSecurePreferences.getProfileName(context)
} else if (publicChat != null) {
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, hexEncodedPublicKey)
} else { } else {
val publicChatID = LokiGroupChatAPI.publicChatServer + "." + LokiGroupChatAPI.publicChatServerID DatabaseFactory.getLokiUserDatabase(context).getDisplayName(hexEncodedPublicKey)
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChatID, hexEncodedPublicKey)
} }
if (userDisplayName != null) { if (userDisplayName != null) {
text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length) text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length)

View File

@ -213,7 +213,7 @@ public class MessageSender {
// Just send the message normally if it's a group message // Just send the message normally if it's a group message
String recipientPublicKey = recipient.getAddress().serialize(); String recipientPublicKey = recipient.getAddress().serialize();
if (GeneralUtilitiesKt.isGroupRecipient(recipientPublicKey)) { if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey)) {
jobManager.add(new PushTextSendJob(messageId, recipient.getAddress())); jobManager.add(new PushTextSendJob(messageId, recipient.getAddress()));
return; return;
} }
@ -243,7 +243,7 @@ public class MessageSender {
// Just send the message normally if it's a group message // Just send the message normally if it's a group message
String recipientPublicKey = recipient.getAddress().serialize(); String recipientPublicKey = recipient.getAddress().serialize();
if (GeneralUtilitiesKt.isGroupRecipient(recipientPublicKey)) { if (GeneralUtilitiesKt.isPublicChat(context, recipientPublicKey)) {
PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress()); PushMediaSendJob.enqueue(context, jobManager, messageId, recipient.getAddress());
return; return;
} }