Add support for announcement groups.

fork-5.53.8
Greyson Parrelli 2021-07-23 16:22:08 -04:00
rodzic 1a56924a56
commit 25234496bf
74 zmienionych plików z 1109 dodań i 208 usunięć

Wyświetl plik

@ -8,15 +8,16 @@ public final class AppCapabilities {
private AppCapabilities() {
}
private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
private static final boolean GV1_MIGRATION = true;
private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
private static final boolean GV1_MIGRATION = true;
private static final boolean ANNOUNCEMENT_GROUPS = true;
/**
* @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not.
*/
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, FeatureFlags.senderKey());
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, FeatureFlags.senderKey(), ANNOUNCEMENT_GROUPS);
}
}

Wyświetl plik

@ -36,6 +36,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.function.Consumer;
/**
* Base activity container for selecting a list of contacts.
@ -122,8 +123,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
return true;
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
callback.accept(true);
}
@Override

Wyświetl plik

@ -53,6 +53,7 @@ import androidx.transition.TransitionManager;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.signal.core.util.logging.Log;
@ -88,6 +89,7 @@ import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
/**
* Fragment for selecting a one or more contacts from a list.
@ -566,28 +568,32 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
if (onContactSelectedListener != null) {
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
if (allowed) {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
});
} else {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
}
});
} else {
if (onContactSelectedListener != null) {
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber(), allowed -> {
if (allowed) {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
});
} else {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
@ -742,8 +748,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
public interface OnContactSelectedListener {
/** @return True if the contact is allowed to be selected, otherwise false. */
boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number);
/** Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it. */
void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback);
void onContactDeselected(Optional<RecipientId> recipientId, String number);
void onSelectionChanged();
}

Wyświetl plik

@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
@ -139,9 +140,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
return true;
callback.accept(true);
}
@Override

Wyświetl plik

