Add support for announcement groups.

This commit is contained in:
Greyson Parrelli
2021-07-23 16:22:08 -04:00
parent 1a56924a56
commit 25234496bf
74 changed files with 1109 additions and 208 deletions

View File

@@ -43,6 +43,7 @@ import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
@@ -86,6 +87,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@@ -392,6 +394,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private InputPanel inputPanel;
private View panelParent;
private View noLongerMemberBanner;
private Stub<TextView> cannotSendInAnnouncementGroupBanner;
private View requestingMemberBanner;
private View cancelJoinRequest;
private Stub<View> mentionsSuggestions;
@@ -419,8 +422,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
private long threadId;
private int distributionType;
private boolean isSecureText;
private boolean isDefaultSms;
private int reactWithAnyEmojiStartPage = -1;
private boolean isDefaultSms = true;
private boolean isMmsEnabled = true;
private boolean isSecurityInitialized = false;
private boolean isSearchRequested = false;
@@ -446,7 +449,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
finish();
return;
}
isDefaultSms = Util.isDefaultSmsProvider(this);
voiceNoteMediaController = new VoiceNoteMediaController(this);
voiceRecorderWakeLock = new VoiceRecorderWakeLock(this);
@@ -1014,7 +1017,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
searchViewModel.onSearchOpened();
searchNav.setVisibility(View.VISIBLE);
searchNav.setData(0, 0);
inputPanel.setVisibility(View.GONE);
inputPanel.setHideForSearch(true);
for (int i = 0; i < menu.size(); i++) {
if (!menu.getItem(i).equals(searchViewItem)) {
@@ -1030,7 +1033,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
isSearchRequested = false;
searchViewModel.onSearchClosed();
searchNav.setVisibility(View.GONE);
inputPanel.setVisibility(View.VISIBLE);
inputPanel.setHideForSearch(false);
fragment.onSearchQueryUpdated(null);
setBlockedUserState(recipient.get(), isSecureText, isDefaultSms);
invalidateOptionsMenu();
@@ -1424,7 +1427,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void handleVideo(final Recipient recipient) {
if (recipient == null) return;
CommunicationActions.startVideoCall(this, recipient);
if (recipient.isPushV2Group() && groupViewModel.isNonAdminInAnnouncementGroup()) {
new MaterialAlertDialogBuilder(this).setTitle(R.string.ConversationActivity_cant_start_group_call)
.setMessage(R.string.ConversationActivity_only_admins_of_this_group_can_start_a_call)
.setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss())
.show();
} else {
CommunicationActions.startVideoCall(this, recipient);
}
}
private void handleDisplayGroupRecipients() {
@@ -1621,17 +1631,20 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
private void initializeEnabledCheck() {
groupViewModel.getSelfMemberLevel().observe(this, selfMemberShip -> {
groupViewModel.getSelfMemberLevel().observe(this, selfMembership -> {
boolean canSendMessages;
boolean leftGroup;
boolean canCancelRequest;
if (selfMemberShip == null) {
if (selfMembership == null) {
leftGroup = false;
canSendMessages = true;
canCancelRequest = false;
if (cannotSendInAnnouncementGroupBanner.resolved()) {
cannotSendInAnnouncementGroupBanner.get().setVisibility(View.GONE);
}
} else {
switch (selfMemberShip) {
switch (selfMembership.getMemberLevel()) {
case NOT_A_MEMBER:
leftGroup = true;
canSendMessages = false;
@@ -1656,10 +1669,22 @@ public class ConversationActivity extends PassphraseRequiredActivity
default:
throw new AssertionError();
}
if (selfMembership.isAnnouncementGroup() && selfMembership.getMemberLevel() != GroupDatabase.MemberLevel.ADMINISTRATOR) {
canSendMessages = false;
cannotSendInAnnouncementGroupBanner.get().setVisibility(View.VISIBLE);
cannotSendInAnnouncementGroupBanner.get().setMovementMethod(LinkMovementMethod.getInstance());
cannotSendInAnnouncementGroupBanner.get().setText(SpanUtil.clickSubstring(this, R.string.ConversationActivity_only_s_can_send_messages, R.string.ConversationActivity_admins, v -> {
ShowAdminsBottomSheetDialog.show(getSupportFragmentManager(), getRecipient().requireGroupId().requireV2());
}));
} else if (cannotSendInAnnouncementGroupBanner.resolved()) {
cannotSendInAnnouncementGroupBanner.get().setVisibility(View.GONE);
}
}
noLongerMemberBanner.setVisibility(leftGroup ? View.VISIBLE : View.GONE);
requestingMemberBanner.setVisibility(canCancelRequest ? View.VISIBLE : View.GONE);
if (canCancelRequest) {
cancelJoinRequest.setOnClickListener(v -> ConversationGroupViewModel.onCancelJoinRequest(getRecipient(), new AsynchronousCallback.MainThread<Void, GroupChangeFailureReason>() {
@Override
@@ -1675,7 +1700,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}.toWorkerCallback()));
}
inputPanel.setVisibility(canSendMessages ? View.VISIBLE : View.GONE);
inputPanel.setHideForGroupState(!canSendMessages);
inputPanel.setEnabled(canSendMessages);
sendButton.setEnabled(canSendMessages);
attachButton.setEnabled(canSendMessages);
@@ -2012,10 +2037,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
reactionDelegate = new ConversationReactionDelegate(reactionOverlayStub);
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
cancelJoinRequest = findViewById(R.id.conversation_cancel_request);
joinGroupCallButton = findViewById(R.id.conversation_group_call_join);
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
cannotSendInAnnouncementGroupBanner = ViewUtil.findStubById(this, R.id.conversation_cannot_send_announcement_stub);
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
cancelJoinRequest = findViewById(R.id.conversation_cancel_request);
joinGroupCallButton = findViewById(R.id.conversation_group_call_join);
container.setIsBubble(isInBubble());
container.addOnKeyboardShownListener(this);
@@ -2678,17 +2704,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) {
if (!isSecureText && isPushGroupConversation()) {
unblockButton.setVisibility(View.GONE);
inputPanel.setVisibility(View.GONE);
inputPanel.setHideForBlockedState(true);
makeDefaultSmsButton.setVisibility(View.GONE);
registerButton.setVisibility(View.VISIBLE);
} else if (!isSecureText && !isDefaultSms && recipient.hasSmsAddress()) {
unblockButton.setVisibility(View.GONE);
inputPanel.setVisibility(View.GONE);
inputPanel.setHideForBlockedState(true);
makeDefaultSmsButton.setVisibility(View.VISIBLE);
registerButton.setVisibility(View.GONE);
} else {
boolean inactivePushGroup = isPushGroupConversation() && !recipient.isActiveGroup();
inputPanel.setVisibility(inactivePushGroup ? View.GONE : View.VISIBLE);
inputPanel.setHideForBlockedState(inactivePushGroup);
unblockButton.setVisibility(View.GONE);
makeDefaultSmsButton.setVisibility(View.GONE);
registerButton.setVisibility(View.GONE);

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,20 +31,18 @@ import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
final class ConversationGroupViewModel extends ViewModel {
private final MutableLiveData<Recipient> liveRecipient;
private final LiveData<GroupActiveState> groupActiveState;
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
private final LiveData<ConversationMemberLevel> selfMembershipLevel;
private final LiveData<Integer> actionableRequestingMembers;
private final LiveData<ReviewState> reviewState;
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
@@ -75,7 +72,6 @@ final class ConversationGroupViewModel extends ViewModel {
(record, dups) -> dups.isEmpty()
? ReviewState.EMPTY
: new ReviewState(record.getId().requireV2(), dups.get(0), dups.size()));
}
void onRecipientChange(Recipient recipient) {
@@ -102,7 +98,7 @@ final class ConversationGroupViewModel extends ViewModel {
return groupActiveState;
}
LiveData<GroupDatabase.MemberLevel> getSelfMemberLevel() {
LiveData<ConversationMemberLevel> getSelfMemberLevel() {
return selfMembershipLevel;
}
@@ -114,6 +110,11 @@ final class ConversationGroupViewModel extends ViewModel {
return gv1MigrationSuggestions;
}
boolean isNonAdminInAnnouncementGroup() {
ConversationMemberLevel level = selfMembershipLevel.getValue();
return level != null && level.getMemberLevel() != GroupDatabase.MemberLevel.ADMINISTRATOR && level.isAnnouncementGroup();
}
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
if (recipient != null && recipient.isGroup()) {
Application context = ApplicationDependencies.getApplication();
@@ -144,11 +145,11 @@ final class ConversationGroupViewModel extends ViewModel {
return new GroupActiveState(record.isActive(), record.isV2Group());
}
private static GroupDatabase.MemberLevel mapToSelfMembershipLevel(@Nullable GroupRecord record) {
private static ConversationMemberLevel mapToSelfMembershipLevel(@Nullable GroupRecord record) {
if (record == null) {
return null;
}
return record.memberLevel(Recipient.self());
return new ConversationMemberLevel(record.memberLevel(Recipient.self()), record.isAnnouncementGroup());
}
@WorkerThread
@@ -257,6 +258,24 @@ final class ConversationGroupViewModel extends ViewModel {
}
}
static final class ConversationMemberLevel {
private final GroupDatabase.MemberLevel memberLevel;
private final boolean isAnnouncementGroup;
private ConversationMemberLevel(GroupDatabase.MemberLevel memberLevel, boolean isAnnouncementGroup) {
this.memberLevel = memberLevel;
this.isAnnouncementGroup = isAnnouncementGroup;
}
public @NonNull GroupDatabase.MemberLevel getMemberLevel() {
return memberLevel;
}
public boolean isAnnouncementGroup() {
return isAnnouncementGroup;
}
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {

View File

@@ -0,0 +1,100 @@
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.ParcelableGroupId;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
/**
* Renders a list of admins for a specified groupId. Tapping on one will allow you to send them a message.
*/
public final class ShowAdminsBottomSheetDialog extends BottomSheetDialogFragment {
private static final String KEY_GROUP_ID = "group_id";
private final LifecycleDisposable disposables = new LifecycleDisposable();
public static void show(@NonNull FragmentManager manager, @NonNull GroupId.V2 groupId) {
ShowAdminsBottomSheetDialog fragment = new ShowAdminsBottomSheetDialog();
Bundle args = new Bundle();
args.putParcelable(KEY_GROUP_ID, ParcelableGroupId.from(groupId));
fragment.setArguments(args);
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
setStyle(DialogFragment.STYLE_NORMAL, R.style.Signal_DayNight_BottomSheet_Rounded);
super.onCreate(savedInstanceState);
}
@Override
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.show_admin_bottom_sheet, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
GroupMemberListView list = view.findViewById(R.id.show_admin_list);
list.setDisplayOnlyMembers(Collections.emptyList());
list.setRecipientClickListener(recipient -> {
CommunicationActions.startConversation(requireContext(), recipient, null);
dismissAllowingStateLoss();
});
disposables.add(Single.fromCallable(() -> getAdmins(requireContext().getApplicationContext(), getGroupId()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(list::setDisplayOnlyMembers));
}
@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
BottomSheetUtil.show(manager, tag, this);
}
private GroupId getGroupId() {
return ParcelableGroupId.get(requireArguments().getParcelable(KEY_GROUP_ID));
}
@WorkerThread
private static @NonNull List<Recipient> getAdmins(@NonNull Context context, @NonNull GroupId groupId) {
return DatabaseFactory.getGroupDatabase(context)
.getGroup(groupId)
.transform(GroupDatabase.GroupRecord::getAdmins)
.or(Collections.emptyList());
}
}