New group management screen.
This commit is contained in:
parent
e0502c24e1
commit
723639d928
30 changed files with 1621 additions and 175 deletions
|
@ -257,6 +257,9 @@
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
android:theme="@style/TextSecure.LightNoActionBar" />
|
android:theme="@style/TextSecure.LightNoActionBar" />
|
||||||
|
|
||||||
|
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".DatabaseMigrationActivity"
|
<activity android:name=".DatabaseMigrationActivity"
|
||||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
|
|
@ -1,74 +1,48 @@
|
||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.lifecycle.Lifecycle;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
|
||||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientExporter;
|
|
||||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public final class GroupMembersDialog {
|
public final class GroupMembersDialog {
|
||||||
|
|
||||||
private final FragmentActivity fragmentActivity;
|
private final FragmentActivity fragmentActivity;
|
||||||
private final Recipient groupRecipient;
|
private final Recipient groupRecipient;
|
||||||
private final Lifecycle lifecycle;
|
|
||||||
|
|
||||||
public GroupMembersDialog(@NonNull FragmentActivity activity,
|
public GroupMembersDialog(@NonNull FragmentActivity activity,
|
||||||
@NonNull Recipient groupRecipient,
|
@NonNull Recipient groupRecipient)
|
||||||
@NonNull Lifecycle lifecycle)
|
|
||||||
{
|
{
|
||||||
this.fragmentActivity = activity;
|
this.fragmentActivity = activity;
|
||||||
this.groupRecipient = groupRecipient;
|
this.groupRecipient = groupRecipient;
|
||||||
this.lifecycle = lifecycle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void display() {
|
public void display() {
|
||||||
SimpleTask.run(
|
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
|
||||||
lifecycle,
|
.setTitle(R.string.ConversationActivity_group_members)
|
||||||
() -> DatabaseFactory.getGroupDatabase(fragmentActivity).getGroupMembers(groupRecipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF),
|
.setIconAttribute(R.attr.group_members_dialog_icon)
|
||||||
members -> {
|
.setCancelable(true)
|
||||||
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
|
.setView(R.layout.dialog_group_members)
|
||||||
.setTitle(R.string.ConversationActivity_group_members)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.setIconAttribute(R.attr.group_members_dialog_icon)
|
.show();
|
||||||
.setCancelable(true)
|
|
||||||
.setView(R.layout.dialog_group_members)
|
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show();
|
|
||||||
|
|
||||||
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
|
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
|
||||||
|
|
||||||
ArrayList<GroupMemberEntry.FullMember> pendingMembers = new ArrayList<>(members.size());
|
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
|
||||||
for (Recipient member : members) {
|
|
||||||
GroupMemberEntry.FullMember entry = new GroupMemberEntry.FullMember(member);
|
|
||||||
|
|
||||||
entry.setOnClick(() -> {
|
//noinspection ConstantConditions
|
||||||
dialog.dismiss();
|
liveGroup.getFullMembers().observe(fragmentActivity, memberListView::setMembers);
|
||||||
contactClick(member);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (member.isLocalNumber()) {
|
dialog.setOnDismissListener(d -> liveGroup.removeObservers(fragmentActivity));
|
||||||
pendingMembers.add(0, entry);
|
|
||||||
} else {
|
|
||||||
pendingMembers.add(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//noinspection ConstantConditions
|
memberListView.setRecipientClickListener(recipient -> {
|
||||||
memberListView.setMembers(pendingMembers);
|
dialog.dismiss();
|
||||||
}
|
contactClick(recipient);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void contactClick(@NonNull Recipient recipient) {
|
private void contactClick(@NonNull Recipient recipient) {
|
||||||
|
|
|
@ -148,7 +148,11 @@ import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||||
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
|
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
|
||||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||||
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
||||||
|
@ -846,6 +850,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true;
|
case R.id.menu_distribution_broadcast: handleDistributionBroadcastEnabled(item); return true;
|
||||||
case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true;
|
case R.id.menu_distribution_conversation: handleDistributionConversationEnabled(item); return true;
|
||||||
case R.id.menu_edit_group: handleEditPushGroup(); return true;
|
case R.id.menu_edit_group: handleEditPushGroup(); return true;
|
||||||
|
case R.id.menu_manage_group: handleManagePushGroup(); return true;
|
||||||
case R.id.menu_pending_members: handlePendingMembers(); return true;
|
case R.id.menu_pending_members: handlePendingMembers(); return true;
|
||||||
case R.id.menu_leave: handleLeavePushGroup(); return true;
|
case R.id.menu_leave: handleLeavePushGroup(); return true;
|
||||||
case R.id.menu_invite: handleInviteLink(); return true;
|
case R.id.menu_invite: handleInviteLink(); return true;
|
||||||
|
@ -924,29 +929,43 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
//////// Event Handlers
|
//////// Event Handlers
|
||||||
|
|
||||||
private void handleSelectMessageExpiration() {
|
private void handleSelectMessageExpiration() {
|
||||||
if (isPushGroupConversation() && !isActiveGroup()) {
|
boolean activeGroup = isActiveGroup();
|
||||||
|
|
||||||
|
if (isPushGroupConversation() && !activeGroup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//noinspection CodeBlock2Expr
|
ExpirationDialog.show(this, recipient.get().getExpireMessages(),
|
||||||
ExpirationDialog.show(this, recipient.get().getExpireMessages(), expirationTime -> {
|
expirationTime ->
|
||||||
new AsyncTask<Void, Void, Void>() {
|
SimpleTask.run(
|
||||||
@Override
|
getLifecycle(),
|
||||||
protected Void doInBackground(Void... params) {
|
() -> {
|
||||||
DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient.getId(), expirationTime);
|
if (activeGroup) {
|
||||||
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipient(), System.currentTimeMillis(), expirationTime * 1000L);
|
try {
|
||||||
MessageSender.send(ConversationActivity.this, outgoingMessage, threadId, false, null);
|
GroupManager.updateGroupTimer(ConversationActivity.this, getRecipient().requireGroupId().requirePush(), expirationTime);
|
||||||
|
} catch (GroupInsufficientRightsException e) {
|
||||||
return null;
|
Log.w(TAG, e);
|
||||||
}
|
return ConversationActivity.this.getString(R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this);
|
||||||
|
} catch (GroupChangeFailedException e) {
|
||||||
@Override
|
Log.w(TAG, e);
|
||||||
protected void onPostExecute(Void result) {
|
return ConversationActivity.this.getString(R.string.ManageGroupActivity_failed_to_update_the_group);
|
||||||
invalidateOptionsMenu();
|
}
|
||||||
if (fragment != null) fragment.setLastSeen(0);
|
} else {
|
||||||
}
|
DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient.getId(), expirationTime);
|
||||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipient(), System.currentTimeMillis(), expirationTime * 1000L);
|
||||||
});
|
MessageSender.send(ConversationActivity.this, outgoingMessage, threadId, false, null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
(errorString) -> {
|
||||||
|
if (errorString != null) {
|
||||||
|
Toast.makeText(ConversationActivity.this, errorString, Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
invalidateOptionsMenu();
|
||||||
|
if (fragment != null) fragment.setLastSeen(0);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleMuteNotifications() {
|
private void handleMuteNotifications() {
|
||||||
|
@ -1129,6 +1148,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
startActivityForResult(intent, GROUP_EDIT);
|
startActivityForResult(intent, GROUP_EDIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleManagePushGroup() {
|
||||||
|
startActivityForResult(ManageGroupActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId()), GROUP_EDIT);
|
||||||
|
}
|
||||||
|
|
||||||
private void handlePendingMembers() {
|
private void handlePendingMembers() {
|
||||||
startActivity(PendingMemberInvitesActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId().requireV2()));
|
startActivity(PendingMemberInvitesActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId().requireV2()));
|
||||||
}
|
}
|
||||||
|
@ -1182,7 +1205,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleDisplayGroupRecipients() {
|
private void handleDisplayGroupRecipients() {
|
||||||
new GroupMembersDialog(this, getRecipient(), getLifecycle()).display();
|
new GroupMembersDialog(this, getRecipient()).display();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAddToContacts() {
|
private void handleAddToContacts() {
|
||||||
|
|
|
@ -15,12 +15,14 @@ import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
import net.sqlcipher.database.SQLiteDatabase;
|
import net.sqlcipher.database.SQLiteDatabase;
|
||||||
|
|
||||||
|
import org.signal.storageservice.protos.groups.AccessControl;
|
||||||
import org.signal.storageservice.protos.groups.Member;
|
import org.signal.storageservice.protos.groups.Member;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||||
import org.signal.zkgroup.InvalidInputException;
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
@ -651,6 +653,34 @@ public final class GroupDatabase extends Database {
|
||||||
public boolean isAdmin(@NonNull Recipient recipient) {
|
public boolean isAdmin(@NonNull Recipient recipient) {
|
||||||
return isV2Group() && requireV2GroupProperties().isAdmin(recipient);
|
return isV2Group() && requireV2GroupProperties().isAdmin(recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Who is allowed to add to the membership of this group.
|
||||||
|
*/
|
||||||
|
public GroupAccessControl getMembershipAdditionAccessControl() {
|
||||||
|
if (isV2Group()) {
|
||||||
|
if (requireV2GroupProperties().getDecryptedGroup().getAccessControl().getMembers() == AccessControl.AccessRequired.MEMBER) {
|
||||||
|
return GroupAccessControl.ALL_MEMBERS;
|
||||||
|
}
|
||||||
|
return GroupAccessControl.ONLY_ADMINS;
|
||||||
|
} else {
|
||||||
|
return GroupAccessControl.ALL_MEMBERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Who is allowed to modify the attributes of this group, name/avatar/timer etc.
|
||||||
|
*/
|
||||||
|
public GroupAccessControl getAttributesAccessControl() {
|
||||||
|
if (isV2Group()) {
|
||||||
|
if (requireV2GroupProperties().getDecryptedGroup().getAccessControl().getAttributes() == AccessControl.AccessRequired.MEMBER) {
|
||||||
|
return GroupAccessControl.ALL_MEMBERS;
|
||||||
|
}
|
||||||
|
return GroupAccessControl.ONLY_ADMINS;
|
||||||
|
} else {
|
||||||
|
return GroupAccessControl.ALL_MEMBERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class V2GroupProperties {
|
public static class V2GroupProperties {
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package org.thoughtcrime.securesms.groups;
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
|
public enum GroupAccessControl {
|
||||||
|
ALL_MEMBERS(R.string.GroupManagement_access_level_all_members),
|
||||||
|
ONLY_ADMINS(R.string.GroupManagement_access_level_only_admins);
|
||||||
|
|
||||||
|
private final @StringRes int string;
|
||||||
|
|
||||||
|
GroupAccessControl(@StringRes int string) {
|
||||||
|
this.string = string;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @StringRes int getString() {
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package org.thoughtcrime.securesms.groups;
|
||||||
|
|
||||||
|
public final class GroupChangeFailedException extends Exception {
|
||||||
|
|
||||||
|
GroupChangeFailedException(Throwable throwable) {
|
||||||
|
super(throwable);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package org.thoughtcrime.securesms.groups;
|
||||||
|
|
||||||
|
public final class GroupInsufficientRightsException extends Exception {
|
||||||
|
|
||||||
|
GroupInsufficientRightsException(Throwable throwable) {
|
||||||
|
super(throwable);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import org.signal.zkgroup.groups.UuidCiphertext;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
import org.whispersystems.signalservice.api.util.InvalidNumberException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -58,6 +59,17 @@ public final class GroupManager {
|
||||||
return V1GroupManager.leaveGroup(context, groupId.requireV1());
|
return V1GroupManager.leaveGroup(context, groupId.requireV1());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.Push groupId, int expirationTime)
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException
|
||||||
|
{
|
||||||
|
if (groupId.isV2()) {
|
||||||
|
throw new GroupChangeFailedException(new AssertionError("NYI")); // TODO: GV2 allow timer change
|
||||||
|
} else {
|
||||||
|
V1GroupManager.updateGroupTimer(context, groupId.requireV1(), expirationTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
public static void cancelInvites(@NonNull Context context,
|
public static void cancelInvites(@NonNull Context context,
|
||||||
@NonNull GroupId.V2 groupId,
|
@NonNull GroupId.V2 groupId,
|
||||||
|
@ -67,6 +79,24 @@ public final class GroupManager {
|
||||||
throw new AssertionError("NYI"); // TODO: GV2 allow invite cancellation
|
throw new AssertionError("NYI"); // TODO: GV2 allow invite cancellation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public static void applyMembershipAdditionRightsChange(@NonNull Context context,
|
||||||
|
@NonNull GroupId.V2 groupId,
|
||||||
|
@NonNull GroupAccessControl newRights)
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException
|
||||||
|
{
|
||||||
|
throw new GroupChangeFailedException(new AssertionError("NYI")); // TODO: GV2 allow membership addition rights change
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
public static void applyAttributesRightsChange(@NonNull Context context,
|
||||||
|
@NonNull GroupId.V2 groupId,
|
||||||
|
@NonNull GroupAccessControl newRights)
|
||||||
|
throws GroupChangeFailedException, GroupInsufficientRightsException
|
||||||
|
{
|
||||||
|
throw new GroupChangeFailedException(new AssertionError("NYI")); // TODO: GV2 allow attributes rights change
|
||||||
|
}
|
||||||
|
|
||||||
public static class GroupActionResult {
|
public static class GroupActionResult {
|
||||||
private final Recipient groupRecipient;
|
private final Recipient groupRecipient;
|
||||||
private final long threadId;
|
private final long threadId;
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
package org.thoughtcrime.securesms.groups;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MediatorLiveData;
|
||||||
|
import androidx.lifecycle.Transformations;
|
||||||
|
|
||||||
|
import com.annimon.stream.ComparatorCompat;
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class LiveGroup extends MediatorLiveData<GroupDatabase.GroupRecord> {
|
||||||
|
|
||||||
|
private static final Comparator<GroupMemberEntry.FullMember> LOCAL_FIRST = (m1, m2) -> Boolean.compare(m2.getMember().isLocalNumber(), m1.getMember().isLocalNumber());
|
||||||
|
private static final Comparator<GroupMemberEntry.FullMember> ADMIN_FIRST = (m1, m2) -> Boolean.compare(m2.isAdmin(), m1.isAdmin());
|
||||||
|
private static final Comparator<? super GroupMemberEntry.FullMember> MEMBER_ORDER = ComparatorCompat.chain(LOCAL_FIRST)
|
||||||
|
.thenComparing(ADMIN_FIRST);
|
||||||
|
|
||||||
|
private final GroupDatabase groupDatabase;
|
||||||
|
private final LiveData<Recipient> recipient;
|
||||||
|
|
||||||
|
public LiveGroup(@NonNull GroupId groupId) {
|
||||||
|
Context context = ApplicationDependencies.getApplication();
|
||||||
|
|
||||||
|
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||||
|
|
||||||
|
recipient = Recipient.externalGroup(context, groupId).live().getLiveData();
|
||||||
|
|
||||||
|
addSource(recipient, this::refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refresh(@NonNull Recipient groupRecipient) {
|
||||||
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
|
Optional<GroupDatabase.GroupRecord> group = groupDatabase.getGroup(groupRecipient.getId());
|
||||||
|
if (group.isPresent()) {
|
||||||
|
postValue(group.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<String> getTitle() {
|
||||||
|
return Transformations.map(this, GroupDatabase.GroupRecord::getTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> isSelfAdmin() {
|
||||||
|
return Transformations.map(this, g -> g.isAdmin(Recipient.self()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> getRecipientIsAdmin(@NonNull RecipientId recipientId) {
|
||||||
|
return LiveDataUtil.mapAsync(this, g -> g.isAdmin(Recipient.resolved(recipientId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Integer> getPendingMemberCount() {
|
||||||
|
return Transformations.map(this, g -> g.isV2Group() ? g.requireV2GroupProperties().getDecryptedGroup().getPendingMembersCount() : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<GroupAccessControl> getMembershipAdditionAccessControl() {
|
||||||
|
return Transformations.map(this, GroupDatabase.GroupRecord::getMembershipAdditionAccessControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<GroupAccessControl> getAttributesAccessControl() {
|
||||||
|
return Transformations.map(this, GroupDatabase.GroupRecord::getAttributesAccessControl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<GroupMemberEntry.FullMember>> getFullMembers() {
|
||||||
|
return LiveDataUtil.mapAsync(this,
|
||||||
|
g -> Stream.of(g.getMembers())
|
||||||
|
.map(m -> {
|
||||||
|
Recipient recipient = Recipient.resolved(m);
|
||||||
|
return new GroupMemberEntry.FullMember(recipient, g.isAdmin(recipient));
|
||||||
|
})
|
||||||
|
.sorted(MEMBER_ORDER)
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Integer> getExpireMessages() {
|
||||||
|
return Transformations.map(recipient, Recipient::getExpireMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> selfCanEditGroupAttributes() {
|
||||||
|
return LiveDataUtil.combineLatest(isSelfAdmin(),
|
||||||
|
getAttributesAccessControl(),
|
||||||
|
(admin, rights) -> {
|
||||||
|
switch (rights) {
|
||||||
|
case ALL_MEMBERS:
|
||||||
|
return true;
|
||||||
|
case ONLY_ADMINS:
|
||||||
|
return admin;
|
||||||
|
default:
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<String> getMembershipCountDescription(@NonNull Resources resources) {
|
||||||
|
return LiveDataUtil.combineLatest(getFullMembers(),
|
||||||
|
getPendingMemberCount(),
|
||||||
|
(fullMembers, invitedCount) -> getMembershipDescription(resources, invitedCount, fullMembers.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getMembershipDescription(@NonNull Resources resources, int invitedCount, int fullMemberCount) {
|
||||||
|
return invitedCount > 0 ? resources.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, fullMemberCount,
|
||||||
|
fullMemberCount, invitedCount)
|
||||||
|
: resources.getQuantityString(R.plurals.MessageRequestProfileView_members, fullMemberCount,
|
||||||
|
fullMemberCount);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,12 +15,14 @@ import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
|
||||||
import org.thoughtcrime.securesms.jobs.LeaveGroupJob;
|
import org.thoughtcrime.securesms.jobs.LeaveGroupJob;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.MmsException;
|
import org.thoughtcrime.securesms.mms.MmsException;
|
||||||
|
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
|
||||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
|
@ -175,4 +177,16 @@ final class V1GroupManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
static void updateGroupTimer(@NonNull Context context, @NonNull GroupId.V1 groupId, int expirationTime) {
|
||||||
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||||
|
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||||
|
Recipient recipient = Recipient.externalGroup(context, groupId);
|
||||||
|
long threadId = threadDatabase.getThreadIdFor(recipient);
|
||||||
|
|
||||||
|
recipientDatabase.setExpireMessages(recipient.getId(), expirationTime);
|
||||||
|
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(recipient, System.currentTimeMillis(), expirationTime * 1000L);
|
||||||
|
MessageSender.send(context, outgoingMessage, threadId, false, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,20 +12,11 @@ import java.util.Collection;
|
||||||
|
|
||||||
public abstract class GroupMemberEntry {
|
public abstract class GroupMemberEntry {
|
||||||
|
|
||||||
private final DefaultValueLiveData<Boolean> busy = new DefaultValueLiveData<>(false);
|
private final DefaultValueLiveData<Boolean> busy = new DefaultValueLiveData<>(false);
|
||||||
@Nullable private Runnable onClick;
|
|
||||||
|
|
||||||
private GroupMemberEntry() {
|
private GroupMemberEntry() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnClick(@NonNull Runnable onClick) {
|
|
||||||
this.onClick = onClick;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @Nullable Runnable getOnClick() {
|
|
||||||
return onClick;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<Boolean> getBusy() {
|
public LiveData<Boolean> getBusy() {
|
||||||
return busy;
|
return busy;
|
||||||
}
|
}
|
||||||
|
@ -34,17 +25,52 @@ public abstract class GroupMemberEntry {
|
||||||
this.busy.postValue(busy);
|
this.busy.postValue(busy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract boolean equals(@Nullable Object obj);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract int hashCode();
|
||||||
|
|
||||||
|
abstract boolean sameId(GroupMemberEntry newItem);
|
||||||
|
|
||||||
public final static class FullMember extends GroupMemberEntry {
|
public final static class FullMember extends GroupMemberEntry {
|
||||||
|
|
||||||
private final Recipient member;
|
private final Recipient member;
|
||||||
|
private final boolean isAdmin;
|
||||||
|
|
||||||
public FullMember(@NonNull Recipient member) {
|
public FullMember(@NonNull Recipient member, boolean isAdmin) {
|
||||||
this.member = member;
|
this.member = member;
|
||||||
|
this.isAdmin = isAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Recipient getMember() {
|
public Recipient getMember() {
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAdmin() {
|
||||||
|
return isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean sameId(GroupMemberEntry newItem) {
|
||||||
|
if (getClass() != newItem.getClass()) return false;
|
||||||
|
|
||||||
|
return member.getId().equals(((GroupMemberEntry.FullMember) newItem).member.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (!(obj instanceof FullMember)) return false;
|
||||||
|
|
||||||
|
FullMember other = (FullMember) obj;
|
||||||
|
return other.member.equals(member) &&
|
||||||
|
other.isAdmin == isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return member.hashCode() * 31 + (isAdmin ? 1 : 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final static class PendingMember extends GroupMemberEntry {
|
public final static class PendingMember extends GroupMemberEntry {
|
||||||
|
@ -69,6 +95,32 @@ public abstract class GroupMemberEntry {
|
||||||
public boolean isCancellable() {
|
public boolean isCancellable() {
|
||||||
return cancellable;
|
return cancellable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean sameId(GroupMemberEntry newItem) {
|
||||||
|
if (getClass() != newItem.getClass()) return false;
|
||||||
|
|
||||||
|
return invitee.getId().equals(((GroupMemberEntry.PendingMember) newItem).invitee.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (!(obj instanceof PendingMember)) return false;
|
||||||
|
|
||||||
|
PendingMember other = (PendingMember) obj;
|
||||||
|
return other.invitee.equals(invitee) &&
|
||||||
|
other.inviteeCipherText.equals(inviteeCipherText) &&
|
||||||
|
other.cancellable == cancellable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int hash = invitee.hashCode();
|
||||||
|
hash *= 31;
|
||||||
|
hash += inviteeCipherText.hashCode();
|
||||||
|
hash *= 31;
|
||||||
|
return hash + (cancellable ? 1 : 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final static class UnknownPendingMemberCount extends GroupMemberEntry {
|
public final static class UnknownPendingMemberCount extends GroupMemberEntry {
|
||||||
|
@ -99,5 +151,31 @@ public abstract class GroupMemberEntry {
|
||||||
public boolean isCancellable() {
|
public boolean isCancellable() {
|
||||||
return cancellable;
|
return cancellable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean sameId(GroupMemberEntry newItem) {
|
||||||
|
if (getClass() != newItem.getClass()) return false;
|
||||||
|
|
||||||
|
return inviter.getId().equals(((GroupMemberEntry.UnknownPendingMemberCount) newItem).inviter.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object obj) {
|
||||||
|
if (!(obj instanceof UnknownPendingMemberCount)) return false;
|
||||||
|
|
||||||
|
UnknownPendingMemberCount other = (UnknownPendingMemberCount) obj;
|
||||||
|
return other.inviter.equals(inviter) &&
|
||||||
|
other.ciphertexts.equals(ciphertexts) &&
|
||||||
|
other.cancellable == cancellable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int hash = inviter.hashCode();
|
||||||
|
hash *= 31;
|
||||||
|
hash += ciphertexts.hashCode();
|
||||||
|
hash *= 31;
|
||||||
|
return hash + (cancellable ? 1 : 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||||
|
@ -17,7 +18,7 @@ import org.thoughtcrime.securesms.util.LifecycleRecyclerAdapter;
|
||||||
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
|
import org.thoughtcrime.securesms.util.LifecycleViewHolder;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.List;
|
||||||
|
|
||||||
final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberListAdapter.ViewHolder> {
|
final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberListAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
@ -27,12 +28,19 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
|
|
||||||
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
|
private final ArrayList<GroupMemberEntry> data = new ArrayList<>();
|
||||||
|
|
||||||
@Nullable private AdminActionsListener adminActionsListener;
|
@Nullable private AdminActionsListener adminActionsListener;
|
||||||
|
@Nullable private RecipientClickListener recipientClickListener;
|
||||||
|
|
||||||
void updateData(@NonNull Collection<? extends GroupMemberEntry> recipients) {
|
void updateData(@NonNull List<? extends GroupMemberEntry> recipients) {
|
||||||
data.clear();
|
if (data.isEmpty()) {
|
||||||
data.addAll(recipients);
|
data.addAll(recipients);
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
|
} else {
|
||||||
|
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallback(data, recipients));
|
||||||
|
data.clear();
|
||||||
|
data.addAll(recipients);
|
||||||
|
diffResult.dispatchUpdatesTo(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -41,11 +49,11 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
case FULL_MEMBER:
|
case FULL_MEMBER:
|
||||||
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
|
return new FullMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.group_recipient_list_item,
|
.inflate(R.layout.group_recipient_list_item,
|
||||||
parent, false), adminActionsListener);
|
parent, false), recipientClickListener, adminActionsListener);
|
||||||
case OWN_INVITE_PENDING:
|
case OWN_INVITE_PENDING:
|
||||||
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
|
return new OwnInvitePendingMemberViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.group_recipient_list_item,
|
.inflate(R.layout.group_recipient_list_item,
|
||||||
parent, false), adminActionsListener);
|
parent, false), recipientClickListener, adminActionsListener);
|
||||||
case OTHER_INVITE_PENDING_COUNT:
|
case OTHER_INVITE_PENDING_COUNT:
|
||||||
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
|
return new UnknownPendingMemberCountViewHolder(LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.group_recipient_list_item,
|
.inflate(R.layout.group_recipient_list_item,
|
||||||
|
@ -59,6 +67,10 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
this.adminActionsListener = adminActionsListener;
|
this.adminActionsListener = adminActionsListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setRecipientClickListener(@Nullable RecipientClickListener recipientClickListener) {
|
||||||
|
this.recipientClickListener = recipientClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
holder.bind(data.get(position));
|
holder.bind(data.get(position));
|
||||||
|
@ -86,24 +98,31 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
|
|
||||||
static abstract class ViewHolder extends LifecycleViewHolder {
|
static abstract class ViewHolder extends LifecycleViewHolder {
|
||||||
|
|
||||||
final Context context;
|
final Context context;
|
||||||
private final AvatarImageView avatar;
|
final AvatarImageView avatar;
|
||||||
private final TextView recipient;
|
final TextView recipient;
|
||||||
final PopupMenuView popupMenu;
|
final PopupMenuView popupMenu;
|
||||||
final View popupMenuContainer;
|
final View popupMenuContainer;
|
||||||
final ProgressBar busyProgress;
|
final ProgressBar busyProgress;
|
||||||
@Nullable final AdminActionsListener adminActionsListener;
|
final View admin;
|
||||||
|
@Nullable final RecipientClickListener recipientClickListener;
|
||||||
|
@Nullable final AdminActionsListener adminActionsListener;
|
||||||
|
|
||||||
ViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
ViewHolder(@NonNull View itemView,
|
||||||
|
@Nullable RecipientClickListener recipientClickListener,
|
||||||
|
@Nullable AdminActionsListener adminActionsListener)
|
||||||
|
{
|
||||||
super(itemView);
|
super(itemView);
|
||||||
|
|
||||||
this.context = itemView.getContext();
|
this.context = itemView.getContext();
|
||||||
this.avatar = itemView.findViewById(R.id.recipient_avatar);
|
this.avatar = itemView.findViewById(R.id.recipient_avatar);
|
||||||
this.recipient = itemView.findViewById(R.id.recipient_name);
|
this.recipient = itemView.findViewById(R.id.recipient_name);
|
||||||
this.popupMenu = itemView.findViewById(R.id.popupMenu);
|
this.popupMenu = itemView.findViewById(R.id.popupMenu);
|
||||||
this.popupMenuContainer = itemView.findViewById(R.id.popupMenuProgressContainer);
|
this.popupMenuContainer = itemView.findViewById(R.id.popupMenuProgressContainer);
|
||||||
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
|
this.busyProgress = itemView.findViewById(R.id.menuBusyProgress);
|
||||||
this.adminActionsListener = adminActionsListener;
|
this.admin = itemView.findViewById(R.id.admin);
|
||||||
|
this.recipientClickListener = recipientClickListener;
|
||||||
|
this.adminActionsListener = adminActionsListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
void bindRecipient(@NonNull Recipient recipient) {
|
void bindRecipient(@NonNull Recipient recipient) {
|
||||||
|
@ -117,15 +136,23 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
this.avatar.setRecipient(recipient);
|
this.avatar.setRecipient(recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void bindRecipientClick(@NonNull Recipient recipient) {
|
||||||
|
View.OnClickListener onClickListener = v -> {
|
||||||
|
if (recipientClickListener != null) {
|
||||||
|
recipientClickListener.onClick(recipient);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.avatar.setOnClickListener(onClickListener);
|
||||||
|
this.recipient.setOnClickListener(onClickListener);
|
||||||
|
}
|
||||||
|
|
||||||
void bind(@NonNull GroupMemberEntry memberEntry) {
|
void bind(@NonNull GroupMemberEntry memberEntry) {
|
||||||
busyProgress.setVisibility(View.GONE);
|
busyProgress.setVisibility(View.GONE);
|
||||||
|
admin.setVisibility(View.GONE);
|
||||||
hideMenu();
|
hideMenu();
|
||||||
|
|
||||||
Runnable onClick = memberEntry.getOnClick();
|
avatar.setOnClickListener(null);
|
||||||
View.OnClickListener onClickListener = v -> { if (onClick != null) onClick.run(); };
|
recipient.setOnClickListener(null);
|
||||||
|
|
||||||
avatar.setOnClickListener(onClickListener);
|
|
||||||
recipient.setOnClickListener(onClickListener);
|
|
||||||
|
|
||||||
memberEntry.getBusy().observe(this, busy -> {
|
memberEntry.getBusy().observe(this, busy -> {
|
||||||
busyProgress.setVisibility(busy ? View.VISIBLE : View.GONE);
|
busyProgress.setVisibility(busy ? View.VISIBLE : View.GONE);
|
||||||
|
@ -146,8 +173,11 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
|
|
||||||
final static class FullMemberViewHolder extends ViewHolder {
|
final static class FullMemberViewHolder extends ViewHolder {
|
||||||
|
|
||||||
FullMemberViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
FullMemberViewHolder(@NonNull View itemView,
|
||||||
super(itemView, adminActionsListener);
|
@Nullable RecipientClickListener recipientClickListener,
|
||||||
|
@Nullable AdminActionsListener adminActionsListener)
|
||||||
|
{
|
||||||
|
super(itemView, recipientClickListener, adminActionsListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -157,13 +187,18 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
GroupMemberEntry.FullMember fullMember = (GroupMemberEntry.FullMember) memberEntry;
|
GroupMemberEntry.FullMember fullMember = (GroupMemberEntry.FullMember) memberEntry;
|
||||||
|
|
||||||
bindRecipient(fullMember.getMember());
|
bindRecipient(fullMember.getMember());
|
||||||
|
bindRecipientClick(fullMember.getMember());
|
||||||
|
admin.setVisibility(fullMember.isAdmin() ? View.VISIBLE : View.INVISIBLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final static class OwnInvitePendingMemberViewHolder extends ViewHolder {
|
final static class OwnInvitePendingMemberViewHolder extends ViewHolder {
|
||||||
|
|
||||||
OwnInvitePendingMemberViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
OwnInvitePendingMemberViewHolder(@NonNull View itemView,
|
||||||
super(itemView, adminActionsListener);
|
@Nullable RecipientClickListener recipientClickListener,
|
||||||
|
@Nullable AdminActionsListener adminActionsListener)
|
||||||
|
{
|
||||||
|
super(itemView, recipientClickListener, adminActionsListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -173,6 +208,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
GroupMemberEntry.PendingMember pendingMember = (GroupMemberEntry.PendingMember) memberEntry;
|
GroupMemberEntry.PendingMember pendingMember = (GroupMemberEntry.PendingMember) memberEntry;
|
||||||
|
|
||||||
bindRecipient(pendingMember.getInvitee());
|
bindRecipient(pendingMember.getInvitee());
|
||||||
|
bindRecipientClick(pendingMember.getInvitee());
|
||||||
|
|
||||||
if (pendingMember.isCancellable() && adminActionsListener != null) {
|
if (pendingMember.isCancellable() && adminActionsListener != null) {
|
||||||
popupMenu.setMenu(R.menu.own_invite_pending_menu,
|
popupMenu.setMenu(R.menu.own_invite_pending_menu,
|
||||||
|
@ -191,7 +227,7 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
|
final static class UnknownPendingMemberCountViewHolder extends ViewHolder {
|
||||||
|
|
||||||
UnknownPendingMemberCountViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
UnknownPendingMemberCountViewHolder(@NonNull View itemView, @Nullable AdminActionsListener adminActionsListener) {
|
||||||
super(itemView, adminActionsListener);
|
super(itemView, null, adminActionsListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -228,4 +264,40 @@ final class GroupMemberListAdapter extends LifecycleRecyclerAdapter<GroupMemberL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final static class DiffCallback extends DiffUtil.Callback {
|
||||||
|
private final List<? extends GroupMemberEntry> oldData;
|
||||||
|
private final List<? extends GroupMemberEntry> newData;
|
||||||
|
|
||||||
|
DiffCallback(List<? extends GroupMemberEntry> oldData, List<? extends GroupMemberEntry> newData) {
|
||||||
|
this.oldData = oldData;
|
||||||
|
this.newData = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOldListSize() {
|
||||||
|
return oldData.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getNewListSize() {
|
||||||
|
return newData.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||||
|
GroupMemberEntry oldItem = oldData.get(oldItemPosition);
|
||||||
|
GroupMemberEntry newItem = newData.get(newItemPosition);
|
||||||
|
|
||||||
|
return oldItem.sameId(newItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||||
|
GroupMemberEntry oldItem = oldData.get(oldItemPosition);
|
||||||
|
GroupMemberEntry newItem = newData.get(newItemPosition);
|
||||||
|
|
||||||
|
return oldItem.equals(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.List;
|
||||||
|
|
||||||
public final class GroupMemberListView extends RecyclerView {
|
public final class GroupMemberListView extends RecyclerView {
|
||||||
|
|
||||||
|
@ -52,7 +52,11 @@ public final class GroupMemberListView extends RecyclerView {
|
||||||
membersAdapter.setAdminActionsListener(adminActionsListener);
|
membersAdapter.setAdminActionsListener(adminActionsListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMembers(@NonNull Collection<? extends GroupMemberEntry> recipients) {
|
public void setRecipientClickListener(@Nullable RecipientClickListener listener) {
|
||||||
|
membersAdapter.setRecipientClickListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMembers(@NonNull List<? extends GroupMemberEntry> recipients) {
|
||||||
membersAdapter.updateData(recipients);
|
membersAdapter.updateData(recipients);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.ui;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
|
||||||
|
public interface RecipientClickListener {
|
||||||
|
void onClick(@NonNull Recipient recipient);
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
|
|
||||||
|
public class ManageGroupActivity extends PassphraseRequiredActionBarActivity {
|
||||||
|
|
||||||
|
private static final String GROUP_ID = "GROUP_ID";
|
||||||
|
|
||||||
|
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||||
|
|
||||||
|
public static Intent newIntent(@NonNull Context context, @NonNull GroupId groupId) {
|
||||||
|
Intent intent = new Intent(context, ManageGroupActivity.class);
|
||||||
|
intent.putExtra(GROUP_ID, groupId.toString());
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreCreate() {
|
||||||
|
dynamicTheme.onCreate(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||||
|
super.onCreate(savedInstanceState, ready);
|
||||||
|
setContentView(R.layout.group_manage_activity);
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
getSupportFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.container, ManageGroupFragment.newInstance(getIntent().getStringExtra(GROUP_ID)))
|
||||||
|
.commitNow();
|
||||||
|
}
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
dynamicTheme.onResume(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSupportNavigateUp() {
|
||||||
|
onBackPressed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||||
|
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupRightsDialog;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
|
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class ManageGroupFragment extends Fragment {
|
||||||
|
private static final String GROUP_ID = "GROUP_ID";
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(ManageGroupFragment.class);
|
||||||
|
|
||||||
|
private static final int RETURN_FROM_MEDIA = 33114;
|
||||||
|
|
||||||
|
private ManageGroupViewModel viewModel;
|
||||||
|
private GroupMemberListView groupMemberList;
|
||||||
|
private View listPending;
|
||||||
|
private TextView groupTitle;
|
||||||
|
private TextView memberCount;
|
||||||
|
private AvatarImageView avatar;
|
||||||
|
private ThreadPhotoRailView threadPhotoRailView;
|
||||||
|
private View groupMediaCard;
|
||||||
|
private View accessControlCard;
|
||||||
|
private View pendingMembersCard;
|
||||||
|
private ManageGroupViewModel.CursorFactory cursorFactory;
|
||||||
|
private View photoRailLabel;
|
||||||
|
private Button editGroupAccessValue;
|
||||||
|
private Button editGroupMembershipValue;
|
||||||
|
private Button disappearingMessages;
|
||||||
|
private Button blockGroup;
|
||||||
|
private Button leaveGroup;
|
||||||
|
|
||||||
|
static ManageGroupFragment newInstance(@NonNull String groupId) {
|
||||||
|
ManageGroupFragment fragment = new ManageGroupFragment();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
|
||||||
|
args.putString(GROUP_ID, groupId);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
@Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
View view = inflater.inflate(R.layout.group_manage_fragment, container, false);
|
||||||
|
|
||||||
|
avatar = view.findViewById(R.id.group_avatar);
|
||||||
|
groupTitle = view.findViewById(R.id.group_title);
|
||||||
|
memberCount = view.findViewById(R.id.member_count);
|
||||||
|
groupMemberList = view.findViewById(R.id.group_members);
|
||||||
|
listPending = view.findViewById(R.id.listPending);
|
||||||
|
threadPhotoRailView = view.findViewById(R.id.recent_photos);
|
||||||
|
groupMediaCard = view.findViewById(R.id.group_media_card);
|
||||||
|
accessControlCard = view.findViewById(R.id.group_access_control_card);
|
||||||
|
pendingMembersCard = view.findViewById(R.id.group_pending_card);
|
||||||
|
photoRailLabel = view.findViewById(R.id.rail_label);
|
||||||
|
editGroupAccessValue = view.findViewById(R.id.edit_group_access_value);
|
||||||
|
editGroupMembershipValue = view.findViewById(R.id.edit_group_membership_value);
|
||||||
|
disappearingMessages = view.findViewById(R.id.disappearing_messages);
|
||||||
|
blockGroup = view.findViewById(R.id.blockGroup);
|
||||||
|
leaveGroup = view.findViewById(R.id.leaveGroup);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
|
Context context = requireContext();
|
||||||
|
GroupId.Push groupId = GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(GROUP_ID))).requirePush();
|
||||||
|
ManageGroupViewModel.Factory factory = new ManageGroupViewModel.Factory(context, groupId);
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(requireActivity(), factory).get(ManageGroupViewModel.class);
|
||||||
|
|
||||||
|
viewModel.getMembers().observe(getViewLifecycleOwner(), members -> groupMemberList.setMembers(members));
|
||||||
|
|
||||||
|
viewModel.getPendingMemberCount().observe(getViewLifecycleOwner(),
|
||||||
|
members -> {
|
||||||
|
if (members > 0) {
|
||||||
|
listPending.setEnabled(true);
|
||||||
|
listPending.setOnClickListener(v -> {
|
||||||
|
FragmentActivity activity = requireActivity();
|
||||||
|
activity.startActivity(PendingMemberInvitesActivity.newIntent(activity, groupId.requireV2()));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
listPending.setEnabled(false);
|
||||||
|
listPending.setOnClickListener(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
viewModel.getTitle().observe(getViewLifecycleOwner(), groupTitle::setText);
|
||||||
|
viewModel.getMemberCountSummary().observe(getViewLifecycleOwner(), memberCount::setText);
|
||||||
|
|
||||||
|
viewModel.getGroupViewState().observe(getViewLifecycleOwner(), vs -> {
|
||||||
|
if (vs == null) return;
|
||||||
|
photoRailLabel.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(context, vs.getThreadId())));
|
||||||
|
avatar.setRecipient(vs.getGroupRecipient());
|
||||||
|
|
||||||
|
setMediaCursorFactory(vs.getMediaCursorFactory());
|
||||||
|
|
||||||
|
threadPhotoRailView.setListener(mediaRecord ->
|
||||||
|
startActivityForResult(MediaPreviewActivity.intentFromMediaRecord(context,
|
||||||
|
mediaRecord,
|
||||||
|
ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR),
|
||||||
|
RETURN_FROM_MEDIA));
|
||||||
|
|
||||||
|
accessControlCard.setVisibility(vs.getGroupRecipient().requireGroupId().isV2() ? View.VISIBLE : View.GONE);
|
||||||
|
pendingMembersCard.setVisibility(vs.getGroupRecipient().requireGroupId().isV2() ? View.VISIBLE : View.GONE);
|
||||||
|
});
|
||||||
|
|
||||||
|
leaveGroup.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
|
||||||
|
leaveGroup.setOnClickListener(v -> LeaveGroupDialog.handleLeavePushGroup(context,
|
||||||
|
getLifecycle(),
|
||||||
|
groupId.requirePush(),
|
||||||
|
null));
|
||||||
|
|
||||||
|
viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
|
||||||
|
|
||||||
|
disappearingMessages.setOnClickListener(v -> viewModel.handleExpirationSelection());
|
||||||
|
blockGroup.setOnClickListener(v -> viewModel.blockAndLeave(requireActivity()));
|
||||||
|
|
||||||
|
viewModel.getMembershipRights().observe(getViewLifecycleOwner(), r -> {
|
||||||
|
if (r != null) {
|
||||||
|
editGroupMembershipValue.setText(r.getString());
|
||||||
|
editGroupMembershipValue.setOnClickListener(v -> new GroupRightsDialog(context, GroupRightsDialog.Type.MEMBERSHIP, r, (from, to) -> viewModel.applyMembershipRightsChange(to)).show());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
viewModel.getEditGroupAttributesRights().observe(getViewLifecycleOwner(), r -> {
|
||||||
|
if (r != null) {
|
||||||
|
editGroupAccessValue.setText(r.getString());
|
||||||
|
editGroupAccessValue.setOnClickListener(v -> new GroupRightsDialog(context, GroupRightsDialog.Type.ATTRIBUTES, r, (from, to) -> viewModel.applyAttributesRightsChange(to)).show());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
viewModel.getIsAdmin().observe(getViewLifecycleOwner(), admin -> {
|
||||||
|
editGroupMembershipValue.setEnabled(admin);
|
||||||
|
editGroupAccessValue.setEnabled(admin);
|
||||||
|
});
|
||||||
|
|
||||||
|
viewModel.getCanEditGroupAttributes().observe(getViewLifecycleOwner(), canEdit -> disappearingMessages.setEnabled(canEdit));
|
||||||
|
|
||||||
|
groupMemberList.setRecipientClickListener(recipient -> RecipientBottomSheetDialogFragment.create(recipient.getId(), groupId).show(requireFragmentManager(), "BOTTOM"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setMediaCursorFactory(@Nullable ManageGroupViewModel.CursorFactory cursorFactory) {
|
||||||
|
if (this.cursorFactory != cursorFactory) {
|
||||||
|
this.cursorFactory = cursorFactory;
|
||||||
|
applyMediaCursorFactory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyMediaCursorFactory() {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context == null) return;
|
||||||
|
if (this.cursorFactory != null) {
|
||||||
|
Cursor cursor = this.cursorFactory.create();
|
||||||
|
threadPhotoRailView.setCursor(GlideApp.with(context), cursor);
|
||||||
|
groupMediaCard.setVisibility(cursor.getCount() > 0 ? View.VISIBLE : View.GONE);
|
||||||
|
} else {
|
||||||
|
threadPhotoRailView.setCursor(GlideApp.with(context), null);
|
||||||
|
groupMediaCard.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (requestCode == RETURN_FROM_MEDIA) {
|
||||||
|
applyMediaCursorFactory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
import androidx.core.util.Consumer;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
final class ManageGroupRepository {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(ManageGroupRepository.class);
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final GroupId groupId;
|
||||||
|
private final ExecutorService executor;
|
||||||
|
|
||||||
|
ManageGroupRepository(@NonNull Context context, @NonNull GroupId groupId) {
|
||||||
|
this.context = context;
|
||||||
|
this.executor = SignalExecutors.BOUNDED;
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GroupId getGroupId() {
|
||||||
|
return groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void getGroupState(@NonNull Consumer<GroupStateResult> onGroupStateLoaded) {
|
||||||
|
executor.execute(() -> onGroupStateLoaded.accept(getGroupState()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private GroupStateResult getGroupState() {
|
||||||
|
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||||
|
Recipient groupRecipient = Recipient.externalGroup(context, groupId);
|
||||||
|
long threadId = threadDatabase.getThreadIdFor(groupRecipient);
|
||||||
|
|
||||||
|
return new GroupStateResult(threadId, groupRecipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setExpiration(int newExpirationTime, @NonNull Error error) {
|
||||||
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
|
try {
|
||||||
|
GroupManager.updateGroupTimer(context, groupId.requirePush(), newExpirationTime);
|
||||||
|
} catch (GroupInsufficientRightsException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
error.onError(FailureReason.NO_RIGHTS);
|
||||||
|
} catch (GroupChangeFailedException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
error.onError(FailureReason.OTHER);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights, @NonNull Error error) {
|
||||||
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
|
try {
|
||||||
|
GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights);
|
||||||
|
} catch (GroupInsufficientRightsException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
error.onError(FailureReason.NO_RIGHTS);
|
||||||
|
} catch (GroupChangeFailedException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
error.onError(FailureReason.OTHER);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyAttributesRightsChange(@NonNull GroupAccessControl newRights, @NonNull Error error) {
|
||||||
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
|
try {
|
||||||
|
GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights);
|
||||||
|
} catch (GroupInsufficientRightsException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
error.onError(FailureReason.NO_RIGHTS);
|
||||||
|
} catch (GroupChangeFailedException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
error.onError(FailureReason.OTHER);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void getRecipient(@NonNull Consumer<Recipient> recipientCallback) {
|
||||||
|
SimpleTask.run(SignalExecutors.BOUNDED,
|
||||||
|
() -> Recipient.externalGroup(context, groupId),
|
||||||
|
recipientCallback::accept);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class GroupStateResult {
|
||||||
|
|
||||||
|
private final long threadId;
|
||||||
|
private final Recipient recipient;
|
||||||
|
|
||||||
|
private GroupStateResult(long threadId,
|
||||||
|
Recipient recipient)
|
||||||
|
{
|
||||||
|
this.threadId = threadId;
|
||||||
|
this.recipient = recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getThreadId() {
|
||||||
|
return threadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Recipient getRecipient() {
|
||||||
|
return recipient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FailureReason {
|
||||||
|
NO_RIGHTS(R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this),
|
||||||
|
OTHER(R.string.ManageGroupActivity_failed_to_update_the_group);
|
||||||
|
|
||||||
|
private final @StringRes int toastMessage;
|
||||||
|
|
||||||
|
FailureReason(@StringRes int toastMessage) {
|
||||||
|
this.toastMessage = toastMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @StringRes int getToastMessage() {
|
||||||
|
return toastMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Error {
|
||||||
|
void onError(@NonNull FailureReason failureReason);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.ui.managegroup;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.Transformations;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||||
|
import org.thoughtcrime.securesms.ExpirationDialog;
|
||||||
|
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||||
|
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||||
|
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ManageGroupViewModel extends ViewModel {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final ManageGroupRepository manageGroupRepository;
|
||||||
|
private final LiveData<String> title;
|
||||||
|
private final LiveData<Boolean> isAdmin;
|
||||||
|
private final LiveData<Boolean> canEditGroupAttributes;
|
||||||
|
private final LiveData<List<GroupMemberEntry.FullMember>> members;
|
||||||
|
private final LiveData<Integer> pendingMemberCount;
|
||||||
|
private final LiveData<String> disappearingMessageTimer;
|
||||||
|
private final LiveData<String> memberCountSummary;
|
||||||
|
private final LiveData<GroupAccessControl> editMembershipRights;
|
||||||
|
private final LiveData<GroupAccessControl> editGroupAttributesRights;
|
||||||
|
private final MutableLiveData<GroupViewState> groupViewState = new MutableLiveData<>(null);
|
||||||
|
|
||||||
|
private ManageGroupViewModel(@NonNull Context context, @NonNull ManageGroupRepository manageGroupRepository) {
|
||||||
|
this.context = context;
|
||||||
|
this.manageGroupRepository = manageGroupRepository;
|
||||||
|
|
||||||
|
manageGroupRepository.getGroupState(this::groupStateLoaded);
|
||||||
|
|
||||||
|
LiveGroup liveGroup = new LiveGroup(manageGroupRepository.getGroupId());
|
||||||
|
|
||||||
|
this.title = liveGroup.getTitle();
|
||||||
|
this.isAdmin = liveGroup.isSelfAdmin();
|
||||||
|
this.members = liveGroup.getFullMembers();
|
||||||
|
this.pendingMemberCount = liveGroup.getPendingMemberCount();
|
||||||
|
this.memberCountSummary = liveGroup.getMembershipCountDescription(context.getResources());
|
||||||
|
this.editMembershipRights = liveGroup.getMembershipAdditionAccessControl();
|
||||||
|
this.editGroupAttributesRights = liveGroup.getAttributesAccessControl();
|
||||||
|
this.disappearingMessageTimer = Transformations.map(liveGroup.getExpireMessages(), expiration -> ExpirationUtil.getExpirationDisplayValue(context, expiration));
|
||||||
|
this.canEditGroupAttributes = liveGroup.selfCanEditGroupAttributes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private void groupStateLoaded(@NonNull ManageGroupRepository.GroupStateResult groupStateResult) {
|
||||||
|
groupViewState.postValue(new GroupViewState(groupStateResult.getThreadId(),
|
||||||
|
groupStateResult.getRecipient(),
|
||||||
|
() -> new ThreadMediaLoader(context, groupStateResult.getThreadId(), MediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest).getCursor()));
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<List<GroupMemberEntry.FullMember>> getMembers() {
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<Integer> getPendingMemberCount() {
|
||||||
|
return pendingMemberCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<String> getMemberCountSummary() {
|
||||||
|
return memberCountSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<GroupViewState> getGroupViewState() {
|
||||||
|
return groupViewState;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<String> getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<GroupAccessControl> getMembershipRights() {
|
||||||
|
return editMembershipRights;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<GroupAccessControl> getEditGroupAttributesRights() {
|
||||||
|
return editGroupAttributesRights;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<Boolean> getIsAdmin() {
|
||||||
|
return isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<Boolean> getCanEditGroupAttributes() {
|
||||||
|
return canEditGroupAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiveData<String> getDisappearingMessageTimer() {
|
||||||
|
return disappearingMessageTimer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleExpirationSelection() {
|
||||||
|
manageGroupRepository.getRecipient(groupRecipient ->
|
||||||
|
ExpirationDialog.show(context,
|
||||||
|
groupRecipient.getExpireMessages(),
|
||||||
|
expirationTime -> manageGroupRepository.setExpiration(expirationTime, this::showErrorToast)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights) {
|
||||||
|
manageGroupRepository.applyMembershipRightsChange(newRights, this::showErrorToast);
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyAttributesRightsChange(@NonNull GroupAccessControl newRights) {
|
||||||
|
manageGroupRepository.applyAttributesRightsChange(newRights, this::showErrorToast);
|
||||||
|
}
|
||||||
|
|
||||||
|
void blockAndLeave(@NonNull FragmentActivity activity) {
|
||||||
|
manageGroupRepository.getRecipient(recipient -> BlockUnblockDialog.showBlockFor(activity, activity.getLifecycle(), recipient,
|
||||||
|
() -> RecipientUtil.block(context, recipient)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private void showErrorToast(@NonNull ManageGroupRepository.FailureReason e) {
|
||||||
|
Util.runOnMain(() -> Toast.makeText(context, e.getToastMessage(), Toast.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class GroupViewState {
|
||||||
|
private final long threadId;
|
||||||
|
@NonNull private final Recipient groupRecipient;
|
||||||
|
@NonNull private final CursorFactory mediaCursorFactory;
|
||||||
|
|
||||||
|
private GroupViewState(long threadId,
|
||||||
|
@NonNull Recipient groupRecipient,
|
||||||
|
@NonNull CursorFactory mediaCursorFactory)
|
||||||
|
{
|
||||||
|
this.threadId = threadId;
|
||||||
|
this.groupRecipient = groupRecipient;
|
||||||
|
this.mediaCursorFactory = mediaCursorFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getThreadId() {
|
||||||
|
return threadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Recipient getGroupRecipient() {
|
||||||
|
return groupRecipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull CursorFactory getMediaCursorFactory() {
|
||||||
|
return mediaCursorFactory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CursorFactory {
|
||||||
|
Cursor create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Factory implements ViewModelProvider.Factory {
|
||||||
|
private final Context context;
|
||||||
|
private final GroupId.Push groupId;
|
||||||
|
|
||||||
|
public Factory(@NonNull Context context, @NonNull GroupId.Push groupId) {
|
||||||
|
this.context = context;
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return (T) new ManageGroupViewModel(context, new ManageGroupRepository(context.getApplicationContext(), groupId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package org.thoughtcrime.securesms.groups.ui.managegroup.dialogs;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.ArrayRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||||
|
|
||||||
|
public final class GroupRightsDialog {
|
||||||
|
|
||||||
|
private final AlertDialog.Builder builder;
|
||||||
|
|
||||||
|
@NonNull private GroupAccessControl rights;
|
||||||
|
|
||||||
|
public GroupRightsDialog(@NonNull Context context,
|
||||||
|
@NonNull Type type,
|
||||||
|
@NonNull GroupAccessControl currentRights,
|
||||||
|
@NonNull GroupRightsDialog.OnChange onChange)
|
||||||
|
{
|
||||||
|
rights = currentRights;
|
||||||
|
|
||||||
|
builder = new AlertDialog.Builder(context)
|
||||||
|
.setTitle(type.message)
|
||||||
|
.setSingleChoiceItems(type.choices, currentRights.ordinal(), (dialog, which) -> rights = GroupAccessControl.values()[which])
|
||||||
|
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||||
|
})
|
||||||
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||||
|
GroupAccessControl newGroupAccessControl = rights;
|
||||||
|
|
||||||
|
if (newGroupAccessControl != currentRights) {
|
||||||
|
onChange.changed(currentRights, newGroupAccessControl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show() {
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnChange {
|
||||||
|
void changed(@NonNull GroupAccessControl from, @NonNull GroupAccessControl to);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Type {
|
||||||
|
MEMBERSHIP(R.string.GroupManagement_choose_who_can_add_or_invite_new_members,
|
||||||
|
R.array.GroupManagement_edit_group_membership_choices),
|
||||||
|
|
||||||
|
ATTRIBUTES(R.string.GroupManagement_choose_who_can_change_the_group_name_and_photo,
|
||||||
|
R.array.GroupManagement_edit_group_info_choices);
|
||||||
|
|
||||||
|
@StringRes private final int message;
|
||||||
|
@ArrayRes private final int choices;
|
||||||
|
|
||||||
|
Type(@StringRes int message, @ArrayRes int choices) {
|
||||||
|
this.message = message;
|
||||||
|
this.choices = choices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
|
||||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
@ -16,44 +15,29 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
|
||||||
final class RecipientDialogRepository {
|
final class RecipientDialogRepository {
|
||||||
|
|
||||||
@NonNull private final GroupDatabase groupDatabase;
|
@NonNull private final Context context;
|
||||||
@NonNull private final Context context;
|
@NonNull private final RecipientId recipientId;
|
||||||
@NonNull private final RecipientId recipientId;
|
@Nullable private final GroupId groupId;
|
||||||
@Nullable private final GroupId groupId;
|
|
||||||
|
|
||||||
RecipientDialogRepository(@NonNull Context context,
|
RecipientDialogRepository(@NonNull Context context,
|
||||||
@NonNull RecipientId recipientId,
|
@NonNull RecipientId recipientId,
|
||||||
@Nullable GroupId groupId)
|
@Nullable GroupId groupId)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
this.recipientId = recipientId;
|
||||||
this.recipientId = recipientId;
|
this.groupId = groupId;
|
||||||
this.groupId = groupId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull RecipientId getRecipientId() {
|
@NonNull
|
||||||
|
RecipientId getRecipientId() {
|
||||||
return recipientId;
|
return recipientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable GroupId getGroupId() {
|
@Nullable
|
||||||
|
GroupId getGroupId() {
|
||||||
return groupId;
|
return groupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
void isAdminOfGroup(@NonNull RecipientId recipientId, @NonNull AdminCallback callback) {
|
|
||||||
SimpleTask.run(SignalExecutors.BOUNDED,
|
|
||||||
() -> {
|
|
||||||
if (groupId != null) {
|
|
||||||
Recipient recipient = Recipient.resolved(recipientId);
|
|
||||||
return groupDatabase.getGroup(groupId)
|
|
||||||
.transform(g -> g.isAdmin(recipient))
|
|
||||||
.or(false);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
callback::isAdmin);
|
|
||||||
}
|
|
||||||
|
|
||||||
void getIdentity(@NonNull IdentityCallback callback) {
|
void getIdentity(@NonNull IdentityCallback callback) {
|
||||||
SimpleTask.run(SignalExecutors.BOUNDED,
|
SimpleTask.run(SignalExecutors.BOUNDED,
|
||||||
() -> DatabaseFactory.getIdentityDatabase(context)
|
() -> DatabaseFactory.getIdentityDatabase(context)
|
||||||
|
@ -62,16 +46,12 @@ final class RecipientDialogRepository {
|
||||||
callback::remoteIdentity);
|
callback::remoteIdentity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getRecipient(@NonNull RecipientCallback recipientCallback) {
|
void getRecipient(@NonNull RecipientCallback recipientCallback) {
|
||||||
SimpleTask.run(SignalExecutors.BOUNDED,
|
SimpleTask.run(SignalExecutors.BOUNDED,
|
||||||
() -> Recipient.resolved(recipientId),
|
() -> Recipient.resolved(recipientId),
|
||||||
recipientCallback::onRecipient);
|
recipientCallback::onRecipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdminCallback {
|
|
||||||
void isAdmin(boolean admin);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IdentityCallback {
|
interface IdentityCallback {
|
||||||
void remoteIdentity(@Nullable IdentityDatabase.IdentityRecord identityRecord);
|
void remoteIdentity(@Nullable IdentityDatabase.IdentityRecord identityRecord);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.Transformations;
|
|
||||||
import androidx.lifecycle.ViewModel;
|
import androidx.lifecycle.ViewModel;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
@ -17,12 +16,12 @@ import org.thoughtcrime.securesms.RecipientPreferenceActivity;
|
||||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||||
import org.thoughtcrime.securesms.groups.GroupId;
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
|
|
||||||
|
|
||||||
final class RecipientDialogViewModel extends ViewModel {
|
final class RecipientDialogViewModel extends ViewModel {
|
||||||
|
|
||||||
|
@ -39,24 +38,23 @@ final class RecipientDialogViewModel extends ViewModel {
|
||||||
this.recipientDialogRepository = recipientDialogRepository;
|
this.recipientDialogRepository = recipientDialogRepository;
|
||||||
this.identity = new MutableLiveData<>();
|
this.identity = new MutableLiveData<>();
|
||||||
|
|
||||||
MutableLiveData<Boolean> localIsAdmin = new DefaultValueLiveData<>(false);
|
boolean recipientIsSelf = recipientDialogRepository.getRecipientId().equals(Recipient.self().getId());
|
||||||
MutableLiveData<Boolean> recipientIsAdmin = new DefaultValueLiveData<>(false);
|
|
||||||
|
|
||||||
if (recipientDialogRepository.getGroupId() != null && recipientDialogRepository.getGroupId().isV2()) {
|
if (recipientDialogRepository.getGroupId() != null && recipientDialogRepository.getGroupId().isV2() && !recipientIsSelf) {
|
||||||
recipientDialogRepository.isAdminOfGroup(Recipient.self().getId(), localIsAdmin::setValue);
|
LiveGroup source = new LiveGroup(recipientDialogRepository.getGroupId());
|
||||||
recipientDialogRepository.isAdminOfGroup(recipientDialogRepository.getRecipientId(), recipientIsAdmin::setValue);
|
|
||||||
|
LiveData<Boolean> localIsAdmin = source.isSelfAdmin();
|
||||||
|
LiveData<Boolean> recipientIsAdmin = source.getRecipientIsAdmin(recipientDialogRepository.getRecipientId());
|
||||||
|
|
||||||
|
adminActionStatus = LiveDataUtil.combineLatest(localIsAdmin, recipientIsAdmin,
|
||||||
|
(localAdmin, recipientAdmin) ->
|
||||||
|
new AdminActionStatus(localAdmin,
|
||||||
|
localAdmin && !recipientAdmin,
|
||||||
|
localAdmin && recipientAdmin));
|
||||||
|
} else {
|
||||||
|
adminActionStatus = new MutableLiveData<>(new AdminActionStatus(false, false, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
adminActionStatus = Transformations.map(new LiveDataPair<>(localIsAdmin, recipientIsAdmin, false, false),
|
|
||||||
pair -> {
|
|
||||||
boolean localAdmin = pair.first();
|
|
||||||
boolean recipientAdmin = pair.second();
|
|
||||||
|
|
||||||
return new AdminActionStatus(localAdmin,
|
|
||||||
localAdmin && !recipientAdmin,
|
|
||||||
localAdmin && recipientAdmin);
|
|
||||||
});
|
|
||||||
|
|
||||||
recipient = Recipient.live(recipientDialogRepository.getRecipientId()).getLiveData();
|
recipient = Recipient.live(recipientDialogRepository.getRecipientId()).getLiveData();
|
||||||
|
|
||||||
recipientDialogRepository.getIdentity(identity::setValue);
|
recipientDialogRepository.getIdentity(identity::setValue);
|
||||||
|
|
|
@ -4,11 +4,43 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MediatorLiveData;
|
import androidx.lifecycle.MediatorLiveData;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Function;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
public final class LiveDataUtil {
|
public final class LiveDataUtil {
|
||||||
|
|
||||||
private LiveDataUtil() {
|
private LiveDataUtil() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the {@param backgroundFunction} on {@link SignalExecutors#BOUNDED}.
|
||||||
|
* <p>
|
||||||
|
* The background function order is run serially, albeit possibly across multiple threads.
|
||||||
|
* <p>
|
||||||
|
* The background function may not run for all {@param source} updates. Later updates taking priority.
|
||||||
|
*/
|
||||||
|
public static <A, B> LiveData<B> mapAsync(@NonNull LiveData<A> source, @NonNull Function<A, B> backgroundFunction) {
|
||||||
|
return mapAsync(SignalExecutors.BOUNDED, source, backgroundFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the {@param backgroundFunction} on the supplied {@param executor}.
|
||||||
|
* <p>
|
||||||
|
* Regardless of the executor supplied, the background function is run serially.
|
||||||
|
* <p>
|
||||||
|
* The background function may not run for all {@param source} updates. Later updates taking priority.
|
||||||
|
*/
|
||||||
|
public static <A, B> LiveData<B> mapAsync(@NonNull Executor executor, @NonNull LiveData<A> source, @NonNull Function<A, B> backgroundFunction) {
|
||||||
|
MediatorLiveData<B> outputLiveData = new MediatorLiveData<>();
|
||||||
|
Executor liveDataExecutor = new SerialLiveDataExecutor(executor);
|
||||||
|
|
||||||
|
outputLiveData.addSource(source, currentValue -> liveDataExecutor.execute(() -> outputLiveData.postValue(backgroundFunction.apply(currentValue))));
|
||||||
|
|
||||||
|
return outputLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Once there is non-null data on both input {@link LiveData}, the {@link Combine} function is run
|
* Once there is non-null data on both input {@link LiveData}, the {@link Combine} function is run
|
||||||
* and produces a live data of the combined data.
|
* and produces a live data of the combined data.
|
||||||
|
@ -64,4 +96,42 @@ public final class LiveDataUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executor decorator that runs serially but enqueues just the latest task, dropping any pending task.
|
||||||
|
* <p>
|
||||||
|
* Based on SerialExecutor https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Executor.html
|
||||||
|
* but modified to represent a queue of size one which is replaced by the latest call to {@link #execute(Runnable)}.
|
||||||
|
*/
|
||||||
|
private static final class SerialLiveDataExecutor implements Executor {
|
||||||
|
private final Executor executor;
|
||||||
|
private Runnable next;
|
||||||
|
private Runnable active;
|
||||||
|
|
||||||
|
SerialLiveDataExecutor(@NonNull Executor executor) {
|
||||||
|
this.executor = executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void execute(@NonNull Runnable command) {
|
||||||
|
next = () -> {
|
||||||
|
try {
|
||||||
|
command.run();
|
||||||
|
} finally {
|
||||||
|
scheduleNext();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (active == null) {
|
||||||
|
scheduleNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void scheduleNext() {
|
||||||
|
active = next;
|
||||||
|
next = null;
|
||||||
|
if (active != null) {
|
||||||
|
executor.execute(active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
5
app/src/main/res/color/ultramarine_text_button.xml
Normal file
5
app/src/main/res/color/ultramarine_text_button.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="@color/core_ultramarine" android:state_enabled="true" />
|
||||||
|
<item android:color="?attr/title_text_color_secondary" />
|
||||||
|
</selector>
|
24
app/src/main/res/layout/group_manage_activity.xml
Normal file
24
app/src/main/res/layout/group_manage_activity.xml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context=".groups.ui.managegroup.ManageGroupActivity">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||||
|
app:title=" "
|
||||||
|
app:titleTextColor="?attr/title_text_color_primary" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
248
app/src/main/res/layout/group_manage_fragment.xml
Normal file
248
app/src/main/res/layout/group_manage_fragment.xml
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/pending_member_background"
|
||||||
|
android:fillViewport="true"
|
||||||
|
tools:context=".groups.ui.managegroup.ManageGroupFragment">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/group_title_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="16dp">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
|
android:id="@+id/group_avatar"
|
||||||
|
android:layout_width="96dp"
|
||||||
|
android:layout_height="96dp"
|
||||||
|
android:layout_gravity="center_horizontal" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/group_title"
|
||||||
|
style="@style/TextAppearance.Signal.Title2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
tools:text="Parkdale Run Crew" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/member_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="12 members (4 invited)" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/group_disappearing_messages_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/group_title_card">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
style="@style/Widget.Signal.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:text="@string/ManageGroupActivity_disappearing_messages" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/disappearing_messages"
|
||||||
|
style="@style/Widget.Signal.Button.TextButton.Ultramarine"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="textEnd"
|
||||||
|
tools:text="Off" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/group_media_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/group_disappearing_messages_card"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/rail_label"
|
||||||
|
style="@style/Widget.Signal.Button.TextButton.Ultramarine"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/recipient_preference_activity__shared_media" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.ThreadPhotoRailView
|
||||||
|
android:id="@+id/recent_photos"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="90dp"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/group_access_control_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/group_media_card"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/edit_group_membership_title"
|
||||||
|
style="@style/Widget.Signal.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ManageGroupActivity_who_can_edit_group_membership" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/edit_group_membership_value"
|
||||||
|
style="@style/Widget.Signal.Button.TextButton.Ultramarine"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="textEnd"
|
||||||
|
tools:text="Only admin" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/edit_group_access_title"
|
||||||
|
style="@style/Widget.Signal.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ManageGroupActivity_who_can_edit_group_info" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/edit_group_access_value"
|
||||||
|
style="@style/Widget.Signal.Button.TextButton.Ultramarine"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAlignment="textEnd"
|
||||||
|
tools:text="All members" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/group_membership_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/group_access_control_card">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
|
||||||
|
android:id="@+id/group_members"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:maxHeight="280dp"
|
||||||
|
tools:listitem="@layout/group_recipient_list_item" />
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/group_pending_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/group_membership_card">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/listPending"
|
||||||
|
style="@style/Widget.Signal.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ManageGroupActivity_pending_group_invites" />
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/group_block_and_leave_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:cardBackgroundColor="?android:attr/windowBackground"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/group_pending_card">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/blockGroup"
|
||||||
|
style="@style/Widget.Signal.Button.TextButton.Red"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ManageGroupActivity_block_group" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/leaveGroup"
|
||||||
|
style="@style/Widget.Signal.Button.TextButton.Red"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ManageGroupActivity_leave_group" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
|
@ -22,15 +22,31 @@
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:gravity="start|center_vertical"
|
android:gravity="start|center_vertical"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
android:textColor="?title_text_color_primary"
|
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||||
android:textSize="14sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/popupMenuProgressContainer"
|
app:layout_constraintEnd_toStartOf="@+id/admin"
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
|
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
|
||||||
app:layout_constraintTop_toTopOf="@+id/recipient_avatar"
|
app:layout_constraintTop_toTopOf="@+id/recipient_avatar"
|
||||||
tools:text="@tools:sample/full_names" />
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/admin"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:text="@string/GroupRecipientListItem_admin"
|
||||||
|
android:textColor="?attr/title_text_color_secondary"
|
||||||
|
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/recipient_name"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/popupMenuProgressContainer"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/recipient_name"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/recipient_name"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/popupMenuProgressContainer"
|
android:id="@+id/popupMenuProgressContainer"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
|
|
|
@ -17,6 +17,17 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/admin_action_busy"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/recipient_avatar"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/recipient_avatar"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/recipient_avatar"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/full_name"
|
android:id="@+id/full_name"
|
||||||
style="@style/TextAppearance.Signal.Body1.Bold"
|
style="@style/TextAppearance.Signal.Body1.Bold"
|
||||||
|
@ -53,7 +64,7 @@
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/message_button"
|
android:id="@+id/message_button"
|
||||||
style="@style/Signal.Button.TextButton.Drawable"
|
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
@ -63,7 +74,7 @@
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/secure_call_button"
|
android:id="@+id/secure_call_button"
|
||||||
style="@style/Signal.Button.TextButton.Drawable"
|
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
@ -72,7 +83,7 @@
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/block_button"
|
android:id="@+id/block_button"
|
||||||
style="@style/Signal.Button.TextButton.Drawable"
|
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
@ -83,7 +94,7 @@
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/unblock_button"
|
android:id="@+id/unblock_button"
|
||||||
style="@style/Signal.Button.TextButton.Drawable"
|
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
@ -94,7 +105,7 @@
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/view_safety_number_button"
|
android:id="@+id/view_safety_number_button"
|
||||||
style="@style/Signal.Button.TextButton.Drawable"
|
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
@ -103,7 +114,7 @@
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/make_group_admin_button"
|
android:id="@+id/make_group_admin_button"
|
||||||
style="@style/Signal.Button.TextButton.Drawable"
|
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
@ -114,7 +125,7 @@
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/remove_group_admin_button"
|
android:id="@+id/remove_group_admin_button"
|
||||||
style="@style/Signal.Button.TextButton.Drawable"
|
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
@ -122,10 +133,9 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:drawableStartCompat="?attr/recipient_make_admin_icon"
|
app:drawableStartCompat="?attr/recipient_make_admin_icon"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/remove_from_group_button"
|
android:id="@+id/remove_from_group_button"
|
||||||
style="@style/Signal.Button.TextButton.Drawable"
|
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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">
|
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<!-- TODO GV2 Remove old one when GV2 can replace all functions -->
|
||||||
<item android:id="@+id/menu_edit_group"
|
<item android:id="@+id/menu_edit_group"
|
||||||
android:title="@string/conversation__menu_edit_group"
|
android:title="@string/conversation__menu_edit_group"
|
||||||
app:showAsAction="collapseActionView" />
|
app:showAsAction="collapseActionView" />
|
||||||
|
|
||||||
|
<item android:id="@+id/menu_manage_group"
|
||||||
|
android:title="@string/conversation__menu_manage_group"
|
||||||
|
app:showAsAction="collapseActionView" />
|
||||||
|
|
||||||
<item android:id="@+id/menu_leave"
|
<item android:id="@+id/menu_leave"
|
||||||
android:title="@string/conversation__menu_leave_group"
|
android:title="@string/conversation__menu_leave_group"
|
||||||
app:showAsAction="collapseActionView"/>
|
app:showAsAction="collapseActionView"/>
|
||||||
|
|
|
@ -429,6 +429,7 @@
|
||||||
<!-- GroupCreateActivity -->
|
<!-- GroupCreateActivity -->
|
||||||
<string name="GroupCreateActivity_actionbar_title">New group</string>
|
<string name="GroupCreateActivity_actionbar_title">New group</string>
|
||||||
<string name="GroupCreateActivity_actionbar_edit_title">Edit group</string>
|
<string name="GroupCreateActivity_actionbar_edit_title">Edit group</string>
|
||||||
|
<string name="GroupCreateActivity_actionbar_manage_title">Manage group</string>
|
||||||
<string name="GroupCreateActivity_group_name_hint">Group name</string>
|
<string name="GroupCreateActivity_group_name_hint">Group name</string>
|
||||||
<string name="GroupCreateActivity_actionbar_mms_title">New MMS group</string>
|
<string name="GroupCreateActivity_actionbar_mms_title">New MMS group</string>
|
||||||
<string name="GroupCreateActivity_contacts_dont_support_push">You have selected a contact that doesn\'t support Signal groups, so this group will be MMS.</string>
|
<string name="GroupCreateActivity_contacts_dont_support_push">You have selected a contact that doesn\'t support Signal groups, so this group will be MMS.</string>
|
||||||
|
@ -457,6 +458,14 @@
|
||||||
<string name="GroupManagement_access_level_all_members">All members</string>
|
<string name="GroupManagement_access_level_all_members">All members</string>
|
||||||
<string name="GroupManagement_access_level_only_admins">Only admins</string>
|
<string name="GroupManagement_access_level_only_admins">Only admins</string>
|
||||||
<string name="GroupManagement_access_level_unknown" translatable="false">Unknown</string>
|
<string name="GroupManagement_access_level_unknown" translatable="false">Unknown</string>
|
||||||
|
<array name="GroupManagement_edit_group_membership_choices">
|
||||||
|
<item>@string/GroupManagement_access_level_all_members</item>
|
||||||
|
<item>@string/GroupManagement_access_level_only_admins</item>
|
||||||
|
</array>
|
||||||
|
<array name="GroupManagement_edit_group_info_choices">
|
||||||
|
<item>@string/GroupManagement_access_level_all_members</item>
|
||||||
|
<item>@string/GroupManagement_access_level_only_admins</item>
|
||||||
|
</array>
|
||||||
|
|
||||||
<!-- PendingMembersActivity -->
|
<!-- PendingMembersActivity -->
|
||||||
<string name="PendingMemberInvitesActivity_pending_group_invites">Pending group invites</string>
|
<string name="PendingMemberInvitesActivity_pending_group_invites">Pending group invites</string>
|
||||||
|
@ -477,6 +486,20 @@
|
||||||
<item quantity="other">Error canceling invites</item>
|
<item quantity="other">Error canceling invites</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
|
<!-- ManageGroupActivity -->
|
||||||
|
<string name="ManageGroupActivity_disappearing_messages">Disappearing messages</string>
|
||||||
|
<string name="ManageGroupActivity_pending_group_invites">Pending group invites</string>
|
||||||
|
<string name="ManageGroupActivity_who_can_edit_group_membership">Who can edit group membership</string>
|
||||||
|
<string name="ManageGroupActivity_who_can_edit_group_info">Who can edit group info</string>
|
||||||
|
<string name="ManageGroupActivity_block_group">Block group</string>
|
||||||
|
<string name="ManageGroupActivity_leave_group">Leave group</string>
|
||||||
|
|
||||||
|
<string name="ManageGroupActivity_you_dont_have_the_rights_to_do_this">You don\'t have the rights to do this</string>
|
||||||
|
<string name="ManageGroupActivity_failed_to_update_the_group">Failed to update the group</string>
|
||||||
|
|
||||||
|
<string name="GroupManagement_choose_who_can_add_or_invite_new_members">Choose who can add or invite new members</string>
|
||||||
|
<string name="GroupManagement_choose_who_can_change_the_group_name_and_photo">Choose who can change the group name and photo</string>
|
||||||
|
|
||||||
<plurals name="GroupMemberList_invited">
|
<plurals name="GroupMemberList_invited">
|
||||||
<item quantity="one">%1$s invited 1 person</item>
|
<item quantity="one">%1$s invited 1 person</item>
|
||||||
<item quantity="other">%1$s invited %2$d people</item>
|
<item quantity="other">%1$s invited %2$d people</item>
|
||||||
|
@ -1836,6 +1859,7 @@
|
||||||
<!-- conversation -->
|
<!-- conversation -->
|
||||||
<string name="conversation__menu_add_attachment">Add attachment</string>
|
<string name="conversation__menu_add_attachment">Add attachment</string>
|
||||||
<string name="conversation__menu_edit_group">Edit group</string>
|
<string name="conversation__menu_edit_group">Edit group</string>
|
||||||
|
<string name="conversation__menu_manage_group">Manage group</string>
|
||||||
<string name="conversation__menu_leave_group">Leave group</string>
|
<string name="conversation__menu_leave_group">Leave group</string>
|
||||||
<string name="conversation__menu_view_all_media">All media</string>
|
<string name="conversation__menu_view_all_media">All media</string>
|
||||||
<string name="conversation__menu_conversation_settings">Conversation settings</string>
|
<string name="conversation__menu_conversation_settings">Conversation settings</string>
|
||||||
|
@ -2167,6 +2191,9 @@
|
||||||
<string name="RecipientBottomSheet_make_group_admin">Make group admin</string>
|
<string name="RecipientBottomSheet_make_group_admin">Make group admin</string>
|
||||||
<string name="RecipientBottomSheet_remove_as_admin">Remove as admin</string>
|
<string name="RecipientBottomSheet_remove_as_admin">Remove as admin</string>
|
||||||
<string name="RecipientBottomSheet_remove_from_group">Remove from group</string>
|
<string name="RecipientBottomSheet_remove_from_group">Remove from group</string>
|
||||||
|
|
||||||
|
<string name="GroupRecipientListItem_admin">Admin</string>
|
||||||
|
|
||||||
<!-- EOF -->
|
<!-- EOF -->
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -415,15 +415,23 @@
|
||||||
</attr>
|
</attr>
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
|
|
||||||
<style name="Signal.Button.TextButton" parent="Widget.AppCompat.Button.Borderless">
|
<style name="Widget.Signal.Button.TextButton" parent="Widget.AppCompat.Button.Borderless">
|
||||||
<item name="android:textAppearance">@style/TextAppearance.Signal.Body1</item>
|
<item name="android:textAppearance">@style/TextAppearance.Signal.Body1</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Signal.Button.TextButton.Drawable">
|
<style name="Widget.Signal.Button.TextButton.Drawable">
|
||||||
<item name="android:textAlignment">viewStart</item>
|
<item name="android:textAlignment">viewStart</item>
|
||||||
<item name="android:drawablePadding">16dp</item>
|
<item name="android:drawablePadding">16dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Signal.Button.TextButton.Red" >
|
||||||
|
<item name="android:textColor">@color/core_red</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Signal.Button.TextButton.Ultramarine" >
|
||||||
|
<item name="android:textColor">@color/ultramarine_text_button</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="Widget.Signal.Button.CalleeDialog" parent="Widget.AppCompat.Button">
|
<style name="Widget.Signal.Button.CalleeDialog" parent="Widget.AppCompat.Button">
|
||||||
<item name="android:textColor">@color/core_ultramarine</item>
|
<item name="android:textColor">@color/core_ultramarine</item>
|
||||||
<item name="android:background">@drawable/callee_dialog_button_background</item>
|
<item name="android:background">@drawable/callee_dialog_button_background</item>
|
||||||
|
|
Loading…
Add table
Reference in a new issue