@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.function.Consumer;
/**
* Activity container for starting a new conversation.
@ -60,7 +61,7 @@ public class NewConversationActivity extends ContactSelectionActivity
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
if (recipientId.isPresent()) {
launch(Recipient.resolved(recipientId.get()));
} else {
@ -94,7 +95,7 @@ public class NewConversationActivity extends ContactSelectionActivity
}
}
return true;
callback.accept(true);
}
@Override

Wyświetl plik

@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.blocked;
import android.app.AlertDialog;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
@ -8,10 +7,12 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
@ -25,6 +26,8 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.function.Consumer;
public class BlockedUsersActivity extends PassphraseRequiredActivity implements BlockedUsersFragment.Listener, ContactSelectionListFragment.OnContactSelectedListener {
private static final String CONTACT_SELECTION_FRAGMENT = "Contact.Selection.Fragment";
@ -84,24 +87,24 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
final String displayName = recipientId.transform(id -> Recipient.resolved(id).getDisplayName(this)).or(number);
AlertDialog confirmationDialog = new AlertDialog.Builder(BlockedUsersActivity.this)
.setTitle(R.string.BlockedUsersActivity__block_user)
.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName))
.setPositiveButton(R.string.BlockedUsersActivity__block, (dialog, which) -> {
if (recipientId.isPresent()) {
viewModel.block(recipientId.get());
} else {
viewModel.createAndBlock(number);
}
dialog.dismiss();
onBackPressed();
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
.setCancelable(true)
.create();
AlertDialog confirmationDialog = new MaterialAlertDialogBuilder(this)
.setTitle(R.string.BlockedUsersActivity__block_user)
.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName))
.setPositiveButton(R.string.BlockedUsersActivity__block, (dialog, which) -> {
if (recipientId.isPresent()) {
viewModel.block(recipientId.get());
} else {
viewModel.createAndBlock(number);
}
dialog.dismiss();
onBackPressed();
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
.setCancelable(true)
.create();
confirmationDialog.setOnShowListener(dialog -> {
confirmationDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(Color.RED);
@ -109,7 +112,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
confirmationDialog.show();
return false;
callback.accept(false);
}
@Override

Wyświetl plik

@ -93,6 +93,10 @@ public class InputPanel extends LinearLayout
private @Nullable Listener listener;
private boolean emojiVisible;
private boolean hideForGroupState;
private boolean hideForBlockedState;
private boolean hideForSearch;
private ConversationStickerSuggestionAdapter stickerSuggestionAdapter;
public InputPanel(Context context) {
@ -317,6 +321,21 @@ public class InputPanel extends LinearLayout
}
}
public void setHideForGroupState(boolean hideForGroupState) {
this.hideForGroupState = hideForGroupState;
updateVisibility();
}
public void setHideForBlockedState(boolean hideForBlockedState) {
this.hideForBlockedState = hideForBlockedState;
updateVisibility();
}
public void setHideForSearch(boolean hideForSearch) {
this.hideForSearch = hideForSearch;
updateVisibility();
}
@Override
public void onRecordPermissionRequired() {
if (listener != null) listener.onRecorderPermissionRequired();
@ -495,6 +514,14 @@ public class InputPanel extends LinearLayout
buttonToggle.animate().alpha(1).setDuration(FADE_TIME).start();
}
private void updateVisibility() {
if (hideForGroupState || hideForBlockedState || hideForSearch) {
setVisibility(GONE);
} else {
setVisibility(VISIBLE);
}
}
public interface Listener extends VoiceNoteDraftView.Listener {
void onRecorderStarted();
void onRecorderLocked();

Wyświetl plik

@ -15,6 +15,7 @@ sealed class ConversationSettingsEvent {
val groupId: GroupId,
val selectionWarning: Int,
val selectionLimit: Int,
val isAnnouncementGroup: Boolean,
val groupMembersWithoutSelf: List<RecipientId>
) : ConversationSettingsEvent()

Wyświetl plik

@ -317,7 +317,15 @@ class ConversationSettingsFragment : DSLSettingsFragment(
ButtonStripPreference.Model(
state = state.buttonStripState,
onVideoClick = {
CommunicationActions.startVideoCall(requireActivity(), state.recipient)
if (state.recipient.isPushV2Group && state.requireGroupSettingsState().isAnnouncementGroup && !state.requireGroupSettingsState().isSelfAdmin) {
MaterialAlertDialogBuilder(requireContext())
.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, _ -> d.dismiss() }
.show()
} else {
CommunicationActions.startVideoCall(requireActivity(), state.recipient)
}
},
onAudioClick = {
CommunicationActions.startVoiceCall(requireActivity(), state.recipient)
@ -666,6 +674,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
ContactsCursorLoader.DisplayMode.FLAG_PUSH,
addMembersToGroup.selectionWarning,
addMembersToGroup.selectionLimit,
addMembersToGroup.isAnnouncementGroup,
addMembersToGroup.groupMembersWithoutSelf
),
REQUEST_CODE_ADD_MEMBERS_TO_GROUP

Wyświetl plik

@ -13,11 +13,13 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.MediaDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.groups.GroupProtoUtil
import org.thoughtcrime.securesms.groups.LiveGroup
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@ -26,6 +28,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.libsignal.util.guava.Preconditions
import java.io.IOException
import java.util.concurrent.TimeUnit
private val TAG = Log.tag(ConversationSettingsRepository::class.java)
@ -130,9 +133,9 @@ class ConversationSettingsRepository(
members.addAll(groupRecord.members)
members.addAll(pendingMembers)
GroupCapacityResult(Recipient.self().id, members, FeatureFlags.groupLimits())
GroupCapacityResult(Recipient.self().id, members, FeatureFlags.groupLimits(), groupRecord.isAnnouncementGroup)
} else {
GroupCapacityResult(Recipient.self().id, groupRecord.members, FeatureFlags.groupLimits())
GroupCapacityResult(Recipient.self().id, groupRecord.members, FeatureFlags.groupLimits(), false)
}
)
}
@ -140,6 +143,25 @@ class ConversationSettingsRepository(
fun addMembers(groupId: GroupId, selected: List<RecipientId>, consumer: (GroupAddMembersResult) -> Unit) {
SignalExecutors.BOUNDED.execute {
val record: GroupDatabase.GroupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get()
if (record.isAnnouncementGroup) {
val needsResolve = selected
.map { Recipient.resolved(it) }
.filter { it.announcementGroupCapability != Recipient.Capability.SUPPORTED && !it.isSelf }
.map { it.id }
.toSet()
ApplicationDependencies.getJobManager().runSynchronously(RetrieveProfileJob(needsResolve), TimeUnit.SECONDS.toMillis(10))
val updatedWithCapabilities = needsResolve.map { Recipient.resolved(it) }
if (updatedWithCapabilities.any { it.announcementGroupCapability != Recipient.Capability.SUPPORTED }) {
consumer(GroupAddMembersResult.Failure(GroupChangeFailureReason.NOT_ANNOUNCEMENT_CAPABLE))
return@execute
}
}
consumer(
try {
val groupActionResult = GroupManager.addMembers(context, groupId.requirePush(), selected)

Wyświetl plik

@ -75,7 +75,8 @@ sealed class SpecificSettingsState {
private val groupDescriptionLoaded: Boolean = false,
val groupLinkEnabled: Boolean = false,
val membershipCountDescription: String = "",
val legacyGroupState: LegacyGroupPreference.State = LegacyGroupPreference.State.NONE
val legacyGroupState: LegacyGroupPreference.State = LegacyGroupPreference.State.NONE,
val isAnnouncementGroup: Boolean = false
) : SpecificSettingsState() {
override val isLoaded: Boolean = groupTitleLoaded && groupDescriptionLoaded

Wyświetl plik

@ -322,6 +322,14 @@ sealed class ConversationSettingsViewModel(
)
}
store.update(liveGroup.isAnnouncementGroup) { announcementGroup, state ->
state.copy(
specificSettingsState = state.requireGroupSettingsState().copy(
isAnnouncementGroup = announcementGroup
)
)
}
val isMessageRequestAccepted: LiveData<Boolean> = LiveDataUtil.mapAsync(liveGroup.groupRecipient) { r -> repository.isMessageRequestAccepted(r) }
val descriptionState: LiveData<DescriptionState> = LiveDataUtil.combineLatest(liveGroup.description, isMessageRequestAccepted, ::DescriptionState)
@ -386,11 +394,13 @@ sealed class ConversationSettingsViewModel(
override fun onAddToGroup() {
repository.getGroupCapacity(groupId) { capacityResult ->
if (capacityResult.getRemainingCapacity() > 0) {
internalEvents.postValue(
ConversationSettingsEvent.AddMembersToGroup(
groupId,
capacityResult.getSelectionWarning(),
capacityResult.getSelectionLimit(),
capacityResult.isAnnouncementGroup,
capacityResult.getMembersWithoutSelf()
)
)

Wyświetl plik

@ -7,7 +7,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
class GroupCapacityResult(
private val selfId: RecipientId,
private val members: List<RecipientId>,
private val selectionLimits: SelectionLimits
private val selectionLimits: SelectionLimits,
val isAnnouncementGroup: Boolean
) {
fun getMembers(): List<RecipientId?> {
return members

Wyświetl plik

@ -72,6 +72,20 @@ class PermissionsSettingsFragment : DSLSettingsFragment(
viewModel.setNonAdminCanEditGroupInfo(it == 1)
}
)
if (state.announcementGroupPermissionEnabled) {
radioListPref(
title = DSLSettingsText.from(R.string.PermissionsSettingsFragment__send_messages),
isEnabled = state.selfCanEditSettings,
listItems = permissionsOptions,
dialogTitle = DSLSettingsText.from(R.string.PermissionsSettingsFragment__who_can_send_messages),
selected = getSelected(!state.announcementGroup),
confirmAction = true,
onSelected = {
viewModel.setAnnouncementGroup(it == 0)
}
)
}
}
}

Wyświetl plik

@ -42,4 +42,18 @@ class PermissionsSettingsRepository(private val context: Context) {
}
}
}
fun applyAnnouncementGroupChange(groupId: GroupId, isAnnouncementGroup: Boolean, error: GroupChangeErrorCallback) {
SignalExecutors.UNBOUNDED.execute {
try {
GroupManager.applyAnnouncementGroupChange(context, groupId.requireV2(), isAnnouncementGroup)
} catch (e: GroupChangeException) {
Log.w(TAG, e)
error.onError(GroupChangeFailureReason.fromException(e))
} catch (e: IOException) {
Log.w(TAG, e)
error.onError(GroupChangeFailureReason.fromException(e))
}
}
}
}

Wyświetl plik

@ -3,5 +3,7 @@ package org.thoughtcrime.securesms.components.settings.conversation.permissions
data class PermissionsSettingsState(
val selfCanEditSettings: Boolean = false,
val nonAdminCanAddMembers: Boolean = false,
val nonAdminCanEditGroupInfo: Boolean = false
val nonAdminCanEditGroupInfo: Boolean = false,
val announcementGroupPermissionEnabled: Boolean = false,
val announcementGroup: Boolean = false
)

Wyświetl plik

@ -6,6 +6,8 @@ import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.groups.GroupAccessControl
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.LiveGroup
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.SingleLiveEvent
import org.thoughtcrime.securesms.util.livedata.Store
@ -33,6 +35,18 @@ class PermissionsSettingsViewModel(
store.update(liveGroup.attributesAccessControl) { attributesAccessControl, state ->
state.copy(nonAdminCanEditGroupInfo = attributesAccessControl == GroupAccessControl.ALL_MEMBERS)
}
store.update(liveGroup.isAnnouncementGroup) { isAnnouncementGroup, state ->
state.copy(
announcementGroup = isAnnouncementGroup,
announcementGroupPermissionEnabled = state.announcementGroupPermissionEnabled || isAnnouncementGroup
)
}
store.update(liveGroup.groupRecipient) { groupRecipient, state ->
val allHaveCapability = groupRecipient.participants.map { it.announcementGroupCapability }.all { it == Recipient.Capability.SUPPORTED }
state.copy(announcementGroupPermissionEnabled = (FeatureFlags.announcementGroups() && allHaveCapability) || state.announcementGroup)
}
}
fun setNonAdminCanAddMembers(nonAdminCanAddMembers: Boolean) {
@ -47,6 +61,12 @@ class PermissionsSettingsViewModel(
}
}
fun setAnnouncementGroup(announcementGroup: Boolean) {
repository.applyAnnouncementGroupChange(groupId, announcementGroup) { reason ->
internalEvents.postValue(PermissionsSettingsEvents.GroupChangeError(reason))
}
}
private fun Boolean.asGroupAccessControl(): GroupAccessControl {
return if (this) {
GroupAccessControl.ALL_MEMBERS

Wyświetl plik

@ -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);

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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());
}
}

Wyświetl plik

@ -18,6 +18,7 @@ import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
@ -56,6 +57,8 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collector;
import java.util.stream.Collectors;
public final class GroupDatabase extends Database {
@ -1037,10 +1040,18 @@ private static final String[] GROUP_PROJECTION = {
}
public @NonNull String getDescription() {
if (v2GroupProperties == null) {
return "";
} else {
if (v2GroupProperties != null) {
return v2GroupProperties.getDecryptedGroup().getDescription();
} else {
return "";
}
}
public boolean isAnnouncementGroup() {
if (v2GroupProperties != null) {
return v2GroupProperties.getDecryptedGroup().getIsAnnouncementGroup() == EnabledState.ENABLED;
} else {
return false;
}
}
@ -1048,6 +1059,15 @@ private static final String[] GROUP_PROJECTION = {
return members;
}
@WorkerThread
public @NonNull List<Recipient> getAdmins() {
if (v2GroupProperties != null) {
return v2GroupProperties.getAdmins(members.stream().map(Recipient::resolved).collect(Collectors.toList()));
} else {
return Collections.emptyList();
}
}
/** V1 members that were lost during the V1->V2 migration */
public @NonNull List<RecipientId> getUnmigratedV1Members() {
return unmigratedV1Members;
@ -1211,6 +1231,10 @@ private static final String[] GROUP_PROJECTION = {
.or(false);
}
public @NonNull List<Recipient> getAdmins(@NonNull List<Recipient> members) {
return members.stream().filter(this::isAdmin).collect(Collectors.toList());
}
public MemberLevel memberLevel(@NonNull Recipient recipient) {
Optional<UUID> uuid = recipient.getUuid();

Wyświetl plik

@ -166,6 +166,7 @@ public class RecipientDatabase extends Database {
static final int GROUPS_V2 = 0;
static final int GROUPS_V1_MIGRATION = 1;
static final int SENDER_KEY = 2;
static final int ANNOUNCEMENT_GROUPS = 3;
}
private static final String[] RECIPIENT_PROJECTION = new String[] {
@ -544,6 +545,7 @@ public class RecipientDatabase extends Database {
if (remapped != null) {
Recipient.live(remapped.first()).refresh(remapped.second());
ApplicationDependencies.getRecipientCache().remap(remapped.first(), remapped.second());
}
if (recipientNeedingRefresh != null || remapped != null) {
@ -1614,6 +1616,7 @@ public class RecipientDatabase extends Database {
value = Bitmask.update(value, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv2()).serialize());
value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration()).serialize());
value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey()).serialize());
value = Bitmask.update(value, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isAnnouncementGroup()).serialize());
ContentValues values = new ContentValues(1);
values.put(CAPABILITIES, value);
@ -3145,6 +3148,7 @@ public class RecipientDatabase extends Database {
private final Recipient.Capability groupsV2Capability;
private final Recipient.Capability groupsV1MigrationCapability;
private final Recipient.Capability senderKeyCapability;
private final Recipient.Capability announcementGroupCapability;
private final InsightsBannerTier insightsBannerTier;
private final byte[] storageId;
private final MentionSetting mentionSetting;
@ -3236,6 +3240,7 @@ public class RecipientDatabase extends Database {
this.groupsV2Capability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH));
this.groupsV1MigrationCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH));
this.senderKeyCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH));
this.announcementGroupCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH));
this.insightsBannerTier = insightsBannerTier;
this.storageId = storageId;
this.mentionSetting = mentionSetting;
@ -3389,6 +3394,10 @@ public class RecipientDatabase extends Database {
return senderKeyCapability;
}
public @NonNull Recipient.Capability getAnnouncementGroupCapability() {
return announcementGroupCapability;
}
public @Nullable byte[] getStorageId() {
return storageId;
}

Wyświetl plik

