kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add support for announcement groups.
rodzic
1a56924a56
commit
25234496bf
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -15,6 +15,7 @@ sealed class ConversationSettingsEvent {
|
|||
val groupId: GroupId,
|
||||
val selectionWarning: Int,
|
||||
val selectionLimit: Int,
|
||||
val isAnnouncementGroup: Boolean,
|
||||
val groupMembersWithoutSelf: List<RecipientId>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -43,6 +43,7 @@ import android.text.Spannable;
|
|||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
|
@ -86,6 +87,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
|
@ -392,6 +394,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
private InputPanel inputPanel;
|
||||
private View panelParent;
|
||||
private View noLongerMemberBanner;
|
||||
private Stub<TextView> cannotSendInAnnouncementGroupBanner;
|
||||
private View requestingMemberBanner;
|
||||
private View cancelJoinRequest;
|
||||
private Stub<View> mentionsSuggestions;
|
||||
|
@ -419,8 +422,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
private long threadId;
|
||||
private int distributionType;
|
||||
private boolean isSecureText;
|
||||
private boolean isDefaultSms;
|
||||
private int reactWithAnyEmojiStartPage = -1;
|
||||
private boolean isDefaultSms = true;
|
||||
private boolean isMmsEnabled = true;
|
||||
private boolean isSecurityInitialized = false;
|
||||
private boolean isSearchRequested = false;
|
||||
|
@ -446,7 +449,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
isDefaultSms = Util.isDefaultSmsProvider(this);
|
||||
voiceNoteMediaController = new VoiceNoteMediaController(this);
|
||||
voiceRecorderWakeLock = new VoiceRecorderWakeLock(this);
|
||||
|
||||
|
@ -1014,7 +1017,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
searchViewModel.onSearchOpened();
|
||||
searchNav.setVisibility(View.VISIBLE);
|
||||
searchNav.setData(0, 0);
|
||||
inputPanel.setVisibility(View.GONE);
|
||||
inputPanel.setHideForSearch(true);
|
||||
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
if (!menu.getItem(i).equals(searchViewItem)) {
|
||||
|
@ -1030,7 +1033,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
isSearchRequested = false;
|
||||
searchViewModel.onSearchClosed();
|
||||
searchNav.setVisibility(View.GONE);
|
||||
inputPanel.setVisibility(View.VISIBLE);
|
||||
inputPanel.setHideForSearch(false);
|
||||
fragment.onSearchQueryUpdated(null);
|
||||
setBlockedUserState(recipient.get(), isSecureText, isDefaultSms);
|
||||
invalidateOptionsMenu();
|
||||
|
@ -1424,7 +1427,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
private void handleVideo(final Recipient recipient) {
|
||||
if (recipient == null) return;
|
||||
|
||||
CommunicationActions.startVideoCall(this, recipient);
|
||||
if (recipient.isPushV2Group() && groupViewModel.isNonAdminInAnnouncementGroup()) {
|
||||
new MaterialAlertDialogBuilder(this).setTitle(R.string.ConversationActivity_cant_start_group_call)
|
||||
.setMessage(R.string.ConversationActivity_only_admins_of_this_group_can_start_a_call)
|
||||
.setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss())
|
||||
.show();
|
||||
} else {
|
||||
CommunicationActions.startVideoCall(this, recipient);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisplayGroupRecipients() {
|
||||
|
@ -1621,17 +1631,20 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
|
||||
private void initializeEnabledCheck() {
|
||||
groupViewModel.getSelfMemberLevel().observe(this, selfMemberShip -> {
|
||||
groupViewModel.getSelfMemberLevel().observe(this, selfMembership -> {
|
||||
boolean canSendMessages;
|
||||
boolean leftGroup;
|
||||
boolean canCancelRequest;
|
||||
|
||||
if (selfMemberShip == null) {
|
||||
if (selfMembership == null) {
|
||||
leftGroup = false;
|
||||
canSendMessages = true;
|
||||
canCancelRequest = false;
|
||||
if (cannotSendInAnnouncementGroupBanner.resolved()) {
|
||||
cannotSendInAnnouncementGroupBanner.get().setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
switch (selfMemberShip) {
|
||||
switch (selfMembership.getMemberLevel()) {
|
||||
case NOT_A_MEMBER:
|
||||
leftGroup = true;
|
||||
canSendMessages = false;
|
||||
|
@ -1656,10 +1669,22 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
if (selfMembership.isAnnouncementGroup() && selfMembership.getMemberLevel() != GroupDatabase.MemberLevel.ADMINISTRATOR) {
|
||||
canSendMessages = false;
|
||||
cannotSendInAnnouncementGroupBanner.get().setVisibility(View.VISIBLE);
|
||||
cannotSendInAnnouncementGroupBanner.get().setMovementMethod(LinkMovementMethod.getInstance());
|
||||
cannotSendInAnnouncementGroupBanner.get().setText(SpanUtil.clickSubstring(this, R.string.ConversationActivity_only_s_can_send_messages, R.string.ConversationActivity_admins, v -> {
|
||||
ShowAdminsBottomSheetDialog.show(getSupportFragmentManager(), getRecipient().requireGroupId().requireV2());
|
||||
}));
|
||||
} else if (cannotSendInAnnouncementGroupBanner.resolved()) {
|
||||
cannotSendInAnnouncementGroupBanner.get().setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
noLongerMemberBanner.setVisibility(leftGroup ? View.VISIBLE : View.GONE);
|
||||
requestingMemberBanner.setVisibility(canCancelRequest ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (canCancelRequest) {
|
||||
cancelJoinRequest.setOnClickListener(v -> ConversationGroupViewModel.onCancelJoinRequest(getRecipient(), new AsynchronousCallback.MainThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
|
@ -1675,7 +1700,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}.toWorkerCallback()));
|
||||
}
|
||||
|
||||
inputPanel.setVisibility(canSendMessages ? View.VISIBLE : View.GONE);
|
||||
inputPanel.setHideForGroupState(!canSendMessages);
|
||||
inputPanel.setEnabled(canSendMessages);
|
||||
sendButton.setEnabled(canSendMessages);
|
||||
attachButton.setEnabled(canSendMessages);
|
||||
|
@ -2012,10 +2037,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
reactionDelegate = new ConversationReactionDelegate(reactionOverlayStub);
|
||||
|
||||
|
||||
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
|
||||
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
|
||||
cancelJoinRequest = findViewById(R.id.conversation_cancel_request);
|
||||
joinGroupCallButton = findViewById(R.id.conversation_group_call_join);
|
||||
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
|
||||
cannotSendInAnnouncementGroupBanner = ViewUtil.findStubById(this, R.id.conversation_cannot_send_announcement_stub);
|
||||
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
|
||||
cancelJoinRequest = findViewById(R.id.conversation_cancel_request);
|
||||
joinGroupCallButton = findViewById(R.id.conversation_group_call_join);
|
||||
|
||||
container.setIsBubble(isInBubble());
|
||||
container.addOnKeyboardShownListener(this);
|
||||
|
@ -2678,17 +2704,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) {
|
||||
if (!isSecureText && isPushGroupConversation()) {
|
||||
unblockButton.setVisibility(View.GONE);
|
||||
inputPanel.setVisibility(View.GONE);
|
||||
inputPanel.setHideForBlockedState(true);
|
||||
makeDefaultSmsButton.setVisibility(View.GONE);
|
||||
registerButton.setVisibility(View.VISIBLE);
|
||||
} else if (!isSecureText && !isDefaultSms && recipient.hasSmsAddress()) {
|
||||
unblockButton.setVisibility(View.GONE);
|
||||
inputPanel.setVisibility(View.GONE);
|
||||
inputPanel.setHideForBlockedState(true);
|
||||
makeDefaultSmsButton.setVisibility(View.VISIBLE);
|
||||
registerButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
boolean inactivePushGroup = isPushGroupConversation() && !recipient.isActiveGroup();
|
||||
inputPanel.setVisibility(inactivePushGroup ? View.GONE : View.VISIBLE);
|
||||
inputPanel.setHideForBlockedState(inactivePushGroup);
|
||||
unblockButton.setVisibility(View.GONE);
|
||||
makeDefaultSmsButton.setVisibility(View.GONE);
|
||||
registerButton.setVisibility(View.GONE);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -32,20 +31,18 @@ import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
final class ConversationGroupViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Recipient> liveRecipient;
|
||||
private final LiveData<GroupActiveState> groupActiveState;
|
||||
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
|
||||
private final LiveData<ConversationMemberLevel> selfMembershipLevel;
|
||||
private final LiveData<Integer> actionableRequestingMembers;
|
||||
private final LiveData<ReviewState> reviewState;
|
||||
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
|
||||
|
@ -75,7 +72,6 @@ final class ConversationGroupViewModel extends ViewModel {
|
|||
(record, dups) -> dups.isEmpty()
|
||||
? ReviewState.EMPTY
|
||||
: new ReviewState(record.getId().requireV2(), dups.get(0), dups.size()));
|
||||
|
||||
}
|
||||
|
||||
void onRecipientChange(Recipient recipient) {
|
||||
|
@ -102,7 +98,7 @@ final class ConversationGroupViewModel extends ViewModel {
|
|||
return groupActiveState;
|
||||
}
|
||||
|
||||
LiveData<GroupDatabase.MemberLevel> getSelfMemberLevel() {
|
||||
LiveData<ConversationMemberLevel> getSelfMemberLevel() {
|
||||
return selfMembershipLevel;
|
||||
}
|
||||
|
||||
|
@ -114,6 +110,11 @@ final class ConversationGroupViewModel extends ViewModel {
|
|||
return gv1MigrationSuggestions;
|
||||
}
|
||||
|
||||
boolean isNonAdminInAnnouncementGroup() {
|
||||
ConversationMemberLevel level = selfMembershipLevel.getValue();
|
||||
return level != null && level.getMemberLevel() != GroupDatabase.MemberLevel.ADMINISTRATOR && level.isAnnouncementGroup();
|
||||
}
|
||||
|
||||
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
|
||||
if (recipient != null && recipient.isGroup()) {
|
||||
Application context = ApplicationDependencies.getApplication();
|
||||
|
@ -144,11 +145,11 @@ final class ConversationGroupViewModel extends ViewModel {
|
|||
return new GroupActiveState(record.isActive(), record.isV2Group());
|
||||
}
|
||||
|
||||
private static GroupDatabase.MemberLevel mapToSelfMembershipLevel(@Nullable GroupRecord record) {
|
||||
private static ConversationMemberLevel mapToSelfMembershipLevel(@Nullable GroupRecord record) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
return record.memberLevel(Recipient.self());
|
||||
return new ConversationMemberLevel(record.memberLevel(Recipient.self()), record.isAnnouncementGroup());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -257,6 +258,24 @@ final class ConversationGroupViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
static final class ConversationMemberLevel {
|
||||
private final GroupDatabase.MemberLevel memberLevel;
|
||||
private final boolean isAnnouncementGroup;
|
||||
|
||||
private ConversationMemberLevel(GroupDatabase.MemberLevel memberLevel, boolean isAnnouncementGroup) {
|
||||
this.memberLevel = memberLevel;
|
||||
this.isAnnouncementGroup = isAnnouncementGroup;
|
||||
}
|
||||
|
||||
public @NonNull GroupDatabase.MemberLevel getMemberLevel() {
|
||||
return memberLevel;
|
||||
}
|
||||
|
||||
public boolean isAnnouncementGroup() {
|
||||
return isAnnouncementGroup;
|
||||
}
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Renders a list of admins for a specified groupId. Tapping on one will allow you to send them a message.
|
||||
*/
|
||||
public final class ShowAdminsBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
public static void show(@NonNull FragmentManager manager, @NonNull GroupId.V2 groupId) {
|
||||
ShowAdminsBottomSheetDialog fragment = new ShowAdminsBottomSheetDialog();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(KEY_GROUP_ID, ParcelableGroupId.from(groupId));
|
||||
fragment.setArguments(args);
|
||||
|
||||
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Signal_DayNight_BottomSheet_Rounded);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.show_admin_bottom_sheet, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
|
||||
|
||||
GroupMemberListView list = view.findViewById(R.id.show_admin_list);
|
||||
list.setDisplayOnlyMembers(Collections.emptyList());
|
||||
|
||||
list.setRecipientClickListener(recipient -> {
|
||||
CommunicationActions.startConversation(requireContext(), recipient, null);
|
||||
dismissAllowingStateLoss();
|
||||
});
|
||||
|
||||
disposables.add(Single.fromCallable(() -> getAdmins(requireContext().getApplicationContext(), getGroupId()))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(list::setDisplayOnlyMembers));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
|
||||
BottomSheetUtil.show(manager, tag, this);
|
||||
}
|
||||
|
||||
private GroupId getGroupId() {
|
||||
return ParcelableGroupId.get(requireArguments().getParcelable(KEY_GROUP_ID));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static @NonNull List<Recipient> getAdmins(@NonNull Context context, @NonNull GroupId groupId) {
|
||||
return DatabaseFactory.getGroupDatabase(context)
|
||||
.getGroup(groupId)
|
||||
.transform(GroupDatabase.GroupRecord::getAdmins)
|
||||
.or(Collections.emptyList());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, "");
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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" />
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,4 +38,6 @@ public interface ChangeSetModifier {
|
|||
void removePromoteRequestingMembers(int i);
|
||||
|
||||
void clearModifyDescription();
|
||||
|
||||
void clearModifyAnnouncementsOnly();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,4 +107,9 @@ final class GroupChangeActionsBuilderChangeSetModifier implements ChangeSetModif
|
|||
public void clearModifyDescription() {
|
||||
result.clearModifyDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearModifyAnnouncementsOnly() {
|
||||
result.clearModifyAnnouncementsOnly();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue