mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 10:51:27 +01:00
Add support for announcement groups.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user