@ -19,6 +19,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil;
import org.thoughtcrime.securesms.util.ExpirationUtil;
@ -107,6 +108,7 @@ final class GroupsV2UpdateMessageProducer {
describeRequestingMembers(change, updates);
describeUnknownEditorRequestingMembersApprovals(change, updates);
describeUnknownEditorRequestingMembersDeletes(change, updates);
describeUnknownEditorAnnouncementGroupChange(change, updates);
describeUnknownEditorMemberRemovals(change, updates);
@ -131,6 +133,7 @@ final class GroupsV2UpdateMessageProducer {
describeRequestingMembers(change, updates);
describeRequestingMembersApprovals(change, updates);
describeRequestingMembersDeletes(change, updates);
describeAnnouncementGroupChange(change, updates);
describeMemberRemovals(change, updates);
@ -712,6 +715,32 @@ final class GroupsV2UpdateMessageProducer {
}
}
private void describeAnnouncementGroupChange(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
if (change.getNewIsAnnouncementGroup() == EnabledState.ENABLED) {
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_allow_only_admins_to_send), R.drawable.ic_update_group_role_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_allow_only_admins_to_send, editor), R.drawable.ic_update_group_role_16));
}
} else if (change.getNewIsAnnouncementGroup() == EnabledState.DISABLED) {
if (editorIsYou) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_allow_all_members_to_send), R.drawable.ic_update_group_role_16));
} else {
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_allow_all_members_to_send, editor), R.drawable.ic_update_group_role_16));
}
}
}
private void describeUnknownEditorAnnouncementGroupChange(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
if (change.getNewIsAnnouncementGroup() == EnabledState.ENABLED) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_allow_only_admins_to_send), R.drawable.ic_update_group_role_16));
} else if (change.getNewIsAnnouncementGroup() == EnabledState.DISABLED) {
updates.add(updateDescription(context.getString(R.string.MessageRecord_allow_all_members_to_send), R.drawable.ic_update_group_role_16));
}
}
interface DescribeMemberStrategy {
/**

Wyświetl plik

@ -296,6 +296,17 @@ public final class GroupManager {
}
}
@WorkerThread
public static void applyAnnouncementGroupChange(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull boolean isAnnouncementGroup)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
{
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
editor.updateAnnouncementGroup(isAnnouncementGroup);
}
}
@WorkerThread
public static void cycleGroupLinkPassword(@NonNull Context context,
@NonNull GroupId.V2 groupId)

Wyświetl plik

@ -336,6 +336,13 @@ final class GroupManagerV2 {
return commitChangeWithConflictResolution(groupOperations.createChangeMembershipRights(rightsToAccessControl(newRights)));
}
@WorkerThread
@NonNull GroupManager.GroupActionResult updateAnnouncementGroup(boolean isAnnouncementGroup)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
return commitChangeWithConflictResolution(groupOperations.createAnnouncementGroupChange(isAnnouncementGroup));
}
@WorkerThread
@NonNull GroupManager.GroupActionResult updateGroupTitleDescriptionAndAvatar(@Nullable String title, @Nullable String description, @Nullable byte[] avatarBytes, boolean avatarChanged)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
@ -345,7 +352,7 @@ final class GroupManagerV2 {
: GroupChange.Actions.newBuilder();
if (description != null) {
change.setModifyDescription(groupOperations.createModifyGroupDescription(description));
change.setModifyDescription(groupOperations.createModifyGroupDescriptionAction(description));
}
if (avatarChanged) {

Wyświetl plik

@ -129,6 +129,10 @@ public final class LiveGroup {
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::getDescription);
}
public LiveData<Boolean> isAnnouncementGroup() {
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::isAnnouncementGroup);
}
public LiveData<Recipient> getGroupRecipient() {
return recipient;
}

Wyświetl plik

@ -11,14 +11,15 @@ import java.io.IOException;
public enum GroupChangeFailureReason {
NO_RIGHTS,
NOT_CAPABLE,
NOT_GV2_CAPABLE,
NOT_ANNOUNCEMENT_CAPABLE,
NOT_A_MEMBER,
BUSY,
NETWORK,
OTHER;
public static @NonNull GroupChangeFailureReason fromException(@NonNull Throwable e) {
if (e instanceof MembershipNotSuitableForV2Exception) return GroupChangeFailureReason.NOT_CAPABLE;
if (e instanceof MembershipNotSuitableForV2Exception) return GroupChangeFailureReason.NOT_GV2_CAPABLE;
if (e instanceof IOException) return GroupChangeFailureReason.NETWORK;
if (e instanceof GroupNotAMemberException) return GroupChangeFailureReason.NOT_A_MEMBER;
if (e instanceof GroupChangeBusyException) return GroupChangeFailureReason.BUSY;

Wyświetl plik

@ -15,12 +15,13 @@ public final class GroupErrors {
}
switch (failureReason) {
case NO_RIGHTS : return R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this;
case NOT_CAPABLE : return R.string.ManageGroupActivity_not_capable;
case NOT_A_MEMBER: return R.string.ManageGroupActivity_youre_not_a_member_of_the_group;
case BUSY : return R.string.ManageGroupActivity_failed_to_update_the_group_please_retry_later;
case NETWORK : return R.string.ManageGroupActivity_failed_to_update_the_group_due_to_a_network_error_please_retry_later;
default : return R.string.ManageGroupActivity_failed_to_update_the_group;
case NO_RIGHTS : return R.string.ManageGroupActivity_you_dont_have_the_rights_to_do_this;
case NOT_GV2_CAPABLE : return R.string.ManageGroupActivity_not_capable;
case NOT_ANNOUNCEMENT_CAPABLE: return R.string.ManageGroupActivity_not_announcement_capable;
case NOT_A_MEMBER : return R.string.ManageGroupActivity_youre_not_a_member_of_the_group;
case BUSY : return R.string.ManageGroupActivity_failed_to_update_the_group_please_retry_later;
case NETWORK : return R.string.ManageGroupActivity_failed_to_update_the_group_due_to_a_network_error_please_retry_later;
default : return R.string.ManageGroupActivity_failed_to_update_the_group;
}
}
}

Wyświetl plik

@ -23,10 +23,12 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class AddMembersActivity extends PushContactSelectionActivity {
public static final String GROUP_ID = "group_id";
public static final String GROUP_ID = "group_id";
public static final String ANNOUNCEMENT_GROUP = "announcement_group";
private View done;
private AddMembersViewModel viewModel;
@ -36,10 +38,12 @@ public class AddMembersActivity extends PushContactSelectionActivity {
int displayModeFlags,
int selectionWarning,
int selectionLimit,
boolean isAnnouncementGroup,
@NonNull List<RecipientId> membersWithoutSelf) {
Intent intent = new Intent(context, AddMembersActivity.class);
intent.putExtra(AddMembersActivity.GROUP_ID, groupId.toString());
intent.putExtra(GROUP_ID, groupId.toString());
intent.putExtra(ANNOUNCEMENT_GROUP, isAnnouncementGroup);
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayModeFlags);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, new SelectionLimits(selectionWarning, selectionLimit));
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(membersWithoutSelf));
@ -75,10 +79,11 @@ public class AddMembersActivity extends PushContactSelectionActivity {
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
if (getGroupId().isV1() && recipientId.isPresent() && !Recipient.resolved(recipientId.get()).hasE164()) {
Toast.makeText(this, R.string.AddMembersActivity__this_person_cant_be_added_to_legacy_groups, Toast.LENGTH_SHORT).show();
return false;
callback.accept(false);
return;
}
if (contactsFragment.hasQueryFilter()) {
@ -87,7 +92,7 @@ public class AddMembersActivity extends PushContactSelectionActivity {
enableDone();
return true;
callback.accept(true);
}
@Override
@ -125,6 +130,10 @@ public class AddMembersActivity extends PushContactSelectionActivity {
return GroupId.parseOrThrow(getIntent().getStringExtra(GROUP_ID));
}
private boolean isAnnouncementGroup() {
return getIntent().getBooleanExtra(ANNOUNCEMENT_GROUP, false);
}
private void displayAlertMessage(@NonNull AddMembersViewModel.AddMemberDialogMessageState state) {
Recipient recipient = Util.firstNonNull(state.getRecipient(), Recipient.UNKNOWN);

Wyświetl plik

@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Group selection activity, will add a single member to selected groups.
@ -112,7 +113,7 @@ public final class AddToGroupsActivity extends ContactSelectionActivity {
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
if (contactsFragment.isMulti()) {
throw new UnsupportedOperationException("Not yet built to handle multi-select.");
// if (contactsFragment.hasQueryFilter()) {
@ -128,7 +129,7 @@ public final class AddToGroupsActivity extends ContactSelectionActivity {
}
}
return true;
callback.accept(true);
}
@Override

Wyświetl plik

@ -39,6 +39,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
public class CreateGroupActivity extends ContactSelectionActivity {
@ -97,14 +98,14 @@ public class CreateGroupActivity extends ContactSelectionActivity {
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
if (contactsFragment.hasQueryFilter()) {
getContactFilterView().clear();
}
shrinkSkip();
return true;
callback.accept(true);
}
@Override

Wyświetl plik

@ -101,6 +101,8 @@ public class RefreshAttributesJob extends BaseJob {
"\n Storage? " + capabilities.isStorage() +
"\n GV2? " + capabilities.isGv2() +
"\n GV1 Migration? " + capabilities.isGv1Migration() +
"\n Sender Key? " + capabilities.isSenderKey() +
"\n Announcement Groups? " + capabilities.isAnnouncementGroup() +
"\n UUID? " + capabilities.isUuid());
SignalServiceAccountManager signalAccountManager = ApplicationDependencies.getSignalServiceAccountManager();

Wyświetl plik

@ -203,7 +203,7 @@ public class RetrieveProfileJob extends BaseJob {
});
}
private RetrieveProfileJob(@NonNull Set<RecipientId> recipientIds) {
public RetrieveProfileJob(@NonNull Set<RecipientId> recipientIds) {
this(new Job.Parameters.Builder().addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(3)
.build(),

Wyświetl plik

@ -31,13 +31,15 @@ public final class LogSectionCapabilities implements LogSection {
AccountAttributes.Capabilities capabilities = AppCapabilities.getCapabilities(false);
return new StringBuilder().append("-- Local").append("\n")
.append("GV2 : ").append(capabilities.isGv2()).append("\n")
.append("GV1 Migration: ").append(capabilities.isGv1Migration()).append("\n")
.append("Sender Key : ").append(capabilities.isSenderKey()).append("\n")
.append("GV2 : ").append(capabilities.isGv2()).append("\n")
.append("GV1 Migration : ").append(capabilities.isGv1Migration()).append("\n")
.append("Sender Key : ").append(capabilities.isSenderKey()).append("\n")
.append("Announcement Groups: ").append(capabilities.isAnnouncementGroup()).append("\n")
.append("\n")
.append("-- Global").append("\n")
.append("GV2 : ").append(self.getGroupsV2Capability()).append("\n")
.append("GV1 Migration: ").append(self.getGroupsV1MigrationCapability()).append("\n")
.append("Sender Key : ").append(self.getSenderKeyCapability()).append("\n");
.append("GV2 : ").append(self.getGroupsV2Capability()).append("\n")
.append("GV1 Migration : ").append(self.getGroupsV1MigrationCapability()).append("\n")
.append("Sender Key : ").append(self.getSenderKeyCapability()).append("\n")
.append("Announcement Groups: ").append(self.getAnnouncementGroupCapability()).append("\n");
}
}

Wyświetl plik

@ -434,11 +434,32 @@ public final class MessageContentProcessor {
return true;
}
if (!groupDatabase.isCurrentMember(groupId, senderRecipient.getId())) {
Optional<GroupRecord> groupRecord = groupDatabase.getGroup(groupId);
if (groupRecord.isPresent() && !groupRecord.get().getMembers().contains(senderRecipient.getId())) {
log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message from member not in group " + groupId);
return true;
}
if (groupRecord.isPresent() && groupRecord.get().isAnnouncementGroup() && !groupRecord.get().getAdmins().contains(senderRecipient)) {
if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage data = content.getDataMessage().get();
if (data.getBody().isPresent() ||
data.getAttachments().isPresent() ||
data.getQuote().isPresent() ||
data.getPreviews().isPresent() ||
data.getMentions().isPresent() ||
data.getSticker().isPresent())
{
Log.w(TAG, "Ignoring message from " + senderRecipient.getId() + " because it has disallowed content, and they're not an admin in an announcement-only group.");
return true;
}
} else if (content.getTypingMessage().isPresent()) {
Log.w(TAG, "Ignoring typing indicator from " + senderRecipient.getId() + " because they're not an admin in an announcement-only group.");
return true;
}
}
return false;
}
@ -2149,7 +2170,13 @@ public final class MessageContentProcessor {
if (content.getTypingMessage().get().getGroupId().isPresent()) {
GroupId groupId = GroupId.push(content.getTypingMessage().get().getGroupId().get());
Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(context, groupId);
return groupRecipient.isBlocked() || !groupRecipient.isActiveGroup();
if (groupRecipient.isBlocked() || !groupRecipient.isActiveGroup()) {
return true;
} else {
Optional<GroupRecord> groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId);
return groupRecord.isPresent() && groupRecord.get().isAnnouncementGroup() && !groupRecord.get().getAdmins().contains(sender);
}
}
}

Wyświetl plik

@ -41,48 +41,49 @@ public class ApplicationMigrations {
private static final int LEGACY_CANONICAL_VERSION = 455;
private static final class Version {
static final int LEGACY = 1;
static final int RECIPIENT_ID = 2;
static final int RECIPIENT_SEARCH = 3;
static final int RECIPIENT_CLEANUP = 4;
static final int AVATAR_MIGRATION = 5;
static final int UUIDS = 6;
static final int CACHED_ATTACHMENTS = 7;
static final int STICKERS_LAUNCH = 8;
static final int LEGACY = 1;
static final int RECIPIENT_ID = 2;
static final int RECIPIENT_SEARCH = 3;
static final int RECIPIENT_CLEANUP = 4;
static final int AVATAR_MIGRATION = 5;
static final int UUIDS = 6;
static final int CACHED_ATTACHMENTS = 7;
static final int STICKERS_LAUNCH = 8;
//static final int TEST_ARGON2 = 9;
static final int SWOON_STICKERS = 10;
static final int STORAGE_SERVICE = 11;
static final int SWOON_STICKERS = 10;
static final int STORAGE_SERVICE = 11;
//static final int STORAGE_KEY_ROTATE = 12;
static final int REMOVE_AVATAR_ID = 13;
static final int STORAGE_CAPABILITY = 14;
static final int PIN_REMINDER = 15;
static final int VERSIONED_PROFILE = 16;
static final int PIN_OPT_OUT = 17;
static final int TRIM_SETTINGS = 18;
static final int THUMBNAIL_CLEANUP = 19;
static final int GV2 = 20;
static final int GV2_2 = 21;
static final int CDS = 22;
static final int BACKUP_NOTIFICATION = 23;
static final int GV1_MIGRATION = 24;
static final int USER_NOTIFICATION = 25;
static final int DAY_BY_DAY_STICKERS = 26;
static final int BLOB_LOCATION = 27;
static final int SYSTEM_NAME_SPLIT = 28;
static final int REMOVE_AVATAR_ID = 13;
static final int STORAGE_CAPABILITY = 14;
static final int PIN_REMINDER = 15;
static final int VERSIONED_PROFILE = 16;
static final int PIN_OPT_OUT = 17;
static final int TRIM_SETTINGS = 18;
static final int THUMBNAIL_CLEANUP = 19;
static final int GV2 = 20;
static final int GV2_2 = 21;
static final int CDS = 22;
static final int BACKUP_NOTIFICATION = 23;
static final int GV1_MIGRATION = 24;
static final int USER_NOTIFICATION = 25;
static final int DAY_BY_DAY_STICKERS = 26;
static final int BLOB_LOCATION = 27;
static final int SYSTEM_NAME_SPLIT = 28;
// Versions 29, 30 accidentally skipped
static final int MUTE_SYNC = 31;
static final int PROFILE_SHARING_UPDATE = 32;
static final int SMS_STORAGE_SYNC = 33;
static final int APPLY_UNIVERSAL_EXPIRE = 34;
static final int SENDER_KEY = 35;
static final int SENDER_KEY_2 = 36;
static final int DB_AUTOINCREMENT = 37;
static final int ATTACHMENT_CLEANUP = 38;
static final int LOG_CLEANUP = 39;
static final int ATTACHMENT_CLEANUP_2 = 40;
static final int MUTE_SYNC = 31;
static final int PROFILE_SHARING_UPDATE = 32;
static final int SMS_STORAGE_SYNC = 33;
static final int APPLY_UNIVERSAL_EXPIRE = 34;
static final int SENDER_KEY = 35;
static final int SENDER_KEY_2 = 36;
static final int DB_AUTOINCREMENT = 37;
static final int ATTACHMENT_CLEANUP = 38;
static final int LOG_CLEANUP = 39;
static final int ATTACHMENT_CLEANUP_2 = 40;
static final int ANNOUNCEMENT_GROUP_CAPABILITY = 41;
}
public static final int CURRENT_VERSION = 40;
public static final int CURRENT_VERSION = 41;
/**
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
@ -352,6 +353,10 @@ public class ApplicationMigrations {
jobs.put(Version.ATTACHMENT_CLEANUP_2, new AttachmentCleanupMigrationJob());
}
if (lastSeenVersion < Version.ANNOUNCEMENT_GROUP_CAPABILITY) {
jobs.put(Version.ANNOUNCEMENT_GROUP_CAPABILITY, new AttributesMigrationJob());
}
return jobs;
}

Wyświetl plik

@ -16,6 +16,8 @@ import androidx.core.app.RemoteInput
import androidx.core.graphics.drawable.IconCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
@ -28,6 +30,7 @@ import org.thoughtcrime.securesms.util.AvatarUtil
import org.thoughtcrime.securesms.util.BubbleUtil
import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.util.guava.Optional
import androidx.core.app.Person as PersonCompat
private const val BIG_PICTURE_DIMEN = 500
@ -103,7 +106,14 @@ sealed class NotificationBuilder(protected val context: Context) {
}
fun addReplyActions(conversation: NotificationConversation) {
if (privacy.isDisplayMessage && isNotLocked && RecipientUtil.isMessageRequestAccepted(context, conversation.recipient)) {
if (privacy.isDisplayMessage && isNotLocked && !conversation.recipient.isPushV1Group && RecipientUtil.isMessageRequestAccepted(context, conversation.recipient)) {
if (conversation.recipient.isPushV2Group) {
val group: Optional<GroupDatabase.GroupRecord> = DatabaseFactory.getGroupDatabase(context).getGroup(conversation.recipient.requireGroupId())
if (group.isPresent && group.get().isAnnouncementGroup && !group.get().isAdmin(Recipient.self())) {
return
}
}
addActions(ReplyMethod.forRecipient(context, conversation.recipient), conversation)
}
}

Wyświetl plik

@ -25,6 +25,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.function.Consumer;
public class PaymentRecipientSelectionFragment extends LoggingFragment implements ContactSelectionListFragment.OnContactSelectedListener, ContactSelectionListFragment.ScrollCallback {
@ -67,14 +69,14 @@ public class PaymentRecipientSelectionFragment extends LoggingFragment implement
}
@Override
public boolean onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, @Nullable String number) {
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, @Nullable String number, Consumer<Boolean> callback) {
if (recipientId.isPresent()) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
() -> Recipient.resolved(recipientId.get()),
this::createPaymentOrShowWarningDialog);
return false;
}
return false;
callback.accept(false);
}
@Override

Wyświetl plik

@ -97,6 +97,19 @@ public final class LiveRecipientCache {
return live;
}
/**
* Handles remapping cache entries when recipients are merged.
*/
public void remap(@NonNull RecipientId oldId, @NonNull RecipientId newId) {
synchronized (recipients) {
if (recipients.containsKey(newId)) {
recipients.put(oldId, recipients.get(newId));
} else {
recipients.remove(oldId);
}
}
}
/**
* Adds a recipient to the cache if we don't have an entry. This will also update a cache entry
* if the provided recipient is resolved, or if the existing cache entry is unresolved.

Wyświetl plik

@ -111,6 +111,7 @@ public class Recipient {
private final Capability groupsV2Capability;
private final Capability groupsV1MigrationCapability;
private final Capability senderKeyCapability;
private final Capability announcementGroupCapability;
private final InsightsBannerTier insightsBannerTier;
private final byte[] storageId;
private final MentionSetting mentionSetting;
@ -360,6 +361,7 @@ public class Recipient {
this.groupsV2Capability = Capability.UNKNOWN;
this.groupsV1MigrationCapability = Capability.UNKNOWN;
this.senderKeyCapability = Capability.UNKNOWN;
this.announcementGroupCapability = Capability.UNKNOWN;
this.storageId = null;
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
this.wallpaper = null;
@ -411,6 +413,7 @@ public class Recipient {
this.groupsV2Capability = details.groupsV2Capability;
this.groupsV1MigrationCapability = details.groupsV1MigrationCapability;
this.senderKeyCapability = details.senderKeyCapability;
this.announcementGroupCapability = details.announcementGroupCapability;
this.storageId = details.storageId;
this.mentionSetting = details.mentionSetting;
this.wallpaper = details.wallpaper;
@ -903,6 +906,10 @@ public class Recipient {
return senderKeyCapability;
}
public @NonNull Capability getAnnouncementGroupCapability() {
return announcementGroupCapability;
}
/**
* True if this recipient supports the message retry system, or false if we should use the legacy session reset system.
*/

Wyświetl plik

@ -64,6 +64,7 @@ public class RecipientDetails {
final Recipient.Capability groupsV2Capability;
final Recipient.Capability groupsV1MigrationCapability;
final Recipient.Capability senderKeyCapability;
final Recipient.Capability announcementGroupCapability;
final InsightsBannerTier insightsBannerTier;
final byte[] storageId;
final MentionSetting mentionSetting;
@ -119,6 +120,7 @@ public class RecipientDetails {
this.groupsV2Capability = settings.getGroupsV2Capability();
this.groupsV1MigrationCapability = settings.getGroupsV1MigrationCapability();
this.senderKeyCapability = settings.getSenderKeyCapability();
this.announcementGroupCapability = settings.getAnnouncementGroupCapability();
this.insightsBannerTier = settings.getInsightsBannerTier();
this.storageId = settings.getStorageId();
this.mentionSetting = settings.getMentionSetting();
@ -174,6 +176,7 @@ public class RecipientDetails {
this.groupsV2Capability = Recipient.Capability.UNKNOWN;
this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN;
this.senderKeyCapability = Recipient.Capability.UNKNOWN;
this.announcementGroupCapability = Recipient.Capability.UNKNOWN;
this.storageId = null;
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
this.wallpaper = null;

Wyświetl plik

@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@ -69,6 +70,9 @@ import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.schedulers.Schedulers;
/**
* Entry point for sharing content into the app.
*
@ -104,6 +108,8 @@ public class ShareActivity extends PassphraseRequiredActivity
private ShareIntents.Args args;
private ShareViewModel viewModel;
private final LifecycleDisposable disposables = new LifecycleDisposable();
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
@ -114,6 +120,8 @@ public class ShareActivity extends PassphraseRequiredActivity
protected void onCreate(Bundle icicle, boolean ready) {
setContentView(R.layout.share_activity);
disposables.bindTo(getLifecycle());
initializeArgs();
initializeViewModel();
initializeMedia();
@ -176,12 +184,28 @@ public class ShareActivity extends PassphraseRequiredActivity
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, java.util.function.Consumer<Boolean> callback) {
if (disallowMultiShare) {
Toast.makeText(this, R.string.ShareActivity__sharing_to_multiple_chats_is, Toast.LENGTH_LONG).show();
return false;
callback.accept(false);
} else {
return viewModel.onContactSelected(new ShareContact(recipientId, number));
disposables.add(viewModel.onContactSelected(new ShareContact(recipientId, number))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
switch (result) {
case TRUE:
callback.accept(true);
break;
case FALSE:
callback.accept(false);
break;
case FALSE_AND_SHOW_PERMISSION_TOAST:
Toast.makeText(this, R.string.ShareActivity_you_do_not_have_permission_to_send_to_this_group, Toast.LENGTH_SHORT).show();
callback.accept(false);
break;
}
}));
}
}

Wyświetl plik

@ -14,6 +14,8 @@ import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -26,6 +28,8 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import io.reactivex.rxjava3.core.Single;
public class ShareViewModel extends ViewModel {
private static final String TAG = Log.tag(ShareViewModel.class);
@ -61,14 +65,28 @@ public class ShareViewModel extends ViewModel {
return selectedContacts.getValue().size() > 1;
}
boolean onContactSelected(@NonNull ShareContact selectedContact) {
Set<ShareContact> contacts = new LinkedHashSet<>(selectedContacts.getValue());
if (contacts.add(selectedContact)) {
selectedContacts.setValue(contacts);
return true;
} else {
return false;
}
@NonNull Single<ContactSelectResult> onContactSelected(@NonNull ShareContact selectedContact) {
return Single.fromCallable(() -> {
if (selectedContact.getRecipientId().isPresent()) {
Recipient recipient = Recipient.resolved(selectedContact.getRecipientId().get());
if (recipient.isPushV2Group()) {
Optional<GroupDatabase.GroupRecord> record = DatabaseFactory.getGroupDatabase(context).getGroup(recipient.requireGroupId());
if (record.isPresent() && record.get().isAnnouncementGroup() && !record.get().isAdmin(Recipient.self())) {
return ContactSelectResult.FALSE_AND_SHOW_PERMISSION_TOAST;
}
}
}
Set<ShareContact> contacts = new LinkedHashSet<>(selectedContacts.getValue());
if (contacts.add(selectedContact)) {
selectedContacts.postValue(contacts);
return ContactSelectResult.TRUE;
} else {
return ContactSelectResult.FALSE;
}
});
}
void onContactDeselected(@NonNull ShareContact selectedContact) {
@ -141,12 +159,8 @@ public class ShareViewModel extends ViewModel {
}
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ShareViewModel());
}
enum ContactSelectResult {
TRUE, FALSE, FALSE_AND_SHOW_PERMISSION_TOAST
}
enum SmsShareRestriction {
@ -154,4 +168,12 @@ public class ShareViewModel extends ViewModel {
DISALLOW_SMS_CONTACTS,
DISALLOW_MULTI_SHARE
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ShareViewModel());
}
}
}

Wyświetl plik

@ -80,6 +80,7 @@ public final class FeatureFlags {
private static final String RETRY_RESPOND_MAX_AGE = "android.retryRespondMaxAge";
private static final String SENDER_KEY = "android.senderKey.3";
private static final String SUGGEST_SMS_BLACKLIST = "android.suggestSmsBlacklist";
private static final String ANNOUNCEMENT_GROUPS = "android.announcementGroups";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -113,7 +114,8 @@ public final class FeatureFlags {
RETRY_RECEIPT_LIFESPAN,
RETRY_RESPOND_MAX_AGE,
SENDER_KEY,
SUGGEST_SMS_BLACKLIST
SUGGEST_SMS_BLACKLIST,
ANNOUNCEMENT_GROUPS
);
@VisibleForTesting
@ -362,6 +364,11 @@ public final class FeatureFlags {
return getBoolean(SENDER_KEY, false);
}
/** Whether or not showing the announcement group setting in the UI is enabled . */
public static boolean announcementGroups() {
return getBoolean(ANNOUNCEMENT_GROUPS, false);
}
/** A comma-delimited list of country codes that should not be told about SMS during onboarding. */
public static @NonNull String suggestSmsBlacklist() {
return getString(SUGGEST_SMS_BLACKLIST, "");

Wyświetl plik

@ -0,0 +1,28 @@
package org.thoughtcrime.securesms.util
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
/**
* A lifecycle-aware [Disposable] that, after being bound to a lifecycle, will automatically dispose all contained disposables at the proper time.
*/
class LifecycleDisposable : DefaultLifecycleObserver {
val disposables: CompositeDisposable = CompositeDisposable()
fun bindTo(lifecycle: Lifecycle): LifecycleDisposable {
lifecycle.addObserver(this)
return this
}
fun add(disposable: Disposable): LifecycleDisposable {
disposables.add(disposable)
return this
}
override fun onDestroy(owner: LifecycleOwner) {
disposables.clear()
}
}

Wyświetl plik

@ -114,6 +114,44 @@ public final class SpanUtil {
return clickSubstring(learnMore, learnMore, onLearnMoreClicked, color);
}
/**
* Takes two resources:
* - one resource that has a single string placeholder
* - and another resource for a string you want to put in that placeholder with a click listener.
*
* Example:
*
* <string name="main_string">This is a %1$s string.</string>
* <string name="clickable_string">clickable</string>
*
* -> This is a clickable string.
* (where "clickable" is blue and will trigger the provided click listener when clicked)
*/
public static Spannable clickSubstring(@NonNull Context context, @StringRes int mainString, @StringRes int clickableString, @NonNull View.OnClickListener clickListener) {
String main = context.getString(mainString, SPAN_PLACE_HOLDER);
String clickable = context.getString(clickableString);
int start = main.indexOf(SPAN_PLACE_HOLDER);
int end = start + SPAN_PLACE_HOLDER.length();
Spannable spannable = new SpannableString(main.substring(0, start) + clickable + main.substring(end));
spannable.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
clickListener.onClick(widget);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
}
}, start, start + clickable.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
return spannable;
}
public static CharSequence clickSubstring(@NonNull Context context,
@NonNull CharSequence fullString,
@NonNull CharSequence substring,

Wyświetl plik

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="1000dp" />
<solid android:color="@color/signal_text_primary_disabled" />
<size
android:width="48dp"
android:height="2dp" />
</shape>

Wyświetl plik

@ -86,6 +86,12 @@
<include layout="@layout/conversation_input_panel" />
<ViewStub
android:id="@+id/conversation_cannot_send_announcement_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/conversation_cannot_send_announcement_group" />
<include layout="@layout/conversation_search_nav" />
<org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView

Wyświetl plik

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/signal_transparent_80"
android:gravity="center"
android:padding="17dp"
android:visibility="gone"
style="@style/Signal.Text.Preview"
tools:text="Only admins can send messages."
android:textColor="@color/signal_text_secondary" />

Wyświetl plik

@ -1,18 +1,20 @@
<?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:id="@+id/react_with_any_emoji_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
<ImageView
android:id="@+id/react_with_any_emoji_pull_bar"
android:layout_width="48dp"
android:layout_height="2dp"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:background="@color/signal_text_primary_disabled" />
android:layout_gravity="center_horizontal"
android:src="@drawable/bottom_sheet_handle"
tools:ignore="ContentDescription" />
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
android:id="@+id/react_with_any_emoji_search"

Wyświetl plik

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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:paddingTop="16dp">
<ImageView
android:id="@+id/show_admin_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/bottom_sheet_handle"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/show_admin_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:text="@string/ConversationActivity_message_an_admin"
style="@style/Signal.Text.Title.BottomSheet"
app:layout_constraintTop_toBottomOf="@id/show_admin_handle" />
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
android:id="@+id/show_admin_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintTop_toBottomOf="@id/show_admin_title"/>
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -245,6 +245,11 @@
<string name="ConversationActivity_attachment_exceeds_size_limits">Attachment exceeds size limits for the type of message you\'re sending.</string>
<string name="ConversationActivity_unable_to_record_audio">Unable to record audio!</string>
<string name="ConversationActivity_you_cant_send_messages_to_this_group">You can\'t send messages to this group because you\'re no longer a member.</string>
<string name="ConversationActivity_only_s_can_send_messages">Only %1$s can send messages.</string>
<string name="ConversationActivity_admins">admins</string>
<string name="ConversationActivity_message_an_admin">Message an admin</string>
<string name="ConversationActivity_cant_start_group_call">Can\'t start group call</string>
<string name="ConversationActivity_only_admins_of_this_group_can_start_a_call">Only admins of this group can start a call.</string>
<string name="ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device">There is no app available to handle this link on your device.</string>
<string name="ConversationActivity_your_request_to_join_has_been_sent_to_the_group_admin">Your request to join has been sent to the group admin. You\'ll be notified when they take action.</string>
<string name="ConversationActivity_cancel_request">Cancel Request</string>
@ -489,6 +494,7 @@
<!-- ShareActivity -->
<string name="ShareActivity_share_with">Share with</string>
<string name="ShareActivity_multiple_attachments_are_only_supported">Multiple attachments are only supported for images and videos</string>
<string name="ShareActivity_you_do_not_have_permission_to_send_to_this_group">You do not have permission to send to this group</string>
<!-- GcmRefreshJob -->
<string name="GcmRefreshJob_Permanent_Signal_communication_failure">Permanent Signal communication failure!</string>
@ -661,6 +667,7 @@
<!-- AddMembersActivity -->
<string name="AddMembersActivity__done">Done</string>
<string name="AddMembersActivity__this_person_cant_be_added_to_legacy_groups">This person can\'t be added to legacy groups.</string>
<string name="AddMembersActivity__this_person_cant_be_added_to_announcement_groups">This person can\'t be added to announcement groups.</string>
<plurals name="AddMembersActivity__add_d_members_to_s">
<item quantity="one">Add \"%1$s\" to \"%2$s\"?</item>
<item quantity="other">Add %3$d members to \"%2$s\"?</item>
@ -737,6 +744,7 @@
<string name="ManageGroupActivity_you_dont_have_the_rights_to_do_this">You don\'t have the rights to do this</string>
<string name="ManageGroupActivity_not_capable">Someone you added does not support new groups and needs to update Signal</string>
<string name="ManageGroupActivity_not_announcement_capable">Someone you added does not support announcement groups and needs to update Signal</string>
<string name="ManageGroupActivity_failed_to_update_the_group">Failed to update the group</string>
<string name="ManageGroupActivity_youre_not_a_member_of_the_group">You\'re not a member of the group</string>
<string name="ManageGroupActivity_failed_to_update_the_group_please_retry_later">Failed to update the group please retry later</string>
@ -1143,6 +1151,14 @@
<string name="MessageRecord_s_changed_who_can_edit_group_membership_to_s">%1$s changed who can edit group membership to \"%2$s\".</string>
<string name="MessageRecord_who_can_edit_group_membership_has_been_changed_to_s">Who can edit group membership has been changed to \"%1$s\".</string>
<!-- GV2 announcement group change -->
<string name="MessageRecord_you_allow_all_members_to_send">You changed the group settings to allow all members to send messages.</string>
<string name="MessageRecord_you_allow_only_admins_to_send">You changed the group settings to only allow admins to send messages.</string>
<string name="MessageRecord_s_allow_all_members_to_send">%1$s changed the group settings to allow all members to send messages.</string>
<string name="MessageRecord_s_allow_only_admins_to_send">%1$s changed the group settings to only allow admins to send messages.</string>
<string name="MessageRecord_allow_all_members_to_send">The group settings were changed to allow all members to send messages.</string>
<string name="MessageRecord_allow_only_admins_to_send">The group settings were changed to only allow admins to send messages.</string>
<!-- GV2 group link invite access level change -->
<string name="MessageRecord_you_turned_on_the_group_link_with_admin_approval_off">You turned on the group link with admin approval off.</string>
<string name="MessageRecord_you_turned_on_the_group_link_with_admin_approval_on">You turned on the group link with admin approval on.</string>
@ -3595,10 +3611,12 @@
<!-- PermissionsSettingsFragment -->
<string name="PermissionsSettingsFragment__add_members">Add members</string>
<string name="PermissionsSettingsFragment__edit_group_info">Edit group info</string>
<string name="PermissionsSettingsFragment__send_messages">Send messages</string>
<string name="PermissionsSettingsFragment__all_members">All members</string>
<string name="PermissionsSettingsFragment__only_admins">Only admins</string>
<string name="PermissionsSettingsFragment__who_can_add_new_members">Who can add new members?</string>
<string name="PermissionsSettingsFragment__who_can_edit_this_groups_info">Who can edit this group\'s info?</string>
<string name="PermissionsSettingsFragment__who_can_send_messages">Who can send messages?</string>
<!-- SoundsAndNotificationsSettingsFragment -->
<string name="SoundsAndNotificationsSettingsFragment__mute_notifications">Mute notifications</string>

Wyświetl plik

@ -187,4 +187,11 @@
<style name="Signal.Text.Title.SettingsBio">
</style>
<style name="Signal.Text.Title.BottomSheet" parent="">
<item name="android:textSize">16sp</item>
<item name="android:lineSpacingExtra">4sp</item>
<item name="android:fontFamily">sans-serif-medium</item>
<item name="android:letterSpacing" tools:ignore="NewApi">0.01</item>
</style>
</resources>

Wyświetl plik

@ -17,7 +17,7 @@ class GroupCapacityResultTest {
@Test
fun `Given an empty group, when I getRemainingCapacity, then I expect maximum capacity`() {
// GIVEN
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(), selectionLimits)
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(), selectionLimits, false)
// WHEN
val result = emptyGroupCapacityResult.getRemainingCapacity()
@ -29,7 +29,7 @@ class GroupCapacityResultTest {
@Test
fun `Given an empty group, when I getSelectionLimit, then I expect SELECTION_LIMIT`() {
// GIVEN
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(), selectionLimits)
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(), selectionLimits, false)
// WHEN
val result = emptyGroupCapacityResult.getSelectionLimit()
@ -41,7 +41,7 @@ class GroupCapacityResultTest {
@Test
fun `Given an empty group, when I getSelectionWarning, then I expect SELECTION_WARNING`() {
// GIVEN
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(), selectionLimits)
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(), selectionLimits, false)
// WHEN
val result = emptyGroupCapacityResult.getSelectionWarning()
@ -53,7 +53,7 @@ class GroupCapacityResultTest {
@Test
fun `Given a group only containing self, when I getSelectionLimit, then I expect SELECTION_LIMIT minus 1`() {
// GIVEN
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(SELF_ID), selectionLimits)
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(SELF_ID), selectionLimits, false)
// WHEN
val result = emptyGroupCapacityResult.getSelectionLimit()
@ -65,7 +65,7 @@ class GroupCapacityResultTest {
@Test
fun `Given a group only containing self, when I getSelectionWarning, then I expect SELECTION_WARNING minus 1`() {
// GIVEN
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(SELF_ID), selectionLimits)
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(SELF_ID), selectionLimits, false)
// WHEN
val result = emptyGroupCapacityResult.getSelectionWarning()
@ -78,7 +78,7 @@ class GroupCapacityResultTest {
fun `Given a group containing self and others, when I getMembers, then I expect all members including self`() {
// GIVEN
val allMembers: List<RecipientId> = (1L..10L).map { RecipientId.from(it) }
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, allMembers, selectionLimits)
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, allMembers, selectionLimits, false)
// WHEN
val result = emptyGroupCapacityResult.getMembers()
@ -91,7 +91,7 @@ class GroupCapacityResultTest {
fun `Given a group containing self and others, when I getMembersWithoutSelf, then I expect all members without self`() {
// GIVEN
val allMembers: List<RecipientId> = (1L..10L).map { RecipientId.from(it) }
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, allMembers, selectionLimits)
val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, allMembers, selectionLimits, false)
val expectedMembers = allMembers - SELF_ID
// WHEN

Wyświetl plik

@ -131,15 +131,19 @@ public class AccountAttributes {
@JsonProperty
private boolean senderKey;
@JsonProperty
private boolean announcementGroup;
@JsonCreator
public Capabilities() {}
public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey) {
this.uuid = uuid;
this.gv2 = gv2;
this.storage = storage;
this.gv1Migration = gv1Migration;
this.senderKey = senderKey;
public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup) {
this.uuid = uuid;
this.gv2 = gv2;
this.storage = storage;
this.gv1Migration = gv1Migration;
this.senderKey = senderKey;
this.announcementGroup = announcementGroup;
}
public boolean isUuid() {
@ -161,5 +165,9 @@ public class AccountAttributes {
public boolean isSenderKey() {
return senderKey;
}
public boolean isAnnouncementGroup() {
return announcementGroup;
}
}
}

Wyświetl plik

@ -38,4 +38,6 @@ public interface ChangeSetModifier {
void removePromoteRequestingMembers(int i);
void clearModifyDescription();
void clearModifyAnnouncementsOnly();
}

Wyświetl plik

@ -128,6 +128,11 @@ final class DecryptedGroupChangeActionsBuilderChangeSetModifier implements Chang
result.clearNewDescription();
}
@Override
public void clearModifyAnnouncementsOnly() {
result.clearNewIsAnnouncementGroup();
}
private static List<ByteString> removeIndexFromByteStringList(List<ByteString> byteStrings, int i) {
List<ByteString> modifiedList = new ArrayList<>(byteStrings);

Wyświetl plik

@ -12,6 +12,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.UuidUtil;
@ -276,6 +277,8 @@ public final class DecryptedGroupUtil {
applyModifyDescriptionAction(builder, change);
applyModifyIsAnnouncementGroupAction(builder, change);
applyModifyAvatarAction(builder, change);
applyModifyDisappearingMessagesTimerAction(builder, change);
@ -411,6 +414,13 @@ public final class DecryptedGroupUtil {
}
}
protected static void applyModifyIsAnnouncementGroupAction(DecryptedGroup.Builder builder, DecryptedGroupChange change) {
if (change.getNewIsAnnouncementGroup() != EnabledState.UNKNOWN) {
builder.setIsAnnouncementGroup(change.getNewIsAnnouncementGroup());
}
}
protected static void applyModifyAvatarAction(DecryptedGroup.Builder builder, DecryptedGroupChange change) {
if (change.hasNewAvatar()) {
builder.setAvatar(change.getNewAvatar().getValue());
@ -580,18 +590,22 @@ public final class DecryptedGroupUtil {
!change.hasNewTitle() && // field 10
!change.hasNewAvatar() && // field 11
!change.hasNewTimer() && // field 12
isSet(change.getNewAttributeAccess()) && // field 13
isSet(change.getNewMemberAccess()) && // field 14
isSet(change.getNewInviteLinkAccess()) && // field 15
isEmpty(change.getNewAttributeAccess()) && // field 13
isEmpty(change.getNewMemberAccess()) && // field 14
isEmpty(change.getNewInviteLinkAccess()) && // field 15
change.getNewRequestingMembersCount() == 0 && // field 16
change.getDeleteRequestingMembersCount() == 0 && // field 17
change.getPromoteRequestingMembersCount() == 0 && // field 18
change.getNewInviteLinkPassword().size() == 0 && // field 19
!change.hasNewDescription(); // field 20
!change.hasNewDescription() && // field 20
isEmpty(change.getNewIsAnnouncementGroup()); // field 20
}
static boolean isSet(AccessControl.AccessRequired newAttributeAccess) {
static boolean isEmpty(AccessControl.AccessRequired newAttributeAccess) {
return newAttributeAccess == AccessControl.AccessRequired.UNKNOWN;
}
static boolean isEmpty(EnabledState enabledState) {
return enabledState == EnabledState.UNKNOWN;
}
}

Wyświetl plik

@ -107,4 +107,9 @@ final class GroupChangeActionsBuilderChangeSetModifier implements ChangeSetModif
public void clearModifyDescription() {
result.clearModifyDescription();
}
@Override
public void clearModifyAnnouncementsOnly() {
result.clearModifyAnnouncementsOnly();
}
}

Wyświetl plik

@ -36,6 +36,10 @@ public final class GroupChangeReconstruct {
builder.setNewDescription(DecryptedString.newBuilder().setValue(toState.getDescription()));
}
if (!fromState.getIsAnnouncementGroup().equals(toState.getIsAnnouncementGroup())) {
builder.setNewIsAnnouncementGroup(toState.getIsAnnouncementGroup());
}
if (!fromState.getAvatar().equals(toState.getAvatar())) {
builder.setNewAvatar(DecryptedString.newBuilder().setValue(toState.getAvatar()));
}

Wyświetl plik

@ -11,6 +11,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.EnabledState;
import java.util.HashMap;
import java.util.List;
@ -41,7 +42,8 @@ public final class GroupChangeUtil {
change.getDeleteRequestingMembersCount() == 0 && // field 17
change.getPromoteRequestingMembersCount() == 0 && // field 18
!change.hasModifyInviteLinkPassword() && // field 19
!change.hasModifyDescription(); // field 20
!change.hasModifyDescription() && // field 20
!change.hasModifyAnnouncementsOnly(); // field 21
}
/**
@ -135,6 +137,7 @@ public final class GroupChangeUtil {
resolveField17DeleteMembers (conflictingChange, changeSetModifier, requestingMembersByUuid);
resolveField18PromoteRequestingMembers (conflictingChange, changeSetModifier, requestingMembersByUuid);
resolveField20ModifyDescription (groupState, conflictingChange, changeSetModifier);
resolveField21ModifyAnnouncementsOnly (groupState, conflictingChange, changeSetModifier);
}
private static void resolveField3AddMembers(DecryptedGroupChange conflictingChange, ChangeSetModifier result, HashMap<ByteString, DecryptedMember> fullMembersByUuid, HashMap<ByteString, DecryptedPendingMember> pendingMembersByUuid) {
@ -310,4 +313,10 @@ public final class GroupChangeUtil {
result.clearModifyDescription();
}
}
private static void resolveField21ModifyAnnouncementsOnly(DecryptedGroup groupState, DecryptedGroupChange conflictingChange, ChangeSetModifier result) {
if (conflictingChange.getNewIsAnnouncementGroup().equals(groupState.getIsAnnouncementGroup())) {
result.clearModifyAnnouncementsOnly();
}
}
}

Wyświetl plik

@ -22,6 +22,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemov
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.NotarySignature;
import org.signal.zkgroup.ServerPublicParams;
@ -59,7 +60,7 @@ public final class GroupsV2Operations {
public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID;
/** Highest change epoch this class knows now to decrypt */
public static final int HIGHEST_KNOWN_EPOCH = 2;
public static final int HIGHEST_KNOWN_EPOCH = 3;
private final ServerPublicParams serverPublicParams;
private final ClientZkProfileOperations clientZkProfileOperations;
@ -149,10 +150,14 @@ public final class GroupsV2Operations {
.setTitle(encryptTitle(title)));
}
public GroupChange.Actions.ModifyDescriptionAction.Builder createModifyGroupDescription(final String description) {
public GroupChange.Actions.ModifyDescriptionAction.Builder createModifyGroupDescriptionAction(final String description) {
return GroupChange.Actions.ModifyDescriptionAction.newBuilder().setDescription(encryptDescription(description));
}
public GroupChange.Actions.Builder createModifyGroupDescription(final String description) {
return GroupChange.Actions.newBuilder().setModifyDescription(createModifyGroupDescriptionAction(description));
}
public GroupChange.Actions.Builder createModifyGroupMembershipChange(Set<GroupCandidate> membersToAdd, UUID selfUuid) {
final GroupOperations groupOperations = forGroup(groupSecretParams);
@ -330,6 +335,14 @@ public final class GroupsV2Operations {
.setAttributesAccess(newRights));
}
public GroupChange.Actions.Builder createAnnouncementGroupChange(boolean isAnnouncementGroup) {
return GroupChange.Actions
.newBuilder()
.setModifyAnnouncementsOnly(GroupChange.Actions.ModifyAnnouncementsOnlyAction
.newBuilder()
.setAnnouncementsOnly(isAnnouncementGroup));
}
private Member.Builder member(ProfileKeyCredential credential, Member.Role role) {
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(new SecureRandom(), groupSecretParams, credential);
@ -386,6 +399,7 @@ public final class GroupsV2Operations {
return DecryptedGroup.newBuilder()
.setTitle(decryptTitle(group.getTitle()))
.setDescription(decryptDescription(group.getDescription()))
.setIsAnnouncementGroup(group.getAnnouncementsOnly() ? EnabledState.ENABLED : EnabledState.DISABLED)
.setAvatar(group.getAvatar())
.setAccessControl(group.getAccessControl())
.setRevision(group.getRevision())
@ -575,6 +589,11 @@ public final class GroupsV2Operations {
builder.setNewDescription(DecryptedString.newBuilder().setValue(decryptDescription(actions.getModifyDescription().getDescription())));
}
// Field 21
if (actions.hasModifyAnnouncementsOnly()) {
builder.setNewIsAnnouncementGroup(actions.getModifyAnnouncementsOnly().getAnnouncementsOnly() ? EnabledState.ENABLED : EnabledState.DISABLED);
}
return builder.build();
}

Wyświetl plik

@ -121,8 +121,12 @@ public class SignalServiceProfile {
@JsonProperty("gv1-migration")
private boolean gv1Migration;
@JsonProperty
private boolean senderKey;
@JsonProperty
private boolean announcementGroup;
@JsonCreator
public Capabilities() {}
@ -141,6 +145,10 @@ public class SignalServiceProfile {
public boolean isSenderKey() {
return senderKey;
}
public boolean isAnnouncementGroup() {
return announcementGroup;
}
}
public ProfileKeyCredentialResponse getProfileKeyCredentialResponse() {

Wyświetl plik

@ -61,6 +61,7 @@ message DecryptedGroup {
repeated DecryptedRequestingMember requestingMembers = 9;
bytes inviteLinkPassword = 10;
string description = 11;
EnabledState isAnnouncementGroup = 12;
}
// Decrypted version of message GroupChange.Actions
@ -86,6 +87,7 @@ message DecryptedGroupChange {
repeated DecryptedApproveMember promoteRequestingMembers = 18;
bytes newInviteLinkPassword = 19;
DecryptedString newDescription = 20;
EnabledState newIsAnnouncementGroup = 21;
}
message DecryptedString {
@ -104,4 +106,12 @@ message DecryptedGroupJoinInfo {
uint32 revision = 6;
bool pendingAdminApproval = 7;
string description = 8;
bool isAnnouncementGroup = 9;
}
enum EnabledState {
UNKNOWN = 0;
ENABLED = 1;
DISABLED = 2;
}

Wyświetl plik

@ -71,6 +71,7 @@ message Group {
repeated RequestingMember requestingMembers = 9;
bytes inviteLinkPassword = 10;
bytes description = 11;
bool announcementsOnly = 12;
}
message GroupChange {
@ -145,11 +146,15 @@ message GroupChange {
}
message ModifyAddFromInviteLinkAccessControlAction {
AccessControl.AccessRequired addFromInviteLinkAccess = 1;
AccessControl.AccessRequired addFromInviteLinkAccess = 1;
}
message ModifyInviteLinkPasswordAction {
bytes inviteLinkPassword = 1;
bytes inviteLinkPassword = 1;
}
message ModifyAnnouncementsOnlyAction {
bool announcementsOnly = 1;
}
bytes sourceUuid = 1;
@ -172,6 +177,7 @@ message GroupChange {
repeated PromoteRequestingMemberAction promoteRequestingMembers = 18;
ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19;
ModifyDescriptionAction modifyDescription = 20;
ModifyAnnouncementsOnlyAction modifyAnnouncementsOnly = 21;
}
bytes actions = 1;

Wyświetl plik

@ -16,7 +16,7 @@ public final class AccountAttributesTest {
"reglock1234",
new byte[10],
false,
new AccountAttributes.Capabilities(true, true, true, true, true),
new AccountAttributes.Capabilities(true, true, true, true, true, true),
false));
assertEquals("{\"signalingKey\":\"skey\"," +
"\"registrationId\":123," +
@ -28,19 +28,18 @@ public final class AccountAttributesTest {
"\"unidentifiedAccessKey\":\"AAAAAAAAAAAAAA==\"," +
"\"unrestrictedUnidentifiedAccess\":false," +
"\"discoverableByPhoneNumber\":false," +
"\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"gv2-3\":true,\"gv1-migration\":true}}", json);
"\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"announcementGroup\":true,\"gv2-3\":true,\"gv1-migration\":true}}", json);
}
@Test
public void gv2_true() {
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false, false));
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"gv2-3\":true,\"gv1-migration\":false}", json);
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false, false, false));
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"gv2-3\":true,\"gv1-migration\":false}", json);
}
@Test
public void gv2_false() {
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false, false));
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"gv2-3\":false,\"gv1-migration\":false}", json);
String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false, false, false));
assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"gv2-3\":false,\"gv1-migration\":false}", json);
}
}

