mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-24 16:57:50 +00:00
Merge pull request #60 from loki-project/private-group-chat
Private group chat
This commit is contained in:
commit
75cd2d6d28
@ -158,6 +158,9 @@
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.redesign.activities.CreatePrivateChatActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.redesign.activities.CreateClosedGroupActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.loki.redesign.activities.JoinPublicChatActivity"
|
||||
android:screenOrientation="portrait"
|
||||
|
6
res/drawable/ic_circle.xml
Normal file
6
res/drawable/ic_circle.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<vector android:height="24dp" android:viewportHeight="15"
|
||||
android:viewportWidth="15" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M7.5,7.5m-6.5,0a6.5,6.5 0,1 1,13 0a6.5,6.5 0,1 1,-13 0"
|
||||
android:strokeColor="#FFFFFF" android:strokeWidth="1"/>
|
||||
</vector>
|
30
res/layout/activity_create_closed_group.xml
Normal file
30
res/layout/activity_create_closed_group.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/default_session_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
style="@style/SmallSessionEditText"
|
||||
android:id="@+id/nameEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:layout_marginBottom="@dimen/large_spacing"
|
||||
android:hint="Enter a group name" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="@color/separator" />
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
@ -40,13 +40,27 @@
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="64dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/joinPublicChatButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_globe"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true" />
|
||||
android:layout_centerVertical="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/createClosedGroupButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_group_white_24dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/joinPublicChatButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginLeft="@dimen/medium_spacing"
|
||||
android:src="@drawable/ic_globe" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
53
res/layout/view_user.xml
Normal file
53
res/layout/view_user.xml
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/conversation_view_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="@dimen/medium_spacing">
|
||||
|
||||
<org.thoughtcrime.securesms.loki.redesign.views.ProfilePictureView
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/medium_profile_picture_size"
|
||||
android:layout_height="@dimen/medium_profile_picture_size" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nameTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/medium_spacing"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/text"
|
||||
android:text="Spiderman" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/tickImageView"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginLeft="@dimen/medium_spacing"
|
||||
android:src="@drawable/ic_circle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="@color/separator" />
|
||||
|
||||
</LinearLayout>
|
10
res/menu/menu_create_closed_group.xml
Normal file
10
res/menu/menu_create_closed_group.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:title="Done"
|
||||
android:id="@+id/createClosedGroupButton"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
@ -1073,7 +1073,12 @@
|
||||
<item quantity="one">%1$s joined the group.</item>
|
||||
<item quantity="other">%1$s joined the group.</item>
|
||||
</plurals>
|
||||
<plurals name="GroupUtil_removed_from_the_group">
|
||||
<item quantity="one">%1$s was removed from the group.</item>
|
||||
<item quantity="other">%1$s were removed from the group.</item>
|
||||
</plurals>
|
||||
<string name="GroupUtil_group_name_is_now">Group name is now \'%1$s\'.</string>
|
||||
<string name="GroupUtil_you_were_removed_from_group">You were removed from the group.</string>
|
||||
|
||||
<!-- profile_group_share_view -->
|
||||
<string name="profile_group_share_view__make_your_profile_name_and_photo_visible_to_this_group">Make your profile name and photo visible to this group?</string>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<domain includeSubdomains="true">imaginary.stream</domain>
|
||||
<domain includeSubdomains="true">storage.seed1.loki.network</domain>
|
||||
<domain includeSubdomains="true">storage.seed2.loki.network</domain>
|
||||
<domain includeSubdomains="true">http://public.loki.foundation</domain>
|
||||
<domain includeSubdomains="true">public.loki.foundation:22023</domain>
|
||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
@ -521,18 +521,18 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
|
||||
public void createDefaultPublicChatsIfNeeded() {
|
||||
List<LokiPublicChat> defaultPublicChats = LokiPublicChatAPI.Companion.getDefaultChats(BuildConfig.DEBUG);
|
||||
for (LokiPublicChat publiChat : defaultPublicChats) {
|
||||
long threadID = GroupManager.getThreadId(publiChat.getId(), this);
|
||||
String migrationKey = publiChat.getId() + "_migrated";
|
||||
for (LokiPublicChat publicChat : defaultPublicChats) {
|
||||
long threadID = GroupManager.getPublicChatThreadId(publicChat.getId(), this);
|
||||
String migrationKey = publicChat.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());
|
||||
boolean isChatSetUp = TextSecurePreferences.isChatSetUp(this, publicChat.getId());
|
||||
if (!isChatSetUp || !publicChat.isDeletable()) {
|
||||
lokiPublicChatManager.addChat(publicChat.getServer(), publicChat.getChannel(), publicChat.getDisplayName());
|
||||
TextSecurePreferences.markChatSetUp(this, publicChat.getId());
|
||||
TextSecurePreferences.setBooleanPreference(this, migrationKey, true);
|
||||
} else if (threadID > -1 && !isChatMigrated) {
|
||||
// Migrate the old public chats
|
||||
DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(publiChat, threadID);
|
||||
DatabaseFactory.getLokiThreadDatabase(this).setPublicChat(publicChat, threadID);
|
||||
TextSecurePreferences.setBooleanPreference(this, migrationKey, true);
|
||||
}
|
||||
}
|
||||
@ -545,7 +545,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
for (LokiRSSFeed feed : feeds) {
|
||||
boolean isFeedSetUp = TextSecurePreferences.isChatSetUp(this, feed.getId());
|
||||
if (!isFeedSetUp || !feed.isDeletable()) {
|
||||
GroupManager.createGroup(feed.getId(), this, new HashSet<>(), null, feed.getDisplayName(), false);
|
||||
GroupManager.createRSSFeedGroup(feed.getId(), this, null, feed.getDisplayName());
|
||||
TextSecurePreferences.markChatSetUp(this, feed.getId());
|
||||
}
|
||||
}
|
||||
@ -554,7 +554,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
||||
private void createRSSFeedPollersIfNeeded() {
|
||||
// Only create the RSS feed pollers if their threads aren't deleted
|
||||
LokiRSSFeed lokiNewsFeed = lokiNewsFeed();
|
||||
long lokiNewsFeedThreadID = GroupManager.getThreadId(lokiNewsFeed.getId(), this);
|
||||
long lokiNewsFeedThreadID = GroupManager.getRSSFeedThreadId(lokiNewsFeed.getId(), this);
|
||||
if (lokiNewsFeedThreadID >= 0 && lokiNewsFeedPoller == null) {
|
||||
lokiNewsFeedPoller = new LokiRSSFeedPoller(this, lokiNewsFeed);
|
||||
// Set up deletion listeners if needed
|
||||
|
@ -72,6 +72,7 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@ -246,7 +247,8 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
return;
|
||||
}
|
||||
if (isSignalGroup()) {
|
||||
new CreateSignalGroupTask(this, avatarBmp, getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
Recipient local = Recipient.from(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), false);
|
||||
new CreateSignalGroupTask(this, avatarBmp, getGroupName(), getAdapter().getRecipients(), Collections.singleton(local)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
} else {
|
||||
new CreateMmsGroupTask(this, getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
@ -254,7 +256,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
private void handleGroupUpdate() {
|
||||
new UpdateSignalGroupTask(this, groupToUpdate.get().id, avatarBmp,
|
||||
getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
getGroupName(), getAdapter().getRecipients(), groupToUpdate.get().admins).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void handleOpenConversation(long threadId, Recipient recipient) {
|
||||
@ -344,9 +346,10 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
for (Recipient recipient : members) {
|
||||
memberAddresses.add(recipient.getAddress());
|
||||
}
|
||||
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(activity)));
|
||||
Address local = Address.fromSerialized(TextSecurePreferences.getLocalNumber(activity));
|
||||
memberAddresses.add(local);
|
||||
|
||||
String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true);
|
||||
String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true, Collections.singletonList(local));
|
||||
Recipient groupRecipient = Recipient.from(activity, Address.fromSerialized(groupId), true);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
|
||||
|
||||
@ -370,16 +373,19 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
protected Bitmap avatar;
|
||||
protected Set<Recipient> members;
|
||||
protected String name;
|
||||
protected Set<Recipient> admins;
|
||||
|
||||
public SignalGroupTask(GroupCreateActivity activity,
|
||||
Bitmap avatar,
|
||||
String name,
|
||||
Set<Recipient> members)
|
||||
Set<Recipient> members,
|
||||
Set<Recipient> admins)
|
||||
{
|
||||
this.activity = activity;
|
||||
this.avatar = avatar;
|
||||
this.name = name;
|
||||
this.members = members;
|
||||
this.admins = admins;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -403,13 +409,13 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private static class CreateSignalGroupTask extends SignalGroupTask {
|
||||
public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set<Recipient> members) {
|
||||
super(activity, avatar, name, members);
|
||||
public CreateSignalGroupTask(GroupCreateActivity activity, Bitmap avatar, String name, Set<Recipient> members, Set<Recipient> admins) {
|
||||
super(activity, avatar, name, members, admins);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
|
||||
return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false));
|
||||
return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false, admins));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -430,16 +436,16 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
private String groupId;
|
||||
|
||||
public UpdateSignalGroupTask(GroupCreateActivity activity, String groupId,
|
||||
Bitmap avatar, String name, Set<Recipient> members)
|
||||
Bitmap avatar, String name, Set<Recipient> members, Set<Recipient> admins)
|
||||
{
|
||||
super(activity, avatar, name, members);
|
||||
super(activity, avatar, name, members, admins);
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
|
||||
try {
|
||||
return Optional.of(GroupManager.updateGroup(activity, groupId, members, avatar, name));
|
||||
return Optional.of(GroupManager.updateGroup(activity, groupId, members, avatar, name, admins));
|
||||
} catch (InvalidNumberException e) {
|
||||
return Optional.absent();
|
||||
}
|
||||
@ -491,7 +497,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
for (Recipient recipient : recipients) {
|
||||
boolean isPush = isActiveInDirectory(recipient);
|
||||
|
||||
if (failIfNotPush && !isPush) {
|
||||
if (failIfNotPush && !isPush && !recipient.getAddress().isPhone()) {
|
||||
results.add(new Result(null, false, activity.getString(R.string.GroupCreateActivity_cannot_add_non_push_to_existing_group,
|
||||
recipient.toShortString())));
|
||||
} else if (TextUtils.equals(TextSecurePreferences.getLocalNumber(activity), recipient.getAddress().serialize())) {
|
||||
@ -537,11 +543,17 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
existingContacts.addAll(recipients);
|
||||
|
||||
if (group.isPresent()) {
|
||||
List<Address> adminList = group.get().getAdmins();
|
||||
final Set<Recipient> admins = new HashSet<>(adminList.size());
|
||||
for (Address admin : adminList) {
|
||||
admins.add(Recipient.from(getContext(), admin, false));
|
||||
}
|
||||
return Optional.of(new GroupData(groupIds[0],
|
||||
existingContacts,
|
||||
BitmapUtil.fromByteArray(group.get().getAvatar()),
|
||||
group.get().getAvatar(),
|
||||
group.get().getTitle()));
|
||||
group.get().getTitle(),
|
||||
admins));
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
@ -582,13 +594,15 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
|
||||
Bitmap avatarBmp;
|
||||
byte[] avatarBytes;
|
||||
String name;
|
||||
Set<Recipient> admins;
|
||||
|
||||
public GroupData(String id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
|
||||
public GroupData(String id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name, Set<Recipient> admins) {
|
||||
this.id = id;
|
||||
this.recipients = recipients;
|
||||
this.avatarBmp = avatarBmp;
|
||||
this.avatarBytes = avatarBytes;
|
||||
this.name = name;
|
||||
this.admins = admins;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -740,9 +740,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
boolean isLokiPublicChat = isGroupConversation(); // TODO: Figure out a better way of determining this
|
||||
boolean isLokiGroupChat = recipient.getAddress().isPublicChat() || recipient.getAddress().isRSSFeed();
|
||||
|
||||
if (isSecureText && !isLokiPublicChat) { // TODO:
|
||||
if (isSecureText && !isLokiGroupChat) {
|
||||
if (recipient.getExpireMessages() > 0) {
|
||||
inflater.inflate(R.menu.conversation_expiring_on, menu);
|
||||
|
||||
@ -762,7 +762,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
|
||||
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
|
||||
*/
|
||||
} else if (isGroupConversation() && !isLokiPublicChat) {
|
||||
} else if (isGroupConversation() && !isLokiGroupChat) {
|
||||
inflater.inflate(R.menu.conversation_group_options, menu);
|
||||
|
||||
if (!isPushGroupConversation()) {
|
||||
@ -1152,10 +1152,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (threadId != -1 && leaveMessage.isPresent()) {
|
||||
MessageSender.send(this, leaveMessage.get(), threadId, false, null);
|
||||
|
||||
// We need to remove the master device from the group
|
||||
String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this);
|
||||
String localNumber = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(this);
|
||||
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(this);
|
||||
String groupId = groupRecipient.getAddress().toGroupString();
|
||||
groupDatabase.setActive(groupId, false);
|
||||
groupDatabase.remove(groupId, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)));
|
||||
groupDatabase.remove(groupId, Address.fromSerialized(localNumber));
|
||||
|
||||
initializeEnabledCheck();
|
||||
} else {
|
||||
@ -2077,7 +2081,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) {
|
||||
if (recipient.isGroupRecipient() && (recipient.getName().equals("Loki News") || recipient.getName().equals("Session Updates"))) {
|
||||
if (recipient.isGroupRecipient() && recipient.getAddress().isRSSFeed()) {
|
||||
unblockButton.setVisibility(View.GONE);
|
||||
composePanel.setVisibility(View.GONE);
|
||||
makeDefaultSmsButton.setVisibility(View.GONE);
|
||||
@ -2106,7 +2110,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private void setGroupShareProfileReminder(@NonNull Recipient recipient) {
|
||||
if (recipient.isPushGroupRecipient() && !recipient.isProfileSharing()) {
|
||||
if (recipient.isPushGroupRecipient() && !recipient.isProfileSharing() && !recipient.getAddress().isPublicChat() && !recipient.getAddress().isRSSFeed()) {
|
||||
groupShareProfileView.get().setRecipient(recipient);
|
||||
groupShareProfileView.get().setVisibility(View.VISIBLE);
|
||||
} else if (groupShareProfileView.resolved()) {
|
||||
@ -2461,7 +2465,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
// Loki - Send a friend request if we're not yet friends with the user in question
|
||||
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
|
||||
outgoingMessage.isFriendRequest = (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS); // Needed for stageOutgoingMessage(...)
|
||||
outgoingMessage.isFriendRequest = !isGroupConversation() && friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS; // Needed for stageOutgoingMessage(...)
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.SEND_SMS, Manifest.permission.READ_SMS)
|
||||
@ -2521,7 +2525,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
// Loki - Send a friend request if we're not yet friends with the user in question
|
||||
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
|
||||
message.isFriendRequest = (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS); // Needed for stageOutgoingMessage(...)
|
||||
message.isFriendRequest = !isGroupConversation() && friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS; // Needed for stageOutgoingMessage(...)
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.SEND_SMS)
|
||||
|
@ -52,17 +52,9 @@ public class Address implements Parcelable, Comparable<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) {
|
||||
this(address, false);
|
||||
}
|
||||
|
||||
private Address(@NonNull String address, Boolean isPublicChat) {
|
||||
if (address == null) throw new AssertionError(address);
|
||||
this.address = address.toLowerCase();
|
||||
this.isPublicChat = isPublicChat;
|
||||
}
|
||||
|
||||
public Address(Parcel in) {
|
||||
@ -77,10 +69,6 @@ public class Address implements Parcelable, Comparable<Address> {
|
||||
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) {
|
||||
String[] escapedAddresses = DelimiterUtil.split(serialized, delimiter);
|
||||
List<Address> addresses = new LinkedList<>();
|
||||
@ -121,13 +109,15 @@ public class Address implements Parcelable, Comparable<Address> {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isGroup() {
|
||||
return GroupUtil.isEncodedGroup(address);
|
||||
}
|
||||
public boolean isGroup() { return GroupUtil.isEncodedGroup(address); }
|
||||
|
||||
public boolean isMmsGroup() {
|
||||
return GroupUtil.isMmsGroup(address);
|
||||
}
|
||||
public boolean isSignalGroup() { return !isPublicChat() && !isRSSFeed(); }
|
||||
|
||||
public boolean isPublicChat() { return GroupUtil.isPublicChat(address); }
|
||||
|
||||
public boolean isRSSFeed() { return GroupUtil.isRssFeed(address); }
|
||||
|
||||
public boolean isMmsGroup() { return GroupUtil.isMmsGroup(address); }
|
||||
|
||||
public boolean isEmail() {
|
||||
return NumberUtil.isValidEmail(address);
|
||||
@ -143,7 +133,7 @@ public class Address implements Parcelable, Comparable<Address> {
|
||||
}
|
||||
|
||||
public @NonNull String toPhoneString() {
|
||||
if (!isPhone() && !isPublicChat) {
|
||||
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");
|
||||
|
@ -24,7 +24,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
@ -52,6 +51,7 @@ public class GroupDatabase extends Database {
|
||||
|
||||
// Loki
|
||||
private static final String AVATAR_URL = "avatar_url";
|
||||
private static final String ADMINS = "admins";
|
||||
|
||||
public static final String CREATE_TABLE =
|
||||
"CREATE TABLE " + TABLE_NAME +
|
||||
@ -68,6 +68,7 @@ public class GroupDatabase extends Database {
|
||||
ACTIVE + " INTEGER DEFAULT 1, " +
|
||||
AVATAR_DIGEST + " BLOB, " +
|
||||
AVATAR_URL + " TEXT, " +
|
||||
ADMINS + " TEXT, " +
|
||||
MMS + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
@ -76,7 +77,7 @@ public class GroupDatabase extends Database {
|
||||
|
||||
private static final String[] GROUP_PROJECTION = {
|
||||
GROUP_ID, TITLE, MEMBERS, AVATAR, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
|
||||
TIMESTAMP, ACTIVE, MMS, AVATAR_URL
|
||||
TIMESTAMP, ACTIVE, MMS, AVATAR_URL, ADMINS
|
||||
};
|
||||
|
||||
static final List<String> TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList();
|
||||
@ -116,8 +117,9 @@ public class GroupDatabase extends Database {
|
||||
return new Reader(cursor);
|
||||
}
|
||||
|
||||
public String getOrCreateGroupForMembers(List<Address> members, boolean mms) {
|
||||
public String getOrCreateGroupForMembers(List<Address> members, boolean mms, List<Address> admins) {
|
||||
Collections.sort(members);
|
||||
Collections.sort(admins);
|
||||
|
||||
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {GROUP_ID},
|
||||
MEMBERS + " = ? AND " + MMS + " = ?",
|
||||
@ -128,7 +130,7 @@ public class GroupDatabase extends Database {
|
||||
return cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID));
|
||||
} else {
|
||||
String groupId = GroupUtil.getEncodedId(allocateGroupId(), mms);
|
||||
create(groupId, null, members, null, null);
|
||||
create(groupId, null, members, null, null, admins);
|
||||
return groupId;
|
||||
}
|
||||
} finally {
|
||||
@ -150,14 +152,33 @@ public class GroupDatabase extends Database {
|
||||
if (!includeSelf && Util.isOwnNumber(context, member))
|
||||
continue;
|
||||
|
||||
recipients.add(Recipient.from(context, member, false));
|
||||
if (member.isPhone()) {
|
||||
recipients.add(Recipient.from(context, member, false));
|
||||
}
|
||||
}
|
||||
|
||||
return recipients;
|
||||
}
|
||||
|
||||
public boolean signalGroupsHaveMember(String hexEncodedPublicKey) {
|
||||
try {
|
||||
Address address = Address.fromSerialized(hexEncodedPublicKey);
|
||||
Reader reader = DatabaseFactory.getGroupDatabase(context).getGroups();
|
||||
GroupRecord record;
|
||||
while ((record = reader.getNext()) != null) {
|
||||
if (record.isSignalGroup() && record.members.contains(address)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void create(@NonNull String groupId, @Nullable String title, @NonNull List<Address> members,
|
||||
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay)
|
||||
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List<Address> admins)
|
||||
{
|
||||
Collections.sort(members);
|
||||
|
||||
@ -179,6 +200,10 @@ public class GroupDatabase extends Database {
|
||||
contentValues.put(ACTIVE, 1);
|
||||
contentValues.put(MMS, GroupUtil.isMmsGroup(groupId));
|
||||
|
||||
if (admins != null) {
|
||||
contentValues.put(ADMINS, Address.toSerializedList(admins, ','));
|
||||
}
|
||||
|
||||
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
Recipient.applyCached(Address.fromSerialized(groupId), recipient -> {
|
||||
@ -260,6 +285,17 @@ public class GroupDatabase extends Database {
|
||||
});
|
||||
}
|
||||
|
||||
public void updateAdmins(String groupId, List<Address> admins) {
|
||||
Collections.sort(admins);
|
||||
|
||||
ContentValues contents = new ContentValues();
|
||||
contents.put(ADMINS, Address.toSerializedList(admins, ','));
|
||||
contents.put(ACTIVE, 1);
|
||||
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
|
||||
new String[] {groupId});
|
||||
}
|
||||
|
||||
public void remove(String groupId, Address source) {
|
||||
List<Address> currentMembers = getCurrentMembers(groupId);
|
||||
currentMembers.remove(source);
|
||||
@ -351,7 +387,8 @@ public class GroupDatabase extends Database {
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1,
|
||||
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)),
|
||||
cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1,
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_URL)));
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_URL)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(ADMINS)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -375,10 +412,11 @@ public class GroupDatabase extends Database {
|
||||
private final boolean active;
|
||||
private final boolean mms;
|
||||
private final String url;
|
||||
private final List<Address> admins;
|
||||
|
||||
public GroupRecord(String id, String title, String members, byte[] avatar,
|
||||
long avatarId, byte[] avatarKey, String avatarContentType,
|
||||
String relay, boolean active, byte[] avatarDigest, boolean mms, String url)
|
||||
String relay, boolean active, byte[] avatarDigest, boolean mms, String url, String admins)
|
||||
{
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
@ -394,6 +432,9 @@ public class GroupDatabase extends Database {
|
||||
|
||||
if (!TextUtils.isEmpty(members)) this.members = Address.fromSerializedList(members, ',');
|
||||
else this.members = new LinkedList<>();
|
||||
|
||||
if (!TextUtils.isEmpty(admins)) this.admins = Address.fromSerializedList(admins, ',');
|
||||
else this.admins = new LinkedList<>();
|
||||
}
|
||||
|
||||
public byte[] getId() {
|
||||
@ -448,6 +489,14 @@ public class GroupDatabase extends Database {
|
||||
return mms;
|
||||
}
|
||||
|
||||
public boolean isPublicChat() { return Address.fromSerialized(id).isPublicChat(); }
|
||||
|
||||
public boolean isRSSFeed() { return Address.fromSerialized(id).isRSSFeed(); }
|
||||
|
||||
public boolean isSignalGroup() { return Address.fromSerialized(id).isSignalGroup(); }
|
||||
|
||||
public String getUrl() { return url; }
|
||||
|
||||
public List<Address> getAdmins() { return admins; }
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +220,7 @@ public class SmsMigrator {
|
||||
memberAddresses.add(recipient.getAddress());
|
||||
}
|
||||
|
||||
String ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(memberAddresses, true);
|
||||
String ourGroupId = DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(memberAddresses, true, null);
|
||||
Recipient ourGroupRecipient = Recipient.from(context, Address.fromSerialized(ourGroupId), true);
|
||||
long ourThreadId = threadDatabase.getThreadIdFor(ourGroupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
|
||||
|
@ -35,14 +35,17 @@ import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.*;
|
||||
import org.thoughtcrime.securesms.loki.LokiMessageDatabase;
|
||||
import org.thoughtcrime.securesms.loki.LokiThreadDatabase;
|
||||
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiAPIDatabase;
|
||||
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyBundleDatabase;
|
||||
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPreKeyRecordDatabase;
|
||||
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiUserDatabase;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@ -76,8 +79,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int lokiV3 = 24;
|
||||
private static final int lokiV4 = 25;
|
||||
private static final int lokiV5 = 26;
|
||||
private static final int lokiV6 = 27;
|
||||
|
||||
private static final int DATABASE_VERSION = lokiV5; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||
private static final int DATABASE_VERSION = lokiV6; // 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;
|
||||
@ -529,6 +533,43 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand());
|
||||
}
|
||||
|
||||
if (oldVersion < lokiV6) {
|
||||
// Migrate public chats from __textsecure_group__ to __loki_public_chat_group__
|
||||
try (Cursor lokiPublicChatCursor = db.rawQuery("SELECT public_chat FROM loki_public_chat_database", null)) {
|
||||
while (lokiPublicChatCursor != null && lokiPublicChatCursor.moveToNext()) {
|
||||
String chatString = lokiPublicChatCursor.getString(0);
|
||||
LokiPublicChat publicChat = LokiPublicChat.fromJSON(chatString);
|
||||
if (publicChat != null) {
|
||||
byte[] groupId = publicChat.getId().getBytes();
|
||||
String oldId = GroupUtil.getEncodedId(groupId, false);
|
||||
String newId = GroupUtil.getEncodedPublicChatId(groupId);
|
||||
ContentValues threadUpdate = new ContentValues();
|
||||
threadUpdate.put("recipient_ids", newId);
|
||||
db.update("thread", threadUpdate, "recipient_ids = ?", new String[]{ oldId });
|
||||
ContentValues groupUpdate = new ContentValues();
|
||||
groupUpdate.put("group_id", newId);
|
||||
db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate rss feeds from __textsecure_group__ to __loki_rss_feed_group__
|
||||
String[] rssFeedIds = new String[] { "loki.network.feed", "loki.network.messenger-updates.feed" };
|
||||
for (String groupId : rssFeedIds) {
|
||||
String oldId = GroupUtil.getEncodedId(groupId.getBytes(), false);
|
||||
String newId = GroupUtil.getEncodedRSSFeedId(groupId.getBytes());
|
||||
ContentValues threadUpdate = new ContentValues();
|
||||
threadUpdate.put("recipient_ids", newId);
|
||||
db.update("thread", threadUpdate, "recipient_ids = ?", new String[]{ oldId });
|
||||
ContentValues groupUpdate = new ContentValues();
|
||||
groupUpdate.put("group_id", newId);
|
||||
db.update("groups", groupUpdate,"group_id = ?", new String[] { oldId });
|
||||
}
|
||||
|
||||
// Add admin field in groups
|
||||
db.execSQL("ALTER TABLE groups ADD COLUMN admins TEXT");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
@ -36,8 +36,13 @@ import java.util.Set;
|
||||
|
||||
public class GroupManager {
|
||||
|
||||
public static long getThreadId(String id, @NonNull Context context) {
|
||||
final String groupId = GroupUtil.getEncodedId(id.getBytes(), false);
|
||||
public static long getPublicChatThreadId(String id, @NonNull Context context) {
|
||||
final String groupId = GroupUtil.getEncodedPublicChatId(id.getBytes());
|
||||
return getThreadIdFromGroupId(groupId, context);
|
||||
}
|
||||
|
||||
public static long getRSSFeedThreadId(String id, @NonNull Context context) {
|
||||
final String groupId = GroupUtil.getEncodedRSSFeedId(id.getBytes());
|
||||
return getThreadIdFromGroupId(groupId, context);
|
||||
}
|
||||
|
||||
@ -50,11 +55,12 @@ public class GroupManager {
|
||||
@NonNull Set<Recipient> members,
|
||||
@Nullable Bitmap avatar,
|
||||
@Nullable String name,
|
||||
boolean mms)
|
||||
boolean mms,
|
||||
@NonNull Set<Recipient> admins)
|
||||
{
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(database.allocateGroupId(), mms);
|
||||
return createGroup(id, context, members, avatar, name, mms);
|
||||
return createGroup(id, context, members, avatar, name, mms, admins);
|
||||
}
|
||||
|
||||
public static @NonNull GroupActionResult createGroup(@NonNull String id,
|
||||
@ -62,56 +68,90 @@ public class GroupManager {
|
||||
@NonNull Set<Recipient> members,
|
||||
@Nullable Bitmap avatar,
|
||||
@Nullable String name,
|
||||
boolean mms)
|
||||
boolean mms,
|
||||
@NonNull Set<Recipient> admins)
|
||||
{
|
||||
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
|
||||
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
final String groupId = GroupUtil.getEncodedId(id.getBytes(), mms);
|
||||
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false);
|
||||
final Set<Address> memberAddresses = getMemberAddresses(members);
|
||||
final Set<Address> adminAddresses = getMemberAddresses(admins);
|
||||
|
||||
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
|
||||
groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null);
|
||||
String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
String ourNumber = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(context);
|
||||
|
||||
memberAddresses.add(Address.fromSerialized(ourNumber));
|
||||
groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>(adminAddresses));
|
||||
|
||||
if (!mms) {
|
||||
groupDatabase.updateAvatar(groupId, avatarBytes);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true);
|
||||
}
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
|
||||
/* Loki: Original Code
|
||||
==================
|
||||
if (!mms) {
|
||||
groupDatabase.updateAvatar(groupId, avatarBytes);
|
||||
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(groupRecipient, true);
|
||||
return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes);
|
||||
return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses);
|
||||
} else {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
public static @NonNull GroupActionResult createPublicChatGroup(@NonNull String id,
|
||||
@NonNull Context context,
|
||||
@Nullable Bitmap avatar,
|
||||
@Nullable String name)
|
||||
{
|
||||
final String groupId = GroupUtil.getEncodedPublicChatId(id.getBytes());
|
||||
return createLokiGroup(groupId, context, avatar, name);
|
||||
}
|
||||
|
||||
public static @NonNull GroupActionResult createRSSFeedGroup(@NonNull String id,
|
||||
@NonNull Context context,
|
||||
@Nullable Bitmap avatar,
|
||||
@Nullable String name)
|
||||
{
|
||||
final String groupId = GroupUtil.getEncodedRSSFeedId(id.getBytes());
|
||||
return createLokiGroup(groupId, context, avatar, name);
|
||||
}
|
||||
|
||||
private static @NonNull GroupActionResult createLokiGroup(@NonNull String groupId,
|
||||
@NonNull Context context,
|
||||
@Nullable Bitmap avatar,
|
||||
@Nullable String name)
|
||||
{
|
||||
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
|
||||
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
final Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), false);
|
||||
final Set<Address> memberAddresses = new HashSet<>();
|
||||
|
||||
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
|
||||
groupDatabase.create(groupId, name, new LinkedList<>(memberAddresses), null, null, new LinkedList<>());
|
||||
|
||||
groupDatabase.updateAvatar(groupId, avatarBytes);
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.CONVERSATION);
|
||||
return new GroupActionResult(groupRecipient, threadId);
|
||||
}
|
||||
|
||||
public static GroupActionResult updateGroup(@NonNull Context context,
|
||||
@NonNull String groupId,
|
||||
@NonNull Set<Recipient> members,
|
||||
@Nullable Bitmap avatar,
|
||||
@Nullable String name)
|
||||
@Nullable String name,
|
||||
@NonNull Set<Recipient> admins)
|
||||
throws InvalidNumberException
|
||||
{
|
||||
final GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
final Set<Address> memberAddresses = getMemberAddresses(members);
|
||||
final Set<Address> adminAddresses = getMemberAddresses(admins);
|
||||
final byte[] avatarBytes = BitmapUtil.toByteArray(avatar);
|
||||
|
||||
memberAddresses.add(Address.fromSerialized(TextSecurePreferences.getLocalNumber(context)));
|
||||
groupDatabase.updateMembers(groupId, new LinkedList<>(memberAddresses));
|
||||
groupDatabase.updateAdmins(groupId, new LinkedList<>(adminAddresses));
|
||||
groupDatabase.updateTitle(groupId, name);
|
||||
groupDatabase.updateAvatar(groupId, avatarBytes);
|
||||
|
||||
if (!GroupUtil.isMmsGroup(groupId)) {
|
||||
return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes);
|
||||
return sendGroupUpdate(context, groupId, memberAddresses, name, avatarBytes, adminAddresses);
|
||||
} else {
|
||||
Recipient groupRecipient = Recipient.from(context, Address.fromSerialized(groupId), true);
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||
@ -123,7 +163,8 @@ public class GroupManager {
|
||||
@NonNull String groupId,
|
||||
@NonNull Set<Address> members,
|
||||
@Nullable String groupName,
|
||||
@Nullable byte[] avatar)
|
||||
@Nullable byte[] avatar,
|
||||
@NonNull Set<Address> admins)
|
||||
{
|
||||
try {
|
||||
Attachment avatarAttachment = null;
|
||||
@ -131,15 +172,20 @@ public class GroupManager {
|
||||
Recipient groupRecipient = Recipient.from(context, groupAddress, false);
|
||||
|
||||
List<String> numbers = new LinkedList<>();
|
||||
|
||||
for (Address member : members) {
|
||||
numbers.add(member.serialize());
|
||||
}
|
||||
|
||||
List<String> adminNumbers = new LinkedList<>();
|
||||
for (Address admin : admins) {
|
||||
adminNumbers.add(admin.serialize());
|
||||
}
|
||||
|
||||
GroupContext.Builder groupContextBuilder = GroupContext.newBuilder()
|
||||
.setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupId)))
|
||||
.setType(GroupContext.Type.UPDATE)
|
||||
.addAllMembers(numbers);
|
||||
.addAllMembers(numbers)
|
||||
.addAllAdmins(adminNumbers);
|
||||
if (groupName != null) groupContextBuilder.setName(groupName);
|
||||
GroupContext groupContext = groupContextBuilder.build();
|
||||
|
||||
|
@ -8,6 +8,7 @@ import android.support.annotation.Nullable;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
@ -23,14 +24,20 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
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.messages.SignalServiceGroup.Type;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@ -38,6 +45,8 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import kotlin.Unit;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer;
|
||||
import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||
@ -58,7 +67,7 @@ public class GroupMessageProcessor {
|
||||
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
SignalServiceGroup group = message.getGroupInfo().get();
|
||||
String id = GroupUtil.getEncodedId(group.getGroupId(), false);
|
||||
String id = GroupUtil.getEncodedId(group);
|
||||
Optional<GroupRecord> record = database.getGroup(id);
|
||||
|
||||
if (record.isPresent() && group.getType() == Type.UPDATE) {
|
||||
@ -81,12 +90,13 @@ public class GroupMessageProcessor {
|
||||
boolean outgoing)
|
||||
{
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(group.getGroupId(), false);
|
||||
String id = GroupUtil.getEncodedId(group);
|
||||
GroupContext.Builder builder = createGroupContext(group);
|
||||
builder.setType(GroupContext.Type.UPDATE);
|
||||
|
||||
SignalServiceAttachment avatar = group.getAvatar().orNull();
|
||||
List<Address> members = group.getMembers().isPresent() ? new LinkedList<Address>() : null;
|
||||
List<Address> admins = group.getAdmins().isPresent() ? new LinkedList<>() : null;
|
||||
|
||||
if (group.getMembers().isPresent()) {
|
||||
for (String member : group.getMembers().get()) {
|
||||
@ -94,8 +104,25 @@ public class GroupMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
// We should only create the group if we are part of the member list
|
||||
String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, TextSecurePreferences.getLocalNumber(context));
|
||||
if (members == null || !members.contains(Address.fromSerialized(hexEncodedPublicKey))) {
|
||||
Log.d("Loki - Group Message", "Received a group create message which doesn't include us in the member list. Ignoring.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (group.getAdmins().isPresent()) {
|
||||
for (String admin : group.getAdmins().get()) {
|
||||
admins.add(Address.fromExternal(context, admin));
|
||||
}
|
||||
}
|
||||
|
||||
database.create(id, group.getName().orNull(), members,
|
||||
avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null);
|
||||
avatar != null && avatar.isPointer() ? avatar.asPointer() : null, null, admins);
|
||||
|
||||
if (group.getMembers().isPresent()) {
|
||||
establishSessionsWithMembersIfNeeded(context, group.getMembers().get());
|
||||
}
|
||||
|
||||
return storeMessage(context, content, group, builder.build(), outgoing);
|
||||
}
|
||||
@ -108,7 +135,16 @@ public class GroupMessageProcessor {
|
||||
{
|
||||
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(group.getGroupId(), false);
|
||||
String id = GroupUtil.getEncodedId(group);
|
||||
|
||||
// Only update group if admin sent the message
|
||||
if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
|
||||
String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, content.getSender());
|
||||
if (!groupRecord.getAdmins().contains(Address.fromSerialized(hexEncodedPublicKey))) {
|
||||
Log.d("Loki - Group Message", "Received a group update message from a non-admin user for " + id +". Ignoring.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Set<Address> recordMembers = new HashSet<>(groupRecord.getMembers());
|
||||
Set<Address> messageMembers = new HashSet<>();
|
||||
@ -141,7 +177,9 @@ public class GroupMessageProcessor {
|
||||
}
|
||||
|
||||
if (missingMembers.size() > 0) {
|
||||
// TODO We should tell added and missing about each-other.
|
||||
for (Address removedMember : missingMembers) {
|
||||
builder.addMembers(removedMember.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
if (group.getName().isPresent() || group.getAvatar().isPresent()) {
|
||||
@ -155,6 +193,10 @@ public class GroupMessageProcessor {
|
||||
|
||||
if (!groupRecord.isActive()) database.setActive(id, true);
|
||||
|
||||
if (group.getMembers().isPresent()) {
|
||||
establishSessionsWithMembersIfNeeded(context, group.getMembers().get());
|
||||
}
|
||||
|
||||
return storeMessage(context, content, group, builder.build(), outgoing);
|
||||
}
|
||||
|
||||
@ -163,7 +205,10 @@ public class GroupMessageProcessor {
|
||||
@NonNull SignalServiceGroup group,
|
||||
@NonNull GroupRecord record)
|
||||
{
|
||||
if (record.getMembers().contains(Address.fromExternal(context, content.getSender()))) {
|
||||
String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, content.getSender());
|
||||
String ourPublicKey = getMasterHexEncodedPublicKey(context, TextSecurePreferences.getLocalNumber(context));
|
||||
// If the requester is a group member and we are admin then we should send them the group update
|
||||
if (record.getMembers().contains(Address.fromSerialized(hexEncodedPublicKey)) && record.getAdmins().contains(Address.fromSerialized(ourPublicKey))) {
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new PushGroupUpdateJob(content.getSender(), group.getGroupId()));
|
||||
@ -179,14 +224,15 @@ public class GroupMessageProcessor {
|
||||
boolean outgoing)
|
||||
{
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
String id = GroupUtil.getEncodedId(group.getGroupId(), false);
|
||||
String id = GroupUtil.getEncodedId(group);
|
||||
List<Address> members = record.getMembers();
|
||||
|
||||
GroupContext.Builder builder = createGroupContext(group);
|
||||
builder.setType(GroupContext.Type.QUIT);
|
||||
|
||||
if (members.contains(Address.fromExternal(context, content.getSender()))) {
|
||||
database.remove(id, Address.fromExternal(context, content.getSender()));
|
||||
String hexEncodedPublicKey = getMasterHexEncodedPublicKey(context, content.getSender());
|
||||
if (members.contains(Address.fromExternal(context, hexEncodedPublicKey))) {
|
||||
database.remove(id, Address.fromExternal(context, hexEncodedPublicKey));
|
||||
if (outgoing) database.setActive(id, false);
|
||||
|
||||
return storeMessage(context, content, group, builder.build(), outgoing);
|
||||
@ -204,14 +250,14 @@ public class GroupMessageProcessor {
|
||||
{
|
||||
if (group.getAvatar().isPresent()) {
|
||||
ApplicationContext.getInstance(context).getJobManager()
|
||||
.add(new AvatarDownloadJob(group.getGroupId()));
|
||||
.add(new AvatarDownloadJob(GroupUtil.getEncodedId(group)));
|
||||
}
|
||||
|
||||
try {
|
||||
if (outgoing) {
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
Address addres = Address.fromExternal(context, GroupUtil.getEncodedId(group.getGroupId(), false));
|
||||
Recipient recipient = Recipient.from(context, addres, false);
|
||||
Address address = Address.fromExternal(context, GroupUtil.getEncodedId(group));
|
||||
Recipient recipient = Recipient.from(context, address, false);
|
||||
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
@ -260,7 +306,38 @@ public class GroupMessageProcessor {
|
||||
builder.addAllMembers(group.getMembers().get());
|
||||
}
|
||||
|
||||
if (group.getAdmins().isPresent()) {
|
||||
builder.addAllAdmins(group.getAdmins().get());
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static String getMasterHexEncodedPublicKey(Context context, String hexEncodedPublicKey) {
|
||||
String ourPublicKey = TextSecurePreferences.getLocalNumber(context);
|
||||
try {
|
||||
String masterHexEncodedPublicKey = hexEncodedPublicKey.equalsIgnoreCase(ourPublicKey)
|
||||
? TextSecurePreferences.getMasterHexEncodedPublicKey(context)
|
||||
: PromiseUtil.timeout(LokiStorageAPI.shared.getPrimaryDevicePublicKey(hexEncodedPublicKey), 5000).get();
|
||||
return masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : hexEncodedPublicKey;
|
||||
} catch (Exception e) {
|
||||
return hexEncodedPublicKey;
|
||||
}
|
||||
}
|
||||
|
||||
private static void establishSessionsWithMembersIfNeeded(Context context, List<String> members) {
|
||||
String ourNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
for (String member : members) {
|
||||
// Make sure we have session with all of the members secondary devices
|
||||
LokiStorageAPI.shared.getAllDevicePublicKeys(member).success(devices -> {
|
||||
if (devices.contains(ourNumber)) { return Unit.INSTANCE; }
|
||||
for (String device : devices) {
|
||||
SignalProtocolAddress protocolAddress = new SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
boolean haveSession = new TextSecureSessionStore(context).containsSession(protocolAddress);
|
||||
if (!haveSession) { MessageSender.sendBackgroundSessionRequest(context, device); }
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,9 +40,9 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
|
||||
|
||||
@Inject SignalServiceMessageReceiver receiver;
|
||||
|
||||
private byte[] groupId;
|
||||
private String groupId;
|
||||
|
||||
public AvatarDownloadJob(@NonNull byte[] groupId) {
|
||||
public AvatarDownloadJob(@NonNull String groupId) {
|
||||
this(new Job.Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(10)
|
||||
@ -50,14 +50,14 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
|
||||
groupId);
|
||||
}
|
||||
|
||||
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull byte[] groupId) {
|
||||
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull String groupId) {
|
||||
super(parameters);
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_GROUP_ID, GroupUtil.getEncodedId(groupId, false)).build();
|
||||
return new Data.Builder().putString(KEY_GROUP_ID, groupId).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -67,9 +67,8 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
|
||||
|
||||
@Override
|
||||
public void onRun() throws IOException {
|
||||
String encodeId = GroupUtil.getEncodedId(groupId, false);
|
||||
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<GroupRecord> record = database.getGroup(encodeId);
|
||||
Optional<GroupRecord> record = database.getGroup(groupId);
|
||||
File attachment = null;
|
||||
|
||||
try {
|
||||
@ -97,7 +96,7 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
|
||||
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, MAX_AVATAR_SIZE);
|
||||
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500);
|
||||
|
||||
database.updateAvatar(encodeId, avatar);
|
||||
database.updateAvatar(groupId, avatar);
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) {
|
||||
@ -120,11 +119,7 @@ public class AvatarDownloadJob extends BaseJob implements InjectableType {
|
||||
public static final class Factory implements Job.Factory<AvatarDownloadJob> {
|
||||
@Override
|
||||
public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
try {
|
||||
return new AvatarDownloadJob(parameters, GroupUtil.getDecodedId(data.getString(KEY_GROUP_ID)));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return new AvatarDownloadJob(parameters, data.getString(KEY_GROUP_ID));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DirectoryHelper;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -67,11 +65,11 @@ public class DirectoryRefreshJob extends BaseJob {
|
||||
public void onRun() throws IOException {
|
||||
Log.i(TAG, "DirectoryRefreshJob.onRun()");
|
||||
|
||||
if (recipient == null) {
|
||||
DirectoryHelper.refreshDirectory(context, notifyOfNewUsers);
|
||||
} else {
|
||||
DirectoryHelper.refreshDirectoryFor(context, recipient);
|
||||
}
|
||||
// if (recipient == null) {
|
||||
// DirectoryHelper.refreshDirectory(context, notifyOfNewUsers);
|
||||
// } else {
|
||||
// DirectoryHelper.refreshDirectoryFor(context, recipient);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -244,7 +244,7 @@ public class MmsDownloadJob extends BaseJob {
|
||||
}
|
||||
|
||||
if (members.size() > 2) {
|
||||
group = Optional.of(Address.fromSerialized(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(new LinkedList<>(members), true)));
|
||||
group = Optional.of(Address.fromSerialized(DatabaseFactory.getGroupDatabase(context).getOrCreateGroupForMembers(new LinkedList<>(members), true, new LinkedList<>())));
|
||||
}
|
||||
|
||||
IncomingMediaMessage message = new IncomingMediaMessage(from, group, body, retrieved.getDate() * 1000L, attachments, subscriptionId, 0, false, false);
|
||||
|
@ -85,7 +85,7 @@ public class MultiDeviceGroupUpdateJob extends BaseJob implements InjectableType
|
||||
reader = DatabaseFactory.getGroupDatabase(context).getGroups();
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
if (!record.isMms()) {
|
||||
if (!record.isMms() && !record.isPublicChat() && !record.isRSSFeed()) {
|
||||
List<String> members = new LinkedList<>();
|
||||
|
||||
for (Address member : record.getMembers()) {
|
||||
|
@ -291,6 +291,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
// Loki - Handle friend request acceptance if needed
|
||||
acceptFriendRequestIfNeeded(content);
|
||||
|
||||
// Loki - Session requests
|
||||
handleSessionRequestIfNeeded(content);
|
||||
|
||||
// Loki - Store pre key bundle
|
||||
// We shouldn't store it if it's a pairing message
|
||||
if (!content.getPairingAuthorisation().isPresent()) {
|
||||
@ -335,7 +338,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
} else {
|
||||
// Loki - Don't process session restore message any further
|
||||
if (message.isSessionRestore()) { return; }
|
||||
if (message.isSessionRestore() || message.isSessionRequest()) { return; }
|
||||
|
||||
if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
|
||||
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId);
|
||||
else if (message.isExpirationUpdate())
|
||||
@ -345,7 +349,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
else if (message.getBody().isPresent())
|
||||
handleTextMessage(content, message, smsMessageId, Optional.absent());
|
||||
|
||||
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))) {
|
||||
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get()))) {
|
||||
handleUnknownGroupMessage(content, message.getGroupInfo().get());
|
||||
}
|
||||
|
||||
@ -601,9 +605,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
private void handleUnknownGroupMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceGroup group)
|
||||
{
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new RequestGroupInfoJob(content.getSender(), group.getGroupId()));
|
||||
if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
|
||||
ApplicationContext.getInstance(context)
|
||||
.getJobManager()
|
||||
.add(new RequestGroupInfoJob(content.getSender(), group.getGroupId()));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleExpirationUpdate(@NonNull SignalServiceContent content,
|
||||
@ -690,7 +696,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
Log.d("Loki", "Sent friend request to " + pubKey);
|
||||
} else if (status == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) {
|
||||
// Accept the incoming friend request
|
||||
becomeFriendsWithContact(pubKey, false);
|
||||
becomeFriendsWithContact(pubKey, false, false);
|
||||
// Send them an accept message back
|
||||
MessageSender.sendBackgroundMessage(context, pubKey);
|
||||
Log.d("Loki", "Became friends with " + deviceContact.getNumber());
|
||||
@ -728,7 +734,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
threadId = handleSynchronizeSentTextMessage(message);
|
||||
}
|
||||
|
||||
if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false))) {
|
||||
if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get()))) {
|
||||
handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get());
|
||||
}
|
||||
|
||||
@ -738,7 +744,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
Recipient recipient = null;
|
||||
|
||||
if (message.getDestination().isPresent()) recipient = Recipient.from(context, Address.fromSerialized(message.getDestination().get()), false);
|
||||
else if (message.getMessage().getGroupInfo().isPresent()) recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false)), false);
|
||||
else if (message.getMessage().getGroupInfo().isPresent()) recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false);
|
||||
|
||||
|
||||
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
|
||||
@ -845,8 +851,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
|
||||
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
|
||||
|
||||
// If message is from group then we need to map it to the correct sender
|
||||
Address sender = message.isGroupUpdate() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress();
|
||||
Address sender = primaryDeviceRecipient.getAddress();
|
||||
|
||||
// If message is from group then we need to map it to get the sender of the message
|
||||
if (message.isGroupMessage()) {
|
||||
sender = getPrimaryDeviceRecipient(content.getSender()).getAddress();
|
||||
}
|
||||
|
||||
// Ignore messages from ourselves
|
||||
if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; }
|
||||
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender, message.getTimestamp(), -1,
|
||||
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
|
||||
quote, sharedContacts, linkPreviews, sticker);
|
||||
@ -1030,8 +1044,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
} else {
|
||||
notifyTypingStoppedFromIncomingMessage(primaryDeviceRecipient, content.getSender(), content.getSenderDevice());
|
||||
|
||||
// If message is from group then we need to map it to the correct sender
|
||||
Address sender = message.isGroupUpdate() ? Address.fromSerialized(content.getSender()) : primaryDeviceRecipient.getAddress();
|
||||
Address sender = primaryDeviceRecipient.getAddress();
|
||||
|
||||
// If message is from group then we need to map it to get the sender of the message
|
||||
if (message.isGroupMessage()) {
|
||||
sender = getPrimaryDeviceRecipient(content.getSender()).getAddress();
|
||||
}
|
||||
|
||||
// Ignore messages from ourselves
|
||||
if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; }
|
||||
|
||||
IncomingTextMessage _textMessage = new IncomingTextMessage(sender,
|
||||
content.getSenderDevice(),
|
||||
message.getTimestamp(), body,
|
||||
@ -1217,10 +1239,29 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
private void acceptFriendRequestIfNeeded(@NonNull SignalServiceContent content) {
|
||||
// If we get anything other than a friend request, we can assume that we have a session with the other user
|
||||
if (content.isFriendRequest() || isGroupChatMessage(content)) { return; }
|
||||
becomeFriendsWithContact(content.getSender(), true);
|
||||
becomeFriendsWithContact(content.getSender(), true, false);
|
||||
}
|
||||
|
||||
private void becomeFriendsWithContact(String pubKey, boolean syncContact) {
|
||||
private void handleSessionRequestIfNeeded(@NonNull SignalServiceContent content) {
|
||||
if (content.isFriendRequest() && isSessionRequest(content)) {
|
||||
// Check if the session request from a member in one of our groups or our friend
|
||||
LokiStorageAPI.shared.getPrimaryDevicePublicKey(content.getSender()).success(primaryDevicePublicKey -> {
|
||||
String sender = primaryDevicePublicKey != null ? primaryDevicePublicKey : content.getSender();
|
||||
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(sender), false));
|
||||
LokiThreadFriendRequestStatus threadFriendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID);
|
||||
boolean isOurFriend = threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS;
|
||||
boolean isInOneOfOurGroups = DatabaseFactory.getGroupDatabase(context).signalGroupsHaveMember(sender);
|
||||
boolean shouldAcceptSessionRequest = isOurFriend || isInOneOfOurGroups;
|
||||
if (shouldAcceptSessionRequest) {
|
||||
// Send a background message to acknowledge session request
|
||||
MessageSender.sendBackgroundMessage(context, content.getSender());
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void becomeFriendsWithContact(String pubKey, boolean syncContact, boolean force) {
|
||||
LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context);
|
||||
Recipient contactID = Recipient.from(context, Address.fromSerialized(pubKey), false);
|
||||
if (contactID.isGroupRecipient()) return;
|
||||
@ -1228,6 +1269,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID);
|
||||
LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID);
|
||||
if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; }
|
||||
|
||||
// We shouldn't be able to skip from None -> Friends in normal circumstances.
|
||||
// Multi-device is the exception to this rule because we want to automatically be friends with a secondary device
|
||||
if (!force && threadFriendRequestStatus == LokiThreadFriendRequestStatus.NONE) { return; }
|
||||
|
||||
// If the thread's friend request status is not `FRIENDS`, but we're receiving a message,
|
||||
// it must be a friend request accepted message. Declining a friend request doesn't send a message.
|
||||
lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS);
|
||||
@ -1248,13 +1294,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
|
||||
private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
|
||||
if (!content.isFriendRequest() || message.isGroupUpdate()) { return; }
|
||||
if (!content.isFriendRequest() || message.isGroupMessage() || message.isSessionRequest()) { return; }
|
||||
// This handles the case where another user sends us a regular message without authorisation
|
||||
Promise<Boolean, Exception> promise = PromiseUtil.timeout(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), 8000);
|
||||
boolean shouldBecomeFriends = PromiseUtil.get(promise, false);
|
||||
if (shouldBecomeFriends) {
|
||||
// Become friends AND update the message they sent
|
||||
becomeFriendsWithContact(content.getSender(), true);
|
||||
becomeFriendsWithContact(content.getSender(), true, true);
|
||||
// Send them an accept message back
|
||||
MessageSender.sendBackgroundMessage(context, content.getSender());
|
||||
} else {
|
||||
@ -1561,10 +1607,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
long threadId;
|
||||
|
||||
if (typingMessage.getGroupId().isPresent()) {
|
||||
// Typing messages should only apply to signal groups, thus we use `getEncodedId`
|
||||
Address groupAddress = Address.fromSerialized(GroupUtil.getEncodedId(typingMessage.getGroupId().get(), false));
|
||||
Recipient groupRecipient = Recipient.from(context, groupAddress, false);
|
||||
|
||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(groupRecipient);
|
||||
} else {
|
||||
// See if we need to redirect the message
|
||||
author = getPrimaryDeviceRecipient(content.getSender());
|
||||
@ -1712,15 +1759,15 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
|
||||
private Recipient getSyncMessageDestination(SentTranscriptMessage message) {
|
||||
if (message.getMessage().getGroupInfo().isPresent()) {
|
||||
return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get().getGroupId(), false)), false);
|
||||
if (message.getMessage().isGroupMessage()) {
|
||||
return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false);
|
||||
} else {
|
||||
return Recipient.from(context, Address.fromSerialized(message.getDestination().get()), false);
|
||||
}
|
||||
}
|
||||
|
||||
private Recipient getSyncMessagePrimaryDestination(SentTranscriptMessage message) {
|
||||
if (message.getMessage().getGroupInfo().isPresent()) {
|
||||
if (message.getMessage().isGroupMessage()) {
|
||||
return getSyncMessageDestination(message);
|
||||
} else {
|
||||
return getPrimaryDeviceRecipient(message.getDestination().get());
|
||||
@ -1728,15 +1775,15 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
|
||||
private Recipient getMessageDestination(SignalServiceContent content, SignalServiceDataMessage message) {
|
||||
if (message.getGroupInfo().isPresent()) {
|
||||
return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false)), false);
|
||||
if (message.isGroupMessage()) {
|
||||
return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())), false);
|
||||
} else {
|
||||
return Recipient.from(context, Address.fromSerialized(content.getSender()), false);
|
||||
}
|
||||
}
|
||||
|
||||
private Recipient getMessagePrimaryDestination(SignalServiceContent content, SignalServiceDataMessage message) {
|
||||
if (message.getGroupInfo().isPresent()) {
|
||||
if (message.isGroupMessage()) {
|
||||
return getMessageDestination(content, message);
|
||||
} else {
|
||||
return getPrimaryDeviceRecipient(content.getSender());
|
||||
@ -1794,7 +1841,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
return true;
|
||||
} else if (conversation.isGroupRecipient()) {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<String> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get().getGroupId(), false))
|
||||
Optional<String> groupId = message.getGroupInfo().isPresent() ? Optional.of(GroupUtil.getEncodedId(message.getGroupInfo().get()))
|
||||
: Optional.absent();
|
||||
|
||||
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
|
||||
@ -1830,8 +1877,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isSessionRequest(SignalServiceContent content) {
|
||||
return content.getDataMessage().isPresent() && content.getDataMessage().get().isSessionRequest();
|
||||
}
|
||||
|
||||
private boolean isGroupChatMessage(SignalServiceContent content) {
|
||||
return content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupInfo().isPresent();
|
||||
return content.getDataMessage().isPresent() && content.getDataMessage().get().isGroupMessage();
|
||||
}
|
||||
|
||||
private void resetRecipientToPush(@NonNull Recipient recipient) {
|
||||
|
@ -12,8 +12,10 @@ import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
@ -25,13 +27,17 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.loki.MultiDeviceUtilities;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
@ -46,10 +52,13 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChat;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -157,7 +166,28 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList();
|
||||
else target = getGroupMessageRecipients(message.getRecipient().getAddress().toGroupString(), messageId);
|
||||
|
||||
List<SendMessageResult> results = deliver(message, target);
|
||||
String localNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
|
||||
// Only send messages to the contacts we have sessions with
|
||||
List<Address> validTargets = Stream.of(target).filter(member -> {
|
||||
// Our device is always valid
|
||||
if (member.serialize().equalsIgnoreCase(localNumber)) { return true; }
|
||||
|
||||
SignalProtocolAddress protocolAddress = new SignalProtocolAddress(member.toPhoneString(), SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
boolean hasSession = new TextSecureSessionStore(context).containsSession(protocolAddress);
|
||||
if (hasSession) { return true; }
|
||||
|
||||
// We should allow sending if we have a prekeybundle for the contact
|
||||
return DatabaseFactory.getLokiPreKeyBundleDatabase(context).hasPreKeyBundle(member.toPhoneString());
|
||||
}).toList();
|
||||
|
||||
// Send a session request to the other devices
|
||||
List<Address> others = Stream.of(target).filter(t -> !validTargets.contains(t)).toList();
|
||||
for (Address device : others) {
|
||||
MessageSender.sendBackgroundSessionRequest(context, device.toPhoneString());
|
||||
}
|
||||
|
||||
List<SendMessageResult> results = deliver(message, validTargets);
|
||||
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList();
|
||||
List<IdentityKeyMismatch> identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(Address.fromSerialized(result.getAddress().getNumber()), result.getIdentityFailure().getIdentityKey())).toList();
|
||||
Set<Address> successAddresses = Stream.of(results).filter(result -> result.getSuccess() != null).map(result -> Address.fromSerialized(result.getAddress().getNumber())).collect(Collectors.toSet());
|
||||
@ -231,7 +261,15 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
throws IOException, UntrustedIdentityException, UndeliverableMessageException {
|
||||
// rotateSenderCertificateIfNecessary();
|
||||
|
||||
String groupId = message.getRecipient().getAddress().toGroupString();
|
||||
// Messages shouldn't be able to be sent to RSS Feeds
|
||||
Address groupAddress = message.getRecipient().getAddress();
|
||||
if (groupAddress.isRSSFeed()) {
|
||||
List<SendMessageResult> results = new ArrayList<>();
|
||||
for (Address destination : destinations) results.add(SendMessageResult.networkFailure(new SignalServiceAddress(destination.toPhoneString())));
|
||||
return results;
|
||||
}
|
||||
|
||||
String groupId = groupAddress.toGroupString();
|
||||
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
|
||||
Optional<Quote> quote = getQuoteFor(message);
|
||||
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
|
||||
@ -247,24 +285,28 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
.map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient))
|
||||
.toList();
|
||||
|
||||
if (message.isGroup()) {
|
||||
SignalServiceGroup.GroupType groupType = SignalServiceGroup.GroupType.SIGNAL;
|
||||
if (groupAddress.isPublicChat()) {
|
||||
groupType = SignalServiceGroup.GroupType.PUBLIC_CHAT;
|
||||
}
|
||||
|
||||
if (message.isGroup() && groupAddress.isSignalGroup()) {
|
||||
// Loki - Only send GroupUpdate or GroupQuit to signal groups
|
||||
OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
|
||||
GroupContext groupContext = groupMessage.getGroupContext();
|
||||
SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0);
|
||||
SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE;
|
||||
SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupContext.getName(), groupContext.getMembersList(), avatar);
|
||||
SignalServiceGroup group = new SignalServiceGroup(type, GroupUtil.getDecodedId(groupId), groupType, groupContext.getName(), groupContext.getMembersList(), avatar, groupContext.getAdminsList());
|
||||
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.withExpiration(message.getRecipient().getExpireMessages())
|
||||
.withBody(message.getBody())
|
||||
.asGroupMessage(group)
|
||||
.build();
|
||||
|
||||
// Loki - Disable group updates for now
|
||||
List<SendMessageResult> results = new ArrayList<>();
|
||||
for (Address destination : destinations) results.add(SendMessageResult.success(new SignalServiceAddress(destination.toPhoneString()), false, false));
|
||||
return results;
|
||||
return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage);
|
||||
} else {
|
||||
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId));
|
||||
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedId(groupId), groupType);
|
||||
SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(message.getSentTimeMillis())
|
||||
.asGroupMessage(group)
|
||||
@ -284,26 +326,56 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
|
||||
}
|
||||
|
||||
private @NonNull List<Address> getGroupMessageRecipients(String groupId, long messageId) {
|
||||
ArrayList<Address> result = new ArrayList<>();
|
||||
if (GroupUtil.isRssFeed(groupId)) { return new ArrayList<>(); }
|
||||
|
||||
// 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));
|
||||
// Loki - All public chat group messages should be directed to their respective servers
|
||||
if (GroupUtil.isPublicChat(groupId)) {
|
||||
ArrayList<Address> result = new ArrayList<>();
|
||||
long threadID = GroupManager.getThreadIdFromGroupId(groupId, context);
|
||||
LokiPublicChat publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID);
|
||||
if (publicChat != null) {
|
||||
result.add(Address.fromSerialized(groupId));
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
/*
|
||||
Our biggest assumption here is that group members will only consist of primary devices.
|
||||
No secondary device should be able to be added to a group.
|
||||
*/
|
||||
List<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
|
||||
|
||||
Set<Address> memberSet = new HashSet<>();
|
||||
if (destinations.isEmpty()) {
|
||||
List<Recipient> groupMembers = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
|
||||
memberSet.addAll(Stream.of(groupMembers).map(Recipient::getAddress).toList());
|
||||
} else {
|
||||
memberSet.addAll(Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList());
|
||||
}
|
||||
|
||||
// Replace primary device public key with ours so message syncing works correctly
|
||||
String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
String localNumber = TextSecurePreferences.getLocalNumber(context);
|
||||
if (masterHexEncodedPublicKey != null && memberSet.contains(Address.fromSerialized(masterHexEncodedPublicKey))) {
|
||||
memberSet.remove(Address.fromSerialized(masterHexEncodedPublicKey));
|
||||
memberSet.add(Address.fromSerialized(localNumber));
|
||||
}
|
||||
|
||||
// Add secondary devices to the list. We shouldn't add our secondary devices
|
||||
for (Address member : memberSet) {
|
||||
if (!member.isPhone() || member.serialize().equalsIgnoreCase(localNumber)) { continue; }
|
||||
try {
|
||||
List<String> secondaryDevices = PromiseUtil.timeout(LokiStorageAPI.shared.getSecondaryDevicePublicKeys(member.serialize()), 5000).get();
|
||||
memberSet.addAll(Stream.of(secondaryDevices).map(string -> {
|
||||
// Loki - Calling .map(Address::fromSerialized) is causing errors, thus we use the long method :(
|
||||
return Address.fromSerialized(string);
|
||||
}).toList());
|
||||
} catch (Exception e) {
|
||||
// Timed out, go to the next member
|
||||
}
|
||||
}
|
||||
|
||||
return new LinkedList<>(memberSet);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
/*
|
||||
List<GroupReceiptInfo> destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
|
||||
if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getAddress).toList();
|
||||
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
|
||||
return Stream.of(members).map(Recipient::getAddress).toList();
|
||||
*/
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<PushGroupSendJob> {
|
||||
|
@ -97,15 +97,20 @@ public class PushGroupUpdateJob extends BaseJob implements InjectableType {
|
||||
}
|
||||
|
||||
List<String> members = new LinkedList<>();
|
||||
|
||||
for (Address member : record.get().getMembers()) {
|
||||
members.add(member.serialize());
|
||||
}
|
||||
|
||||
List<String> admins = new LinkedList<>();
|
||||
for (Address admin : record.get().getAdmins()) {
|
||||
admins.add(admin.serialize());
|
||||
}
|
||||
|
||||
SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE)
|
||||
.withAvatar(avatar)
|
||||
.withId(groupId)
|
||||
.withId(groupId, SignalServiceGroup.GroupType.SIGNAL)
|
||||
.withMembers(members)
|
||||
.withAdmins(admins)
|
||||
.withName(record.get().getTitle())
|
||||
.build();
|
||||
|
||||
|
@ -71,7 +71,7 @@ public class RequestGroupInfoJob extends BaseJob implements InjectableType {
|
||||
@Override
|
||||
public void onRun() throws IOException, UntrustedIdentityException {
|
||||
SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO)
|
||||
.withId(groupId)
|
||||
.withId(groupId, SignalServiceGroup.GroupType.SIGNAL)
|
||||
.build();
|
||||
|
||||
SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
|
||||
|
@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage.Action;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
|
||||
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
@ -101,6 +102,10 @@ public class TypingSendJob extends BaseJob implements InjectableType {
|
||||
// Loki - Don't send typing indicators in group chats or to ourselves
|
||||
if (recipient.isGroupRecipient()) { return; }
|
||||
|
||||
|
||||
LokiThreadFriendRequestStatus friendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadId);
|
||||
if (friendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) { return; }
|
||||
|
||||
boolean isOurDevice = PromiseUtil.get(MultiDeviceUtilities.isOneOfOurDevices(context, recipient.getAddress()), false);
|
||||
if (!isOurDevice) {
|
||||
messageSender.sendTyping(0, addresses, unidentifiedAccess, typingMessage);
|
||||
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki
|
||||
import android.content.Context
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
|
||||
|
||||
@ -62,7 +63,7 @@ object FriendRequestHandler {
|
||||
fun updateLastFriendRequestMessage(context: Context, threadId: Long, status: LokiMessageFriendRequestStatus) {
|
||||
if (threadId < 0) { return }
|
||||
val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return
|
||||
if (!recipient.address.isPhone) { return }
|
||||
if (!recipient.address.isPhone || recipient.address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return }
|
||||
|
||||
val messages = DatabaseFactory.getSmsDatabase(context).getAllMessageIDs(threadId)
|
||||
val lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context)
|
||||
@ -81,7 +82,7 @@ object FriendRequestHandler {
|
||||
// We only want to update the last message status if we're not friends with any of their linked devices
|
||||
// This ensures that we don't spam the UI with accept/decline messages
|
||||
val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId) ?: return
|
||||
if (!recipient.address.isPhone) { return }
|
||||
if (!recipient.address.isPhone || recipient.address.serialize() == TextSecurePreferences.getLocalNumber(context)) { return }
|
||||
|
||||
isFriendsWithAnyLinkedDevice(context, recipient).successUi { isFriends ->
|
||||
if (isFriends) { return@successUi }
|
||||
|
@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPublicChatPoller
|
||||
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>()
|
||||
@ -50,10 +49,10 @@ class LokiPublicChatManager(private val context: Context) {
|
||||
|
||||
public fun addChat(server: String, channel: Long, name: String): LokiPublicChat {
|
||||
val chat = LokiPublicChat(channel, server, name, true)
|
||||
var threadID = GroupManager.getThreadId(chat.id, context)
|
||||
var threadID = GroupManager.getPublicChatThreadId(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)
|
||||
val result = GroupManager.createPublicChatGroup(chat.id, context, null, chat.displayName)
|
||||
threadID = result.threadId
|
||||
}
|
||||
DatabaseFactory.getLokiThreadDatabase(context).setPublicChat(chat, threadID)
|
||||
@ -74,7 +73,7 @@ class LokiPublicChatManager(private val context: Context) {
|
||||
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()
|
||||
chats = chatsInDB.filter { GroupManager.getPublicChatThreadId(it.value.id, context) > -1 }.toMutableMap()
|
||||
}
|
||||
|
||||
private fun listenToThreadDeletion(threadID: Long) {
|
||||
|
@ -45,6 +45,11 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
|
||||
fun getFriendRequestStatus(threadID: Long): LokiThreadFriendRequestStatus {
|
||||
if (threadID < 0) { return LokiThreadFriendRequestStatus.NONE }
|
||||
|
||||
// Loki - Friend request logic doesn't apply to group chats, always treat them as friends
|
||||
val recipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)
|
||||
if (recipient != null && recipient.isGroupRecipient) { return LokiThreadFriendRequestStatus.FRIENDS; }
|
||||
|
||||
val database = databaseHelper.readableDatabase
|
||||
val result = database.get(friendRequestTableName, "${Companion.threadID} = ?", arrayOf( threadID.toString() )) { cursor ->
|
||||
cursor.getInt(friendRequestStatus)
|
||||
|
@ -33,6 +33,9 @@ data class BackgroundMessage private constructor(val data: Map<String, Any>) {
|
||||
|
||||
@JvmStatic
|
||||
fun createSessionRestore(recipient: String) = BackgroundMessage(mapOf( "recipient" to recipient, "friendRequest" to true, "sessionRestore" to true ))
|
||||
|
||||
@JvmStatic
|
||||
fun createSessionRequest(recipient: String) = BackgroundMessage(mapOf("recipient" to recipient, "friendRequest" to true, "sessionRequest" to true))
|
||||
|
||||
internal fun parse(serialized: String): BackgroundMessage {
|
||||
val data = JsonUtil.fromJson(serialized, Map::class.java) as? Map<String, Any> ?: throw AssertionError("JSON parsing failed")
|
||||
@ -99,6 +102,10 @@ class PushBackgroundMessageSendJob private constructor(
|
||||
dataMessage.asSessionRestore(true)
|
||||
}
|
||||
|
||||
if (message.get("sessionRequest", false)) {
|
||||
dataMessage.asSessionRequest(true)
|
||||
}
|
||||
|
||||
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
|
||||
val address = SignalServiceAddress(recipient)
|
||||
try {
|
||||
|
@ -0,0 +1,152 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.LoaderManager
|
||||
import android.support.v4.content.Loader
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_create_closed_group.*
|
||||
import kotlinx.android.synthetic.main.activity_linked_devices.recyclerView
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), MemberClickListener, LoaderManager.LoaderCallbacks<List<String>> {
|
||||
private var members = listOf<String>()
|
||||
set(value) { field = value; createClosedGroupAdapter.members = value }
|
||||
|
||||
private val createClosedGroupAdapter by lazy {
|
||||
val result = CreateClosedGroupAdapter(this)
|
||||
result.glide = GlideApp.with(this)
|
||||
result.memberClickListener = this
|
||||
result
|
||||
}
|
||||
|
||||
private val selectedMembers: Set<String>
|
||||
get() { return createClosedGroupAdapter.selectedMembers }
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
setContentView(R.layout.activity_create_closed_group)
|
||||
supportActionBar!!.title = "New Closed Group"
|
||||
recyclerView.adapter = createClosedGroupAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_create_closed_group, menu)
|
||||
return true
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<List<String>> {
|
||||
return CreateClosedGroupLoader(this)
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<List<String>>, members: List<String>) {
|
||||
update(members)
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<List<String>>) {
|
||||
update(listOf())
|
||||
}
|
||||
|
||||
private fun update(members: List<String>) {
|
||||
this.members = members
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val id = item.itemId
|
||||
when(id) {
|
||||
R.id.createClosedGroupButton -> createClosedGroup()
|
||||
else -> { /* Do nothing */ }
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onMemberClick(member: String) {
|
||||
createClosedGroupAdapter.onMemberClick(member)
|
||||
}
|
||||
|
||||
private fun createClosedGroup() {
|
||||
val name = nameEditText.text.trim()
|
||||
if (name.isEmpty()) {
|
||||
return Toast.makeText(this, "Please enter a group name", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (name.length >= 64) {
|
||||
return Toast.makeText(this, "Please enter a shorter group name", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
val selectedMembers = this.selectedMembers
|
||||
if (selectedMembers.count() < 1) {
|
||||
return Toast.makeText(this, "Please pick at least 1 group member", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
val recipients = selectedMembers.map {
|
||||
Recipient.from(this, Address.fromSerialized(it), false)
|
||||
}.toSet()
|
||||
val ourNumber = TextSecurePreferences.getMasterHexEncodedPublicKey(this) ?: TextSecurePreferences.getLocalNumber(this)
|
||||
val local = Recipient.from(this, Address.fromSerialized(ourNumber), false)
|
||||
CreateClosedGroupTask(WeakReference(this), null, name.toString(), recipients, setOf(local)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
|
||||
}
|
||||
|
||||
private fun handleOpenConversation(threadId: Long, recipient: Recipient) {
|
||||
val intent = Intent(this, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Tasks
|
||||
internal class CreateClosedGroupTask(
|
||||
private val activity: WeakReference<CreateClosedGroupActivity>,
|
||||
private val avatar: Bitmap?,
|
||||
private val name: String?,
|
||||
private val members: Set<Recipient>,
|
||||
private val admins: Set<Recipient>
|
||||
) : AsyncTask<Void, Void, Optional<GroupManager.GroupActionResult>>() {
|
||||
|
||||
override fun doInBackground(vararg params: Void?): Optional<GroupManager.GroupActionResult> {
|
||||
val activity = activity.get() ?: return Optional.absent()
|
||||
return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false, admins))
|
||||
}
|
||||
|
||||
override fun onPostExecute(result: Optional<GroupManager.GroupActionResult>) {
|
||||
val activity = activity.get()
|
||||
if (activity == null) {
|
||||
super.onPostExecute(result)
|
||||
return
|
||||
}
|
||||
|
||||
if (result.isPresent && result.get().threadId > -1) {
|
||||
if (!activity.isFinishing) {
|
||||
activity.handleOpenConversation(result.get().threadId, result.get().groupRecipient)
|
||||
}
|
||||
} else {
|
||||
super.onPostExecute(result)
|
||||
Toast.makeText(activity.applicationContext,
|
||||
R.string.GroupCreateActivity_contacts_invalid_number, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import org.thoughtcrime.securesms.loki.redesign.views.UserView
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class CreateClosedGroupAdapter(private val context: Context) : RecyclerView.Adapter<CreateClosedGroupAdapter.ViewHolder>() {
|
||||
lateinit var glide: GlideRequests
|
||||
val selectedMembers = mutableSetOf<String>()
|
||||
var members = listOf<String>()
|
||||
set(value) { field = value; notifyDataSetChanged() }
|
||||
var memberClickListener: MemberClickListener? = null
|
||||
|
||||
class ViewHolder(val view: UserView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return members.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = UserView(context)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
|
||||
val member = members[position]
|
||||
viewHolder.view.setOnClickListener { memberClickListener?.onMemberClick(member) }
|
||||
val isSelected = selectedMembers.contains(member)
|
||||
viewHolder.view.bind(member, isSelected, glide)
|
||||
}
|
||||
|
||||
fun onMemberClick(member: String) {
|
||||
if (selectedMembers.contains(member)) {
|
||||
selectedMembers.remove(member)
|
||||
} else {
|
||||
selectedMembers.add(member)
|
||||
}
|
||||
val index = members.indexOf(member)
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
interface MemberClickListener {
|
||||
|
||||
fun onMemberClick(member: String)
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus
|
||||
|
||||
class CreateClosedGroupLoader(context: Context) : AsyncLoader<List<String>>(context) {
|
||||
|
||||
override fun loadInBackground(): List<String> {
|
||||
val threadDatabase = DatabaseFactory.getThreadDatabase(context)
|
||||
val lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context)
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val deviceLinks = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey)
|
||||
val userLinkedDeviceHexEncodedPublicKeys = deviceLinks.flatMap {
|
||||
listOf( it.primaryDevicePublicKey.toLowerCase(), it.secondaryDevicePublicKey.toLowerCase() )
|
||||
}.toMutableSet()
|
||||
userLinkedDeviceHexEncodedPublicKeys.add(userHexEncodedPublicKey.toLowerCase())
|
||||
val cursor = threadDatabase.conversationList
|
||||
val reader = threadDatabase.readerFor(cursor)
|
||||
val result = mutableListOf<String>()
|
||||
while (reader.next != null) {
|
||||
val thread = reader.current
|
||||
if (thread.recipient.isGroupRecipient) { continue }
|
||||
if (lokiThreadDatabase.getFriendRequestStatus(thread.threadId) != LokiThreadFriendRequestStatus.FRIENDS) { continue }
|
||||
val hexEncodedPublicKey = thread.recipient.address.toString().toLowerCase()
|
||||
if (userLinkedDeviceHexEncodedPublicKeys.contains(hexEncodedPublicKey)) { continue }
|
||||
result.add(hexEncodedPublicKey)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
@ -83,6 +83,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
profileButton.hexEncodedPublicKey = hexEncodedPublicKey
|
||||
profileButton.update()
|
||||
profileButton.setOnClickListener { openSettings() }
|
||||
createClosedGroupButton.setOnClickListener { createClosedGroup() }
|
||||
joinPublicChatButton.setOnClickListener { joinPublicChat() }
|
||||
// Set up seed reminder view
|
||||
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
|
||||
@ -182,6 +183,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
|
||||
show(intent)
|
||||
}
|
||||
|
||||
private fun createClosedGroup() {
|
||||
val intent = Intent(this, CreateClosedGroupActivity::class.java)
|
||||
show(intent)
|
||||
}
|
||||
|
||||
private fun joinPublicChat() {
|
||||
val intent = Intent(this, JoinPublicChatActivity::class.java)
|
||||
show(intent)
|
||||
|
@ -111,7 +111,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
|
||||
// region Polling
|
||||
private fun getDataMessage(message: LokiPublicChatMessage): SignalServiceDataMessage {
|
||||
val id = group.id.toByteArray()
|
||||
val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null)
|
||||
val serviceGroup = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.PUBLIC_CHAT, null, null, null, null)
|
||||
val quote = if (message.quote != null) {
|
||||
SignalServiceDataMessage.Quote(message.quote!!.quotedMessageTimestamp, SignalServiceAddress(message.quote!!.quoteeHexEncodedPublicKey), message.quote!!.quotedMessageBody, listOf())
|
||||
} else {
|
||||
|
@ -63,7 +63,7 @@ class LokiRSSFeedPoller(private val context: Context, private val feed: LokiRSSF
|
||||
bodyAsHTML = matcher.replaceAll("$2 ($1)")
|
||||
val body = Html.fromHtml(bodyAsHTML).toString().trim()
|
||||
val id = feed.id.toByteArray()
|
||||
val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, null, null, null)
|
||||
val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.RSS_FEED, null, null, null, null)
|
||||
val x2 = SignalServiceDataMessage(timestamp, x1, null, body)
|
||||
val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false, false)
|
||||
PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.absent())
|
||||
|
@ -0,0 +1,52 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_conversation.view.profilePictureView
|
||||
import kotlinx.android.synthetic.main.view_user.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class UserView : LinearLayout {
|
||||
var user: String? = null
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.applicationContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_user, null)
|
||||
addView(contentView)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(user: String, isSelected: Boolean, glide: GlideRequests) {
|
||||
profilePictureView.hexEncodedPublicKey = user
|
||||
profilePictureView.additionalHexEncodedPublicKey = null
|
||||
profilePictureView.isRSSFeed = false
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.update()
|
||||
nameTextView.text = Recipient.from(context, Address.fromSerialized(user), false).name ?: "Unknown Contact"
|
||||
tickImageView.setImageResource(if (isSelected) R.drawable.ic_circle_check else R.drawable.ic_circle)
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -79,7 +79,7 @@ public class IncomingMediaMessage {
|
||||
this.quote = quote.orNull();
|
||||
this.unidentified = unidentified;
|
||||
|
||||
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get().getGroupId(), false));
|
||||
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get()));
|
||||
else this.groupId = null;
|
||||
|
||||
this.attachments.addAll(PointerAttachment.forPointers(attachments));
|
||||
|
@ -78,7 +78,7 @@ public class IncomingTextMessage implements Parcelable {
|
||||
this.unidentified = unidentified;
|
||||
|
||||
if (group.isPresent()) {
|
||||
this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get().getGroupId(), false));
|
||||
this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get()));
|
||||
} else {
|
||||
this.groupId = null;
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
package org.thoughtcrime.securesms.sms;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
@ -60,11 +59,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI;
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiThreadFriendRequestStatus;
|
||||
import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
|
||||
@ -143,6 +138,10 @@ public class MessageSender {
|
||||
public static void sendRestoreSessionMessage(Context context, String contactHexEncodedPublicKey) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRestore(contactHexEncodedPublicKey)));
|
||||
}
|
||||
|
||||
public static void sendBackgroundSessionRequest(Context context, String contactHexEncodedPublicKey) {
|
||||
ApplicationContext.getInstance(context).getJobManager().add(new PushBackgroundMessageSendJob(BackgroundMessage.createSessionRequest(contactHexEncodedPublicKey)));
|
||||
}
|
||||
// endregion
|
||||
|
||||
public static long send(final Context context,
|
||||
@ -202,7 +201,7 @@ public class MessageSender {
|
||||
if (attachment != null) { message.getAttachments().add(attachment); }
|
||||
long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
|
||||
// Loki - Set the message's friend request status as soon as it has hit the database
|
||||
if (message.isFriendRequest) {
|
||||
if (message.isFriendRequest && !recipient.getAddress().isGroup() && !message.isGroup()) {
|
||||
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
|
||||
}
|
||||
sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
|
||||
@ -215,7 +214,7 @@ public class MessageSender {
|
||||
try {
|
||||
long messageID = database.insertMessageOutbox(message, allocatedThreadId, forceSms, insertListener);
|
||||
// Loki - Set the message's friend request status as soon as it has hit the database
|
||||
if (message.isFriendRequest) {
|
||||
if (message.isFriendRequest && !recipient.getAddress().isGroup() && !message.isGroup()) {
|
||||
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, allocatedThreadId);
|
||||
}
|
||||
sendMediaMessage(context, recipient, forceSms, messageID, message.getExpiresIn());
|
||||
|
@ -11,11 +11,13 @@ import network.loki.messenger.R;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.*;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientModifiedListener;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
@ -28,12 +30,32 @@ public class GroupUtil {
|
||||
|
||||
private static final String ENCODED_SIGNAL_GROUP_PREFIX = "__textsecure_group__!";
|
||||
private static final String ENCODED_MMS_GROUP_PREFIX = "__signal_mms_group__!";
|
||||
private static final String ENCODED_PUBLIC_CHAT_GROUP_PREFIX = "__loki_public_chat_group__!";
|
||||
private static final String ENCODED_RSS_FEED_GROUP_PREFIX = "__loki_rss_feed_group__!";
|
||||
private static final String TAG = GroupUtil.class.getSimpleName();
|
||||
|
||||
public static String getEncodedId(SignalServiceGroup group) {
|
||||
byte[] groupId = group.getGroupId();
|
||||
if (group.getGroupType() == SignalServiceGroup.GroupType.PUBLIC_CHAT) {
|
||||
return getEncodedPublicChatId(groupId);
|
||||
} else if (group.getGroupType() == SignalServiceGroup.GroupType.RSS_FEED) {
|
||||
return getEncodedRSSFeedId(groupId);
|
||||
}
|
||||
return getEncodedId(groupId, false);
|
||||
}
|
||||
|
||||
public static String getEncodedId(byte[] groupId, boolean mms) {
|
||||
return (mms ? ENCODED_MMS_GROUP_PREFIX : ENCODED_SIGNAL_GROUP_PREFIX) + Hex.toStringCondensed(groupId);
|
||||
}
|
||||
|
||||
public static String getEncodedPublicChatId(byte[] groupId) {
|
||||
return ENCODED_PUBLIC_CHAT_GROUP_PREFIX + Hex.toStringCondensed(groupId);
|
||||
}
|
||||
|
||||
public static String getEncodedRSSFeedId(byte[] groupId) {
|
||||
return ENCODED_RSS_FEED_GROUP_PREFIX + Hex.toStringCondensed(groupId);
|
||||
}
|
||||
|
||||
public static byte[] getDecodedId(String groupId) throws IOException {
|
||||
if (!isEncodedGroup(groupId)) {
|
||||
throw new IOException("Invalid encoding");
|
||||
@ -48,13 +70,21 @@ public class GroupUtil {
|
||||
}
|
||||
|
||||
public static boolean isEncodedGroup(@NonNull String groupId) {
|
||||
return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX) || groupId.startsWith(ENCODED_MMS_GROUP_PREFIX);
|
||||
return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX) || groupId.startsWith(ENCODED_MMS_GROUP_PREFIX) || groupId.startsWith(ENCODED_PUBLIC_CHAT_GROUP_PREFIX) || groupId.startsWith(ENCODED_RSS_FEED_GROUP_PREFIX);
|
||||
}
|
||||
|
||||
public static boolean isMmsGroup(@NonNull String groupId) {
|
||||
return groupId.startsWith(ENCODED_MMS_GROUP_PREFIX);
|
||||
}
|
||||
|
||||
public static boolean isPublicChat(@NonNull String groupId) {
|
||||
return groupId.startsWith(ENCODED_PUBLIC_CHAT_GROUP_PREFIX);
|
||||
}
|
||||
|
||||
public static boolean isRssFeed(@NonNull String groupId) {
|
||||
return groupId.startsWith(ENCODED_RSS_FEED_GROUP_PREFIX);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static Optional<OutgoingGroupMediaMessage> createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) {
|
||||
String encodedGroupId = groupRecipient.getAddress().toGroupString();
|
||||
@ -88,7 +118,7 @@ public class GroupUtil {
|
||||
}
|
||||
|
||||
try {
|
||||
GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup));
|
||||
GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup));
|
||||
return new GroupDescription(context, groupContext);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
@ -100,24 +130,49 @@ public class GroupUtil {
|
||||
|
||||
@NonNull private final Context context;
|
||||
@Nullable private final GroupContext groupContext;
|
||||
@Nullable private final List<Recipient> members;
|
||||
private final List<Recipient> members;
|
||||
private final List<Recipient> removedMembers;
|
||||
private boolean ourDeviceWasRemoved;
|
||||
|
||||
public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.groupContext = groupContext;
|
||||
|
||||
if (groupContext == null || groupContext.getMembersList().isEmpty()) {
|
||||
this.members = null;
|
||||
} else {
|
||||
this.members = new LinkedList<>();
|
||||
this.members = new LinkedList<>();
|
||||
this.removedMembers = new LinkedList<>();
|
||||
this.ourDeviceWasRemoved = false;
|
||||
|
||||
for (String member : groupContext.getMembersList()) {
|
||||
this.members.add(Recipient.from(context, Address.fromExternal(context, member), true));
|
||||
if (groupContext != null && !groupContext.getMembersList().isEmpty()) {
|
||||
List<String> memberList = groupContext.getMembersList();
|
||||
List<Address> currentMembers = getCurrentGroupMembers();
|
||||
|
||||
// Add them to the member or removed members lists
|
||||
for (String member : memberList) {
|
||||
Address address = Address.fromSerialized(member);
|
||||
Recipient recipient = Recipient.from(context, address, true);
|
||||
if (currentMembers == null || currentMembers.contains(address)) {
|
||||
this.members.add(recipient);
|
||||
} else {
|
||||
this.removedMembers.add(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if our device was removed
|
||||
if (!removedMembers.isEmpty()) {
|
||||
String masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
|
||||
String hexEncodedPublicKey = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : TextSecurePreferences.getLocalNumber(context);
|
||||
Recipient self = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false);
|
||||
ourDeviceWasRemoved = removedMembers.contains(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String toString(Recipient sender) {
|
||||
// Show the local removed message
|
||||
if (ourDeviceWasRemoved) {
|
||||
return context.getString(R.string.GroupUtil_you_were_removed_from_group);
|
||||
}
|
||||
|
||||
StringBuilder description = new StringBuilder();
|
||||
description.append(context.getString(R.string.MessageRecord_s_updated_group, sender.toShortString()));
|
||||
|
||||
@ -127,14 +182,20 @@ public class GroupUtil {
|
||||
|
||||
String title = groupContext.getName();
|
||||
|
||||
if (members != null) {
|
||||
if (!members.isEmpty()) {
|
||||
description.append("\n");
|
||||
description.append(context.getResources().getQuantityString(R.plurals.GroupUtil_joined_the_group,
|
||||
members.size(), toString(members)));
|
||||
}
|
||||
|
||||
if (!removedMembers.isEmpty()) {
|
||||
description.append("\n");
|
||||
description.append(context.getResources().getQuantityString(R.plurals.GroupUtil_removed_from_the_group,
|
||||
removedMembers.size(), toString(removedMembers)));
|
||||
}
|
||||
|
||||
if (title != null && !title.trim().isEmpty()) {
|
||||
if (members != null) description.append(" ");
|
||||
if (!members.isEmpty()) description.append(" ");
|
||||
else description.append("\n");
|
||||
description.append(context.getString(R.string.GroupUtil_group_name_is_now, title));
|
||||
}
|
||||
@ -143,7 +204,7 @@ public class GroupUtil {
|
||||
}
|
||||
|
||||
public void addListener(RecipientModifiedListener listener) {
|
||||
if (this.members != null) {
|
||||
if (!this.members.isEmpty()) {
|
||||
for (Recipient member : this.members) {
|
||||
member.addListener(listener);
|
||||
}
|
||||
@ -162,5 +223,23 @@ public class GroupUtil {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Address> getCurrentGroupMembers() {
|
||||
if (groupContext == null) { return null; }
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
byte[] decodedGroupId = groupContext.getId().toByteArray();
|
||||
String signalGroupId = getEncodedId(decodedGroupId, false);
|
||||
String publicChatId = getEncodedPublicChatId(decodedGroupId);
|
||||
String rssFeedId = getEncodedRSSFeedId(decodedGroupId);
|
||||
GroupRecord groupRecord = null;
|
||||
if (!groupDatabase.isUnknownGroup(signalGroupId)) {
|
||||
groupRecord = groupDatabase.getGroup(signalGroupId).orNull();
|
||||
} else if (!groupDatabase.isUnknownGroup(publicChatId)) {
|
||||
groupRecord = groupDatabase.getGroup(publicChatId).orNull();
|
||||
} else if (!groupDatabase.isUnknownGroup(rssFeedId)) {
|
||||
groupRecord = groupDatabase.getGroup(rssFeedId).orNull();
|
||||
}
|
||||
return (groupRecord != null) ? groupRecord.getMembers() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,8 +74,9 @@ public class IdentityUtil {
|
||||
GroupDatabase.GroupRecord groupRecord;
|
||||
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
if (groupRecord.isRSSFeed() || groupRecord.isPublicChat()) { continue; }
|
||||
if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive() && !groupRecord.isMms()) {
|
||||
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
|
||||
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId(), SignalServiceGroup.GroupType.SIGNAL);
|
||||
|
||||
if (remote) {
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false);
|
||||
@ -126,8 +127,9 @@ public class IdentityUtil {
|
||||
GroupDatabase.GroupRecord groupRecord;
|
||||
|
||||
while ((groupRecord = reader.getNext()) != null) {
|
||||
if (groupRecord.isRSSFeed() || groupRecord.isPublicChat()) { continue; }
|
||||
if (groupRecord.getMembers().contains(recipient.getAddress()) && groupRecord.isActive()) {
|
||||
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId());
|
||||
SignalServiceGroup group = new SignalServiceGroup(groupRecord.getId(), SignalServiceGroup.GroupType.SIGNAL);
|
||||
IncomingTextMessage incoming = new IncomingTextMessage(recipient.getAddress(), 1, time, null, Optional.of(group), 0, false);
|
||||
IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user