Wyświetl plik

@ -15,6 +15,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemov
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
@ -45,7 +46,7 @@ public final class DecryptedGroupUtil_apply_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
20, maxFieldFound);
21, maxFieldFound);
}
@Test
@ -594,6 +595,24 @@ public final class DecryptedGroupUtil_apply_Test {
newGroup);
}
@Test
public void isAnnouncementGroup() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.setIsAnnouncementGroup(EnabledState.DISABLED)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(11)
.setNewIsAnnouncementGroup(EnabledState.ENABLED)
.build());
assertEquals(DecryptedGroup.newBuilder()
.setRevision(11)
.setIsAnnouncementGroup(EnabledState.ENABLED)
.build(),
newGroup);
}
@Test
public void avatar() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()

Wyświetl plik

@ -9,6 +9,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID;
@ -35,7 +36,7 @@ public final class DecryptedGroupUtil_empty_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
20, maxFieldFound);
21, maxFieldFound);
}
@Test
@ -222,4 +223,14 @@ public final class DecryptedGroupUtil_empty_Test {
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_announcement_field_21() {
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
.setNewIsAnnouncementGroup(EnabledState.ENABLED)
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
}

Wyświetl plik

@ -8,6 +8,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
@ -41,7 +42,7 @@ public final class GroupChangeReconstructTest {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class);
assertEquals("GroupChangeReconstruct and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(),
11, maxFieldFound);
12, maxFieldFound);
}
@Test
@ -84,6 +85,16 @@ public final class GroupChangeReconstructTest {
assertEquals(DecryptedGroupChange.newBuilder().setNewDescription(DecryptedString.newBuilder().setValue("B")).build(), decryptedGroupChange);
}
@Test
public void announcement_group_change() {
DecryptedGroup from = DecryptedGroup.newBuilder().setIsAnnouncementGroup(EnabledState.DISABLED).build();
DecryptedGroup to = DecryptedGroup.newBuilder().setIsAnnouncementGroup(EnabledState.ENABLED).build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(DecryptedGroupChange.newBuilder().setNewIsAnnouncementGroup(EnabledState.ENABLED).build(), decryptedGroupChange);
}
@Test
public void avatar_change() {
DecryptedGroup from = DecryptedGroup.newBuilder().setAvatar("A").build();

Wyświetl plik

@ -20,7 +20,7 @@ public final class GroupChangeUtil_changeIsEmpty_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(GroupChange.Actions.class);
assertEquals("GroupChangeUtil and its tests need updating to account for new fields on " + GroupChange.Actions.class.getName(),
20, maxFieldFound);
21, maxFieldFound);
}
@Test
@ -189,4 +189,13 @@ public final class GroupChangeUtil_changeIsEmpty_Test {
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_description_field_21() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.setModifyAnnouncementsOnly(GroupChange.Actions.ModifyAnnouncementsOnlyAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
}

Wyświetl plik

@ -11,6 +11,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
@ -46,7 +47,7 @@ public final class GroupChangeUtil_resolveConflict_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
20, maxFieldFound);
21, maxFieldFound);
}
/**
@ -59,7 +60,7 @@ public final class GroupChangeUtil_resolveConflict_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + GroupChange.class.getName(),
20, maxFieldFound);
21, maxFieldFound);
}
/**
@ -72,7 +73,7 @@ public final class GroupChangeUtil_resolveConflict_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(),
11, maxFieldFound);
12, maxFieldFound);
}
@ -706,4 +707,38 @@ public final class GroupChangeUtil_resolveConflict_Test {
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_21__announcement_change_is_preserved() {
DecryptedGroup groupState = DecryptedGroup.newBuilder()
.setIsAnnouncementGroup(EnabledState.DISABLED)
.build();
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
.setNewIsAnnouncementGroup(EnabledState.ENABLED)
.build();
GroupChange.Actions change = GroupChange.Actions.newBuilder()
.setModifyAnnouncementsOnly(GroupChange.Actions.ModifyAnnouncementsOnlyAction.newBuilder().setAnnouncementsOnly(true))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertEquals(change, resolvedActions);
}
@Test
public void field_21__announcement_change_is_removed() {
DecryptedGroup groupState = DecryptedGroup.newBuilder()
.setIsAnnouncementGroup(EnabledState.ENABLED)
.build();
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
.setNewIsAnnouncementGroup(EnabledState.ENABLED)
.build();
GroupChange.Actions change = GroupChange.Actions.newBuilder()
.setModifyAnnouncementsOnly(GroupChange.Actions.ModifyAnnouncementsOnlyAction.newBuilder().setAnnouncementsOnly(true))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
}

Wyświetl plik

@ -8,6 +8,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
@ -39,7 +40,7 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
20, maxFieldFound);
21, maxFieldFound);
}
/**
@ -52,7 +53,7 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(),
11, maxFieldFound);
12, maxFieldFound);
}
@ -571,4 +572,32 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test {
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
}
@Test
public void field_21__announcement_change_is_preserved() {
DecryptedGroup groupState = DecryptedGroup.newBuilder()
.setIsAnnouncementGroup(EnabledState.DISABLED)
.build();
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
.setNewIsAnnouncementGroup(EnabledState.ENABLED)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertEquals(decryptedChange, resolvedChanges);
}
@Test
public void field_21__no_announcement_change_is_removed() {
DecryptedGroup groupState = DecryptedGroup.newBuilder()
.setIsAnnouncementGroup(EnabledState.ENABLED)
.build();
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
.setNewIsAnnouncementGroup(EnabledState.ENABLED)
.build();
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
}
}

Wyświetl plik

@ -17,6 +17,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemov
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.GroupMasterKey;
@ -40,6 +41,7 @@ import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupsV2Operations_decrypt_change_Test {
@ -57,7 +59,15 @@ public final class GroupsV2Operations_decrypt_change_Test {
clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
groupOperations = new GroupsV2Operations(clientZkOperations).forGroup(groupSecretParams);
}
@Test
public void ensure_GroupV2Operations_decryptChange_knows_about_all_fields_of_DecryptedGroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupV2Operations#decryptChange and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
21, maxFieldFound);
}
@Test
public void cannot_decrypt_change_with_epoch_higher_than_known() throws InvalidProtocolBufferException, VerificationFailedException, InvalidGroupStateException {
GroupChange change = GroupChange.newBuilder()
@ -361,6 +371,22 @@ public final class GroupsV2Operations_decrypt_change_Test {
.setNewInviteLinkPassword(ByteString.copyFrom(newPassword)));
}
@Test
public void can_pass_through_new_description_field20() {
assertDecryption(groupOperations.createModifyGroupDescription("New Description"),
DecryptedGroupChange.newBuilder()
.setNewDescription(DecryptedString.newBuilder().setValue("New Description").build()));
}
@Test
public void can_pass_through_new_announcment_only_field21() {
assertDecryption(GroupChange.Actions.newBuilder()
.setModifyAnnouncementsOnly(GroupChange.Actions.ModifyAnnouncementsOnlyAction.newBuilder()
.setAnnouncementsOnly(true)),
DecryptedGroupChange.newBuilder()
.setNewIsAnnouncementGroup(EnabledState.ENABLED));
}
private static ProfileKey newProfileKey() {
try {
return new ProfileKey(Util.getSecretBytes(32));

Wyświetl plik

@ -21,6 +21,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemov
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.ClientZkGroupCipher;
@ -74,7 +75,7 @@ public final class GroupsV2Operations_decrypt_group_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(Group.class);
assertEquals("GroupOperations and its tests need updating to account for new fields on " + Group.class.getName(),
11, maxFieldFound);
12, maxFieldFound);
}
@Test
@ -283,6 +284,17 @@ public final class GroupsV2Operations_decrypt_group_Test {
assertEquals("Description!", decryptedGroup.getDescription());
}
@Test
public void decrypt_announcements_field_12() throws VerificationFailedException, InvalidGroupStateException {
Group group = Group.newBuilder()
.setAnnouncementsOnly(true)
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(EnabledState.ENABLED, decryptedGroup.getIsAnnouncementGroup());
}
private ByteString encryptProfileKey(UUID uuid, ProfileKey profileKey) {
return ByteString.copyFrom(new ClientZkGroupCipher(groupSecretParams).encryptProfileKey(profileKey, uuid).serialize());
}