kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add support for Group V2 description field.
rodzic
b3aec58e69
commit
8c9df8d3be
|
@ -330,7 +330,7 @@ dependencies {
|
|||
force = true
|
||||
}
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
|
@ -527,6 +527,10 @@ task signProductionWebsiteRelease {
|
|||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return System.currentTimeMillis().toString()
|
||||
}
|
||||
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
def result = exec {
|
||||
executable = 'git'
|
||||
|
@ -539,6 +543,10 @@ def getLastCommitTimestamp() {
|
|||
}
|
||||
|
||||
def getGitHash() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return "abcd1234"
|
||||
}
|
||||
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||
|
|
|
@ -8,10 +8,8 @@ import androidx.annotation.Nullable;
|
|||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import org.thoughtcrime.securesms.components.MaskView;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
|
@ -78,6 +76,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable {
|
|||
void onEnableCallNotificationsClicked();
|
||||
void onPlayInlineContent(ConversationMessage conversationMessage);
|
||||
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
|
||||
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.res.ColorStateList;
|
|||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
@ -87,6 +88,7 @@ public class ConversationBannerView extends ConstraintLayout {
|
|||
|
||||
public void setDescription(@Nullable CharSequence description) {
|
||||
contactDescription.setText(description);
|
||||
contactDescription.setVisibility(TextUtils.isEmpty(description) ? GONE : VISIBLE);
|
||||
}
|
||||
|
||||
public void showBackgroundBubble(boolean enabled) {
|
||||
|
@ -109,6 +111,10 @@ public class ConversationBannerView extends ConstraintLayout {
|
|||
contactDescription.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setLinkifyDescription(boolean enable) {
|
||||
contactDescription.setMovementMethod(enable ? LinkMovementMethod.getInstance() : null);
|
||||
}
|
||||
|
||||
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
|
||||
|
|
|
@ -107,7 +107,9 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionRecycler;
|
|||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
|
@ -492,7 +494,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
});
|
||||
}
|
||||
|
||||
private static void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationBannerView conversationBanner) {
|
||||
private void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationBannerView conversationBanner) {
|
||||
if (conversationBanner == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -536,7 +538,20 @@ public class ConversationFragment extends LoggingFragment {
|
|||
}
|
||||
|
||||
if (groups.isEmpty() || isSelf) {
|
||||
conversationBanner.hideDescription();
|
||||
if (TextUtils.isEmpty(recipientInfo.getGroupDescription())) {
|
||||
conversationBanner.setLinkifyDescription(false);
|
||||
conversationBanner.hideDescription();
|
||||
} else {
|
||||
conversationBanner.setLinkifyDescription(true);
|
||||
boolean linkifyWebLinks = recipientInfo.getMessageRequestState() == MessageRequestState.NONE;
|
||||
conversationBanner.setDescription(GroupDescriptionUtil.style(context,
|
||||
recipientInfo.getGroupDescription(),
|
||||
linkifyWebLinks,
|
||||
() -> GroupDescriptionDialog.show(getChildFragmentManager(),
|
||||
recipient.getDisplayName(context),
|
||||
recipientInfo.getGroupDescription(),
|
||||
linkifyWebLinks)));
|
||||
}
|
||||
} else {
|
||||
final String description;
|
||||
|
||||
|
@ -1630,6 +1645,13 @@ public class ConversationFragment extends LoggingFragment {
|
|||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted) {
|
||||
if (groupId != null) {
|
||||
GroupDescriptionDialog.show(getChildFragmentManager(), groupId, description, isMessageRequestAccepted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshList() {
|
||||
|
|
|
@ -67,6 +67,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
private Recipient conversationRecipient;
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
private MessageRecord messageRecord;
|
||||
private boolean isMessageRequestAccepted;
|
||||
private LiveData<SpannableString> displayBody;
|
||||
private EventListener eventListener;
|
||||
|
||||
|
@ -112,7 +113,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
{
|
||||
this.batchSelected = batchSelected;
|
||||
|
||||
bind(lifecycleOwner, conversationMessage, previousMessageRecord, nextMessageRecord, conversationRecipient, hasWallpaper);
|
||||
bind(lifecycleOwner, conversationMessage, previousMessageRecord, nextMessageRecord, conversationRecipient, hasWallpaper, isMessageRequestAccepted);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -130,12 +131,14 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
boolean hasWallpaper)
|
||||
boolean hasWallpaper,
|
||||
boolean isMessageRequestAccepted)
|
||||
{
|
||||
this.conversationMessage = conversationMessage;
|
||||
this.messageRecord = conversationMessage.getMessageRecord();
|
||||
this.nextMessageRecord = nextMessageRecord;
|
||||
this.conversationRecipient = conversationRecipient;
|
||||
this.conversationMessage = conversationMessage;
|
||||
this.messageRecord = conversationMessage.getMessageRecord();
|
||||
this.nextMessageRecord = nextMessageRecord;
|
||||
this.conversationRecipient = conversationRecipient;
|
||||
this.isMessageRequestAccepted = isMessageRequestAccepted;
|
||||
|
||||
senderObserver.observe(lifecycleOwner, messageRecord.getIndividualRecipient());
|
||||
|
||||
|
@ -164,7 +167,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
|
||||
observeDisplayBody(lifecycleOwner, spannableMessage);
|
||||
|
||||
present(conversationMessage, nextMessageRecord, conversationRecipient);
|
||||
present(conversationMessage, nextMessageRecord, conversationRecipient, isMessageRequestAccepted);
|
||||
|
||||
presentBackground(shouldCollapse(messageRecord, previousMessageRecord),
|
||||
shouldCollapse(messageRecord, nextMessageRecord),
|
||||
|
@ -265,7 +268,8 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
|
||||
private void present(@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull Recipient conversationRecipient)
|
||||
@NonNull Recipient conversationRecipient,
|
||||
boolean isMessageRequestAccepted)
|
||||
{
|
||||
if (batchSelected.contains(conversationMessage)) setSelected(true);
|
||||
else setSelected(false);
|
||||
|
@ -350,6 +354,14 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
eventListener.onInMemoryMessageClicked(inMemoryMessageRecord);
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isGroupV2DescriptionUpdate()) {
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setText(R.string.ConversationUpdateItem_view);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onViewGroupDescriptionChange(conversationRecipient.getGroupId().orNull(), conversationMessage.getMessageRecord().getGroupV2DescriptionUpdate(), isMessageRequestAccepted);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actionButton.setVisibility(GONE);
|
||||
actionButton.setOnClickListener(null);
|
||||
|
@ -439,7 +451,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
public void onChanged(Recipient recipient) {
|
||||
if (recipient.getId() == conversationRecipient.getId() && (conversationRecipient == null || !conversationRecipient.hasSameContent(recipient))) {
|
||||
conversationRecipient = recipient;
|
||||
present(conversationMessage, nextMessageRecord, conversationRecipient);
|
||||
present(conversationMessage, nextMessageRecord, conversationRecipient, isMessageRequestAccepted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -920,6 +920,14 @@ public final class GroupDatabase extends Database {
|
|||
return title;
|
||||
}
|
||||
|
||||
public @NonNull String getDescription() {
|
||||
if (v2GroupProperties == null) {
|
||||
return "";
|
||||
} else {
|
||||
return v2GroupProperties.getDecryptedGroup().getDescription();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientId> getMembers() {
|
||||
return members;
|
||||
}
|
||||
|
|
|
@ -98,6 +98,7 @@ final class GroupsV2UpdateMessageProducer {
|
|||
describeUnknownEditorRevokedInvitations(change, updates);
|
||||
describeUnknownEditorPromotePending(change, updates);
|
||||
describeUnknownEditorNewTitle(change, updates);
|
||||
describeUnknownEditorNewDescription(change, updates);
|
||||
describeUnknownEditorNewAvatar(change, updates);
|
||||
describeUnknownEditorNewTimer(change, updates);
|
||||
describeUnknownEditorNewAttributeAccess(change, updates);
|
||||
|
@ -121,6 +122,7 @@ final class GroupsV2UpdateMessageProducer {
|
|||
describeRevokedInvitations(change, updates);
|
||||
describePromotePending(change, updates);
|
||||
describeNewTitle(change, updates);
|
||||
describeNewDescription(change, updates);
|
||||
describeNewAvatar(change, updates);
|
||||
describeNewTimer(change, updates);
|
||||
describeNewAttributeAccess(change, updates);
|
||||
|
@ -431,12 +433,30 @@ final class GroupsV2UpdateMessageProducer {
|
|||
}
|
||||
}
|
||||
|
||||
private void describeNewDescription(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
if (change.hasNewDescription()) {
|
||||
if (editorIsYou) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_description), R.drawable.ic_update_group_name_16));
|
||||
} else {
|
||||
updates.add(updateDescription(change.getEditor(), editor -> context.getString(R.string.MessageRecord_s_changed_the_group_description, editor), R.drawable.ic_update_group_name_16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewTitle(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewTitle()) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(change.getNewTitle().getValue())), R.drawable.ic_update_group_name_16));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeUnknownEditorNewDescription(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
if (change.hasNewDescription()) {
|
||||
updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_description_has_changed), R.drawable.ic_update_group_name_16));
|
||||
}
|
||||
}
|
||||
|
||||
private void describeNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List<UpdateDescription> updates) {
|
||||
boolean editorIsYou = change.getEditor().equals(selfUuidBytes);
|
||||
|
||||
|
|
|
@ -360,6 +360,22 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
return UpdateDescription.mentioning(joinedMembers, stringFactory, R.drawable.ic_video_16);
|
||||
}
|
||||
|
||||
public boolean isGroupV2DescriptionUpdate() {
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = getDecryptedGroupV2Context();
|
||||
if (decryptedGroupV2Context != null) {
|
||||
return decryptedGroupV2Context.hasChange() && getDecryptedGroupV2Context().getChange().hasNewDescription();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public @NonNull String getGroupV2DescriptionUpdate() {
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = getDecryptedGroupV2Context();
|
||||
if (decryptedGroupV2Context != null) {
|
||||
return decryptedGroupV2Context.getChange().hasNewDescription() ? decryptedGroupV2Context.getChange().getNewDescription().getValue() : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a UUID by it's corresponding recipient's {@link Recipient#getDisplayName(Context)}.
|
||||
*/
|
||||
|
|
|
@ -62,17 +62,22 @@ public final class GroupManager {
|
|||
}
|
||||
|
||||
@WorkerThread
|
||||
public static GroupActionResult updateGroupDetails(@NonNull Context context,
|
||||
@NonNull GroupId groupId,
|
||||
@Nullable byte[] avatar,
|
||||
boolean avatarChanged,
|
||||
@NonNull String name,
|
||||
boolean nameChanged)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||
public static GroupActionResult updateGroupDetails(@NonNull Context context,
|
||||
@NonNull GroupId groupId,
|
||||
@Nullable byte[] avatar,
|
||||
boolean avatarChanged,
|
||||
@NonNull String name,
|
||||
boolean nameChanged,
|
||||
@NonNull String description,
|
||||
boolean descriptionChanged)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
|
||||
{
|
||||
if (groupId.isV2()) {
|
||||
try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId.requireV2())) {
|
||||
return edit.updateGroupTitleAndAvatar(nameChanged ? name : null, avatar, avatarChanged);
|
||||
return edit.updateGroupTitleDescriptionAndAvatar(nameChanged ? name : null,
|
||||
descriptionChanged ? description : null,
|
||||
avatar,
|
||||
avatarChanged);
|
||||
}
|
||||
} else if (groupId.isV1()) {
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context)
|
||||
|
|
|
@ -345,13 +345,17 @@ final class GroupManagerV2 {
|
|||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull GroupManager.GroupActionResult updateGroupTitleAndAvatar(@Nullable String title, @Nullable byte[] avatarBytes, boolean avatarChanged)
|
||||
@NonNull GroupManager.GroupActionResult updateGroupTitleDescriptionAndAvatar(@Nullable String title, @Nullable String description, @Nullable byte[] avatarBytes, boolean avatarChanged)
|
||||
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
|
||||
{
|
||||
try {
|
||||
GroupChange.Actions.Builder change = title != null ? groupOperations.createModifyGroupTitle(title)
|
||||
: GroupChange.Actions.newBuilder();
|
||||
|
||||
if (description != null) {
|
||||
change.setModifyDescription(groupOperations.createModifyGroupDescription(description));
|
||||
}
|
||||
|
||||
if (avatarChanged) {
|
||||
String cdnKey = avatarBytes != null ? groupsV2Api.uploadAvatar(avatarBytes, groupSecretParams, authorization.getAuthorizationForToday(selfUuid, groupSecretParams))
|
||||
: "";
|
||||
|
|
|
@ -125,6 +125,10 @@ public final class LiveGroup {
|
|||
});
|
||||
}
|
||||
|
||||
public LiveData<String> getDescription() {
|
||||
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::getDescription);
|
||||
}
|
||||
|
||||
public LiveData<Recipient> getGroupRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package org.thoughtcrime.securesms.groups;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public final class ParcelableGroupId implements Parcelable {
|
||||
|
||||
private final GroupId groupId;
|
||||
|
||||
public static Parcelable from(@Nullable GroupId groupId) {
|
||||
return new ParcelableGroupId(groupId);
|
||||
}
|
||||
|
||||
public static @Nullable GroupId get(@Nullable ParcelableGroupId parcelableGroupId) {
|
||||
if (parcelableGroupId == null) {
|
||||
return null;
|
||||
}
|
||||
return parcelableGroupId.groupId;
|
||||
}
|
||||
|
||||
ParcelableGroupId(@Nullable GroupId groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
if (groupId != null) {
|
||||
dest.writeString(groupId.toString());
|
||||
} else {
|
||||
dest.writeString(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Creator<ParcelableGroupId> CREATOR = new Creator<ParcelableGroupId>() {
|
||||
@Override
|
||||
public ParcelableGroupId createFromParcel(Parcel in) {
|
||||
return new ParcelableGroupId(GroupId.parseNullableOrThrow(in.readString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelableGroupId[] newArray(int size) {
|
||||
return new ParcelableGroupId[size];
|
||||
}
|
||||
};
|
||||
}
|
|
@ -21,6 +21,10 @@ public final class GroupDetails {
|
|||
return joinInfo.getTitle();
|
||||
}
|
||||
|
||||
public @NonNull String getGroupDescription() {
|
||||
return joinInfo.getDescription();
|
||||
}
|
||||
|
||||
public @Nullable byte[] getAvatarBytes() {
|
||||
return avatarBytes;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.groups.ui.invitesandrequests.joining;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -26,6 +28,8 @@ import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
|||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -42,6 +46,7 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
private AvatarImageView avatar;
|
||||
private TextView groupName;
|
||||
private TextView groupDetails;
|
||||
private TextView groupDescription;
|
||||
private TextView groupJoinExplain;
|
||||
private Button groupJoinButton;
|
||||
private Button groupCancelButton;
|
||||
|
@ -76,6 +81,7 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
busy = view.findViewById(R.id.group_join_busy);
|
||||
avatar = view.findViewById(R.id.group_join_recipient_avatar);
|
||||
groupName = view.findViewById(R.id.group_join_group_name);
|
||||
groupDescription = view.findViewById(R.id.group_join_group_description);
|
||||
groupDetails = view.findViewById(R.id.group_join_group_details);
|
||||
groupJoinExplain = view.findViewById(R.id.group_join_explain);
|
||||
|
||||
|
@ -98,6 +104,10 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
groupName.setText(details.getGroupName());
|
||||
groupDetails.setText(requireContext().getResources().getQuantityString(R.plurals.GroupJoinBottomSheetDialogFragment_group_dot_d_members, details.getGroupMembershipCount(), details.getGroupMembershipCount()));
|
||||
|
||||
if (!TextUtils.isEmpty(details.getGroupDescription())) {
|
||||
updateGroupDescription(details.getGroupName(), details.getGroupDescription());
|
||||
}
|
||||
|
||||
switch (getGroupJoinStatus()) {
|
||||
case UPDATE_LINKED_DEVICE_TO_JOIN:
|
||||
groupJoinExplain.setText(R.string.GroupJoinUpdateRequiredBottomSheetDialogFragment_update_linked_device_message);
|
||||
|
@ -145,6 +155,15 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
);
|
||||
}
|
||||
|
||||
private void updateGroupDescription(@NonNull String name, @NonNull String description) {
|
||||
groupDescription.setVisibility(View.VISIBLE);
|
||||
groupDescription.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
groupDescription.setText(GroupDescriptionUtil.style(requireContext(),
|
||||
description,
|
||||
true,
|
||||
() -> GroupDescriptionDialog.show(getChildFragmentManager(), name, description, true)));
|
||||
}
|
||||
|
||||
private static ExtendedGroupJoinStatus getGroupJoinStatus() {
|
||||
if (Recipient.self().getGroupsV2Capability() != Recipient.Capability.SUPPORTED) {
|
||||
return ExtendedGroupJoinStatus.UPDATE_LINKED_DEVICE_TO_JOIN;
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.content.Intent;
|
|||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
@ -17,7 +19,6 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
|
@ -35,6 +36,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
|
@ -43,10 +45,12 @@ import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
|||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupRightsDialog;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
|
@ -84,6 +88,7 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
private TextView pendingAndRequestingCount;
|
||||
private Toolbar toolbar;
|
||||
private TextView groupName;
|
||||
private EmojiTextView groupDescription;
|
||||
private LearnMoreTextView groupInfoText;
|
||||
private TextView memberCountUnderAvatar;
|
||||
private TextView memberCountAboveList;
|
||||
|
@ -145,6 +150,7 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
avatar = view.findViewById(R.id.group_avatar);
|
||||
toolbar = view.findViewById(R.id.toolbar);
|
||||
groupName = view.findViewById(R.id.name);
|
||||
groupDescription = view.findViewById(R.id.manage_group_description);
|
||||
groupInfoText = view.findViewById(R.id.manage_group_info_text);
|
||||
memberCountUnderAvatar = view.findViewById(R.id.member_count);
|
||||
memberCountAboveList = view.findViewById(R.id.member_count_2);
|
||||
|
@ -233,6 +239,7 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
});
|
||||
|
||||
viewModel.getTitle().observe(getViewLifecycleOwner(), groupName::setText);
|
||||
viewModel.getDescription().observe(getViewLifecycleOwner(), this::updateGroupDescription);
|
||||
viewModel.getMemberCountSummary().observe(getViewLifecycleOwner(), memberCountUnderAvatar::setText);
|
||||
viewModel.getFullMemberCountSummary().observe(getViewLifecycleOwner(), memberCountAboveList::setText);
|
||||
viewModel.getGroupRecipient().observe(getViewLifecycleOwner(), groupRecipient -> {
|
||||
|
@ -432,6 +439,31 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateGroupDescription(@NonNull ManageGroupViewModel.Description description) {
|
||||
if (!TextUtils.isEmpty(description.getDescription()) || (FeatureFlags.groupsV2Description() && description.canEditDescription())) {
|
||||
groupDescription.setVisibility(View.VISIBLE);
|
||||
groupDescription.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
memberCountUnderAvatar.setVisibility(View.GONE);
|
||||
} else {
|
||||
groupDescription.setVisibility(View.GONE);
|
||||
groupDescription.setMovementMethod(null);
|
||||
memberCountUnderAvatar.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(description.getDescription())) {
|
||||
if (FeatureFlags.groupsV2Description() && description.canEditDescription()) {
|
||||
groupDescription.setText(R.string.ManageGroupActivity_add_group_description);
|
||||
groupDescription.setOnClickListener(v -> startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getGroupId())));
|
||||
}
|
||||
} else {
|
||||
groupDescription.setOnClickListener(null);
|
||||
groupDescription.setText(GroupDescriptionUtil.style(requireContext(),
|
||||
description.getDescription(),
|
||||
description.shouldLinkifyWebLinks(),
|
||||
() -> GroupDescriptionDialog.show(getChildFragmentManager(), getGroupId(), null, description.shouldLinkifyWebLinks())));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
|
|
@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
|||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.Store;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -59,6 +60,8 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
private final Context context;
|
||||
private final ManageGroupRepository manageGroupRepository;
|
||||
private final LiveData<String> title;
|
||||
private final Store<Description> descriptionStore;
|
||||
private final LiveData<Description> description;
|
||||
private final LiveData<Boolean> isAdmin;
|
||||
private final LiveData<Boolean> canEditGroupAttributes;
|
||||
private final LiveData<Boolean> canAddMembers;
|
||||
|
@ -71,11 +74,11 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
private final LiveData<GroupAccessControl> editMembershipRights;
|
||||
private final LiveData<GroupAccessControl> editGroupAttributesRights;
|
||||
private final LiveData<Recipient> groupRecipient;
|
||||
private final MutableLiveData<GroupViewState> groupViewState = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<GroupViewState> groupViewState = new MutableLiveData<>(null);
|
||||
private final LiveData<MuteState> muteState;
|
||||
private final LiveData<Boolean> hasCustomNotifications;
|
||||
private final LiveData<Boolean> canCollapseMemberList;
|
||||
private final DefaultValueLiveData<CollapseState> memberListCollapseState = new DefaultValueLiveData<>(CollapseState.COLLAPSED);
|
||||
private final DefaultValueLiveData<CollapseState> memberListCollapseState = new DefaultValueLiveData<>(CollapseState.COLLAPSED);
|
||||
private final LiveData<Boolean> canLeaveGroup;
|
||||
private final LiveData<Boolean> canBlockGroup;
|
||||
private final LiveData<Boolean> canUnblockGroup;
|
||||
|
@ -140,6 +143,15 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
return GroupInfoMessage.NONE;
|
||||
}
|
||||
});
|
||||
|
||||
this.descriptionStore = new Store<>(Description.NONE);
|
||||
this.description = groupId.isV2() ? this.descriptionStore.getStateLiveData() : LiveDataUtil.empty();
|
||||
|
||||
if (groupId.isV2()) {
|
||||
this.descriptionStore.update(liveGroup.getDescription(), (description, state) -> new Description(description, state.shouldLinkifyWebLinks, state.canEditDescription));
|
||||
this.descriptionStore.update(LiveDataUtil.mapAsync(groupRecipient, r -> RecipientUtil.isMessageRequestAccepted(context, r)), (linkify, state) -> new Description(state.description, linkify, state.canEditDescription));
|
||||
this.descriptionStore.update(this.canEditGroupAttributes, (canEdit, state) -> new Description(state.description, state.shouldLinkifyWebLinks, canEdit));
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -181,6 +193,10 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
return title;
|
||||
}
|
||||
|
||||
LiveData<Description> getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
LiveData<MuteState> getMuteState() {
|
||||
return muteState;
|
||||
}
|
||||
|
@ -419,6 +435,32 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
Cursor create();
|
||||
}
|
||||
|
||||
public static class Description {
|
||||
private static final Description NONE = new Description("", false, false);
|
||||
|
||||
private final String description;
|
||||
private final boolean shouldLinkifyWebLinks;
|
||||
private final boolean canEditDescription;
|
||||
|
||||
public Description(String description, boolean shouldLinkifyWebLinks, boolean canEditDescription) {
|
||||
this.description = description;
|
||||
this.shouldLinkifyWebLinks = shouldLinkifyWebLinks;
|
||||
this.canEditDescription = canEditDescription;
|
||||
}
|
||||
|
||||
public @NonNull String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean shouldLinkifyWebLinks() {
|
||||
return shouldLinkifyWebLinks;
|
||||
}
|
||||
|
||||
public boolean canEditDescription() {
|
||||
return canEditDescription;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
private final Context context;
|
||||
private final GroupId groupId;
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.managegroup.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil;
|
||||
|
||||
/**
|
||||
* Dialog to show a full group description. Information regarding the description can be provided
|
||||
* as arguments, or a {@link GroupId} can be provided and the dialog will load it. If both are provided,
|
||||
* the title/description from the arguments takes precedence.
|
||||
*/
|
||||
public final class GroupDescriptionDialog extends DialogFragment {
|
||||
|
||||
private static final String ARGUMENT_GROUP_ID = "group_id";
|
||||
private static final String ARGUMENT_TITLE = "title";
|
||||
private static final String ARGUMENT_DESCRIPTION = "description";
|
||||
private static final String ARGUMENT_LINKIFY = "linkify";
|
||||
private static final String DIALOG_TAG = "GroupDescriptionDialog";
|
||||
|
||||
private TextView descriptionText;
|
||||
|
||||
public static void show(@NonNull FragmentManager fragmentManager, @NonNull String title, @Nullable String description, boolean linkify) {
|
||||
show(fragmentManager, null, title, description, linkify);
|
||||
}
|
||||
|
||||
public static void show(@NonNull FragmentManager fragmentManager, @NonNull GroupId groupId, @Nullable String description, boolean linkify) {
|
||||
show(fragmentManager, groupId, null, description, linkify);
|
||||
}
|
||||
|
||||
private static void show(@NonNull FragmentManager fragmentManager, @Nullable GroupId groupId, @Nullable String title, @Nullable String description, boolean linkify) {
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putParcelable(ARGUMENT_GROUP_ID, ParcelableGroupId.from(groupId));
|
||||
arguments.putString(ARGUMENT_TITLE, title);
|
||||
arguments.putString(ARGUMENT_DESCRIPTION, description);
|
||||
arguments.putBoolean(ARGUMENT_LINKIFY, linkify);
|
||||
|
||||
GroupDescriptionDialog dialogFragment = new GroupDescriptionDialog();
|
||||
dialogFragment.setArguments(arguments);
|
||||
|
||||
dialogFragment.show(fragmentManager, DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.group_description_dialog, null, false);
|
||||
String argumentTitle = requireArguments().getString(ARGUMENT_TITLE, null);
|
||||
String argumentDescription = requireArguments().getString(ARGUMENT_DESCRIPTION, null);
|
||||
GroupId argumentGroupId = ParcelableGroupId.get(requireArguments().getParcelable(ARGUMENT_GROUP_ID));
|
||||
boolean linkify = requireArguments().getBoolean(ARGUMENT_LINKIFY, false);
|
||||
LiveGroup liveGroup = argumentGroupId != null ? new LiveGroup(argumentGroupId) : null;
|
||||
|
||||
descriptionText = dialogView.findViewById(R.id.group_description_dialog_text);
|
||||
descriptionText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext(), R.style.Signal_ThemeOverlay_Dialog_Rounded);
|
||||
Dialog dialog = builder.setTitle(TextUtils.isEmpty(argumentTitle) ? getString(R.string.GroupDescriptionDialog__group_description) : argumentTitle)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create();
|
||||
|
||||
if (argumentDescription != null) {
|
||||
descriptionText.setText(GroupDescriptionUtil.style(requireContext(), argumentDescription, linkify, null));
|
||||
} else if (liveGroup != null) {
|
||||
liveGroup.getDescription().observe(this, d -> descriptionText.setText(GroupDescriptionUtil.style(requireContext(), d, linkify, null)));
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(argumentTitle) && liveGroup != null) {
|
||||
liveGroup.getTitle().observe(this, dialog::setTitle);
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package org.thoughtcrime.securesms.groups.v2;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
|
||||
public final class GroupDescriptionUtil {
|
||||
|
||||
public static final int MAX_DESCRIPTION_LENGTH = 80;
|
||||
|
||||
/**
|
||||
* Style a group description.
|
||||
*
|
||||
* @param description full description
|
||||
* @param linkify flag indicating if web urls should be linkified
|
||||
* @param moreClick Callback for when truncating and need to show more via another means. Required to enable truncating.
|
||||
* @return styled group description
|
||||
*/
|
||||
public static @NonNull Spannable style(@NonNull Context context, @NonNull String description, boolean linkify, @Nullable Runnable moreClick) {
|
||||
SpannableString descriptionSpannable = new SpannableString(description);
|
||||
|
||||
if (linkify) {
|
||||
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
|
||||
boolean hasLinks = Linkify.addLinks(descriptionSpannable, linkPattern);
|
||||
|
||||
if (hasLinks) {
|
||||
Stream.of(descriptionSpannable.getSpans(0, descriptionSpannable.length(), URLSpan.class))
|
||||
.filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL()))
|
||||
.forEach(descriptionSpannable::removeSpan);
|
||||
}
|
||||
}
|
||||
|
||||
if (moreClick != null && descriptionSpannable.length() > MAX_DESCRIPTION_LENGTH) {
|
||||
ClickableSpan style = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
moreClick.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
ds.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
}
|
||||
};
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(descriptionSpannable.subSequence(0, MAX_DESCRIPTION_LENGTH)).append(context.getString(R.string.ManageGroupActivity_more));
|
||||
builder.setSpan(style, MAX_DESCRIPTION_LENGTH + 1, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return builder;
|
||||
}
|
||||
|
||||
return descriptionSpannable;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package org.thoughtcrime.securesms.messagerequests;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
final class GroupInfo {
|
||||
static final GroupInfo ZERO = new GroupInfo(0, 0, "");
|
||||
|
||||
private final int fullMemberCount;
|
||||
private final int pendingMemberCount;
|
||||
private final String description;
|
||||
|
||||
GroupInfo(int fullMemberCount, int pendingMemberCount, @NonNull String description) {
|
||||
this.fullMemberCount = fullMemberCount;
|
||||
this.pendingMemberCount = pendingMemberCount;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
int getFullMemberCount() {
|
||||
return fullMemberCount;
|
||||
}
|
||||
|
||||
int getPendingMemberCount() {
|
||||
return pendingMemberCount;
|
||||
}
|
||||
|
||||
public @NonNull String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package org.thoughtcrime.securesms.messagerequests;
|
||||
|
||||
final class GroupMemberCount {
|
||||
static final GroupMemberCount ZERO = new GroupMemberCount(0, 0);
|
||||
|
||||
private final int fullMemberCount;
|
||||
private final int pendingMemberCount;
|
||||
|
||||
GroupMemberCount(int fullMemberCount, int pendingMemberCount) {
|
||||
this.fullMemberCount = fullMemberCount;
|
||||
this.pendingMemberCount = pendingMemberCount;
|
||||
}
|
||||
|
||||
int getFullMemberCount() {
|
||||
return fullMemberCount;
|
||||
}
|
||||
|
||||
int getPendingMemberCount() {
|
||||
return pendingMemberCount;
|
||||
}
|
||||
}
|
|
@ -56,18 +56,18 @@ final class MessageRequestRepository {
|
|||
});
|
||||
}
|
||||
|
||||
void getMemberCount(@NonNull RecipientId recipientId, @NonNull Consumer<GroupMemberCount> onMemberCountLoaded) {
|
||||
void getGroupInfo(@NonNull RecipientId recipientId, @NonNull Consumer<GroupInfo> onGroupInfoLoaded) {
|
||||
executor.execute(() -> {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
Optional<GroupDatabase.GroupRecord> groupRecord = groupDatabase.getGroup(recipientId);
|
||||
onMemberCountLoaded.accept(groupRecord.transform(record -> {
|
||||
onGroupInfoLoaded.accept(groupRecord.transform(record -> {
|
||||
if (record.isV2Group()) {
|
||||
DecryptedGroup decryptedGroup = record.requireV2GroupProperties().getDecryptedGroup();
|
||||
return new GroupMemberCount(decryptedGroup.getMembersCount(), decryptedGroup.getPendingMembersCount());
|
||||
return new GroupInfo(decryptedGroup.getMembersCount(), decryptedGroup.getPendingMembersCount(), decryptedGroup.getDescription());
|
||||
} else {
|
||||
return new GroupMemberCount(record.getMembers().size(), 0);
|
||||
return new GroupInfo(record.getMembers().size(), 0, "");
|
||||
}
|
||||
}).or(GroupMemberCount.ZERO));
|
||||
}).or(GroupInfo.ZERO));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataTriple;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.Store;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -33,10 +34,9 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
private final MutableLiveData<Recipient> recipient = new MutableLiveData<>();
|
||||
private final LiveData<MessageData> messageData;
|
||||
private final MutableLiveData<List<String>> groups = new MutableLiveData<>(Collections.emptyList());
|
||||
private final MutableLiveData<GroupMemberCount> memberCount = new MutableLiveData<>(GroupMemberCount.ZERO);
|
||||
private final MutableLiveData<GroupInfo> groupInfo = new MutableLiveData<>(GroupInfo.ZERO);
|
||||
private final LiveData<RequestReviewDisplayState> requestReviewDisplayState;
|
||||
private final LiveData<RecipientInfo> recipientInfo = Transformations.map(new LiveDataTriple<>(recipient, memberCount, groups),
|
||||
triple -> new RecipientInfo(triple.first(), triple.second(), triple.third()));
|
||||
private final Store<RecipientInfo> recipientInfoStore = new Store<>(new RecipientInfo(null, null, null, null));
|
||||
|
||||
private final MessageRequestRepository repository;
|
||||
|
||||
|
@ -44,7 +44,7 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
private long threadId;
|
||||
|
||||
private final RecipientForeverObserver recipientObserver = recipient -> {
|
||||
loadMemberCount();
|
||||
loadGroupInfo();
|
||||
this.recipient.setValue(recipient);
|
||||
};
|
||||
|
||||
|
@ -52,6 +52,11 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
this.repository = repository;
|
||||
this.messageData = LiveDataUtil.mapAsync(recipient, this::createMessageDataForRecipient);
|
||||
this.requestReviewDisplayState = LiveDataUtil.mapAsync(messageData, MessageRequestViewModel::transformHolderToReviewDisplayState);
|
||||
|
||||
recipientInfoStore.update(this.recipient, (recipient, state) -> new RecipientInfo(recipient, state.groupInfo, state.sharedGroups, state.messageRequestState));
|
||||
recipientInfoStore.update(this.groupInfo, (groupInfo, state) -> new RecipientInfo(state.recipient, groupInfo, state.sharedGroups, state.messageRequestState));
|
||||
recipientInfoStore.update(this.groups, (sharedGroups, state) -> new RecipientInfo(state.recipient, state.groupInfo, sharedGroups, state.messageRequestState));
|
||||
recipientInfoStore.update(this.messageData, (messageData, state) -> new RecipientInfo(state.recipient, state.groupInfo, state.sharedGroups, messageData.messageState));
|
||||
}
|
||||
|
||||
public void setConversationInfo(@NonNull RecipientId recipientId, long threadId) {
|
||||
|
@ -64,7 +69,7 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
|
||||
loadRecipient();
|
||||
loadGroups();
|
||||
loadMemberCount();
|
||||
loadGroupInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -87,7 +92,7 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
public LiveData<RecipientInfo> getRecipientInfo() {
|
||||
return recipientInfo;
|
||||
return recipientInfoStore.getStateLiveData();
|
||||
}
|
||||
|
||||
public LiveData<Status> getMessageRequestStatus() {
|
||||
|
@ -161,8 +166,8 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
repository.getGroups(liveRecipient.getId(), this.groups::postValue);
|
||||
}
|
||||
|
||||
private void loadMemberCount() {
|
||||
repository.getMemberCount(liveRecipient.getId(), memberCount::postValue);
|
||||
private void loadGroupInfo() {
|
||||
repository.getGroupInfo(liveRecipient.getId(), groupInfo::postValue);
|
||||
}
|
||||
|
||||
private static RequestReviewDisplayState transformHolderToReviewDisplayState(@NonNull MessageData holder) {
|
||||
|
@ -181,14 +186,16 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
public static class RecipientInfo {
|
||||
@Nullable private final Recipient recipient;
|
||||
@NonNull private final GroupMemberCount groupMemberCount;
|
||||
@NonNull private final List<String> sharedGroups;
|
||||
@Nullable private final Recipient recipient;
|
||||
@NonNull private final GroupInfo groupInfo;
|
||||
@NonNull private final List<String> sharedGroups;
|
||||
@Nullable private final MessageRequestState messageRequestState;
|
||||
|
||||
private RecipientInfo(@Nullable Recipient recipient, @Nullable GroupMemberCount groupMemberCount, @Nullable List<String> sharedGroups) {
|
||||
this.recipient = recipient;
|
||||
this.groupMemberCount = groupMemberCount == null ? GroupMemberCount.ZERO : groupMemberCount;
|
||||
this.sharedGroups = sharedGroups == null ? Collections.emptyList() : sharedGroups;
|
||||
private RecipientInfo(@Nullable Recipient recipient, @Nullable GroupInfo groupInfo, @Nullable List<String> sharedGroups, @Nullable MessageRequestState messageRequestState) {
|
||||
this.recipient = recipient;
|
||||
this.groupInfo = groupInfo == null ? GroupInfo.ZERO : groupInfo;
|
||||
this.sharedGroups = sharedGroups == null ? Collections.emptyList() : sharedGroups;
|
||||
this.messageRequestState = messageRequestState;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -197,17 +204,26 @@ public class MessageRequestViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
public int getGroupMemberCount() {
|
||||
return groupMemberCount.getFullMemberCount();
|
||||
return groupInfo.getFullMemberCount();
|
||||
}
|
||||
|
||||
public int getGroupPendingMemberCount() {
|
||||
return groupMemberCount.getPendingMemberCount();
|
||||
return groupInfo.getPendingMemberCount();
|
||||
}
|
||||
|
||||
public @NonNull String getGroupDescription() {
|
||||
return groupInfo.getDescription();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<String> getSharedGroups() {
|
||||
return sharedGroups;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public MessageRequestState getMessageRequestState() {
|
||||
return messageRequestState;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
|
|
|
@ -78,17 +78,34 @@ class EditGroupProfileRepository implements EditProfileRepository {
|
|||
}, nameConsumer::accept);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getCurrentDescription(@NonNull Consumer<String> descriptionConsumer) {
|
||||
SimpleTask.run(() -> {
|
||||
RecipientId recipientId = getRecipientId();
|
||||
|
||||
return DatabaseFactory.getGroupDatabase(context)
|
||||
.getGroup(recipientId)
|
||||
.transform(groupRecord -> {
|
||||
String description = groupRecord.getDescription();
|
||||
return description == null ? "" : description;
|
||||
})
|
||||
.or("");
|
||||
}, descriptionConsumer::accept);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadProfile(@NonNull ProfileName profileName,
|
||||
@NonNull String displayName,
|
||||
boolean displayNameChanged,
|
||||
@NonNull String description,
|
||||
boolean descriptionChanged,
|
||||
@Nullable byte[] avatar,
|
||||
boolean avatarChanged,
|
||||
@NonNull Consumer<UploadResult> uploadResultConsumer)
|
||||
{
|
||||
SimpleTask.run(() -> {
|
||||
try {
|
||||
GroupManager.updateGroupDetails(context, groupId, avatar, avatarChanged, displayName, displayNameChanged);
|
||||
GroupManager.updateGroupDetails(context, groupId, avatar, avatarChanged, displayName, displayNameChanged, description, descriptionChanged);
|
||||
|
||||
return UploadResult.SUCCESS;
|
||||
} catch (GroupChangeException | IOException e) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
|
@ -35,22 +35,20 @@ import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
|
|||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.profiles.manage.EditProfileNameFragment;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil.MAX_DESCRIPTION_LENGTH;
|
||||
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.EXCLUDE_SYSTEM;
|
||||
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.GROUP_ID;
|
||||
import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_BUTTON_TEXT;
|
||||
|
@ -61,6 +59,8 @@ public class EditProfileFragment extends LoggingFragment {
|
|||
|
||||
private static final String TAG = Log.tag(EditProfileFragment.class);
|
||||
private static final short REQUEST_CODE_SELECT_AVATAR = 31726;
|
||||
private static final int MAX_DESCRIPTION_GLYPHS = 480;
|
||||
private static final int MAX_DESCRIPTION_BYTES = 8192;
|
||||
|
||||
private Toolbar toolbar;
|
||||
private View title;
|
||||
|
@ -97,8 +97,8 @@ public class EditProfileFragment extends LoggingFragment {
|
|||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
GroupId groupId = GroupId.parseNullableOrThrow(requireArguments().getString(GROUP_ID, null));
|
||||
|
||||
initializeResources(view, groupId);
|
||||
initializeViewModel(requireArguments().getBoolean(EXCLUDE_SYSTEM, false), groupId, savedInstanceState != null);
|
||||
initializeResources(view, groupId);
|
||||
initializeProfileAvatar();
|
||||
initializeProfileName();
|
||||
}
|
||||
|
@ -183,9 +183,25 @@ public class EditProfileFragment extends LoggingFragment {
|
|||
givenName.requestFocus();
|
||||
toolbar.setTitle(R.string.EditProfileFragment__edit_group_name_and_photo);
|
||||
preview.setVisibility(View.GONE);
|
||||
familyName.setVisibility(View.GONE);
|
||||
familyName.setEnabled(false);
|
||||
view.findViewById(R.id.description_text).setVisibility(View.GONE);
|
||||
|
||||
if (FeatureFlags.groupsV2Description()) {
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(familyName, MAX_DESCRIPTION_GLYPHS);
|
||||
familyName.addTextChangedListener(new AfterTextChanged(s -> {
|
||||
EditProfileNameFragment.trimFieldToMaxByteLength(s, MAX_DESCRIPTION_BYTES);
|
||||
viewModel.setFamilyName(s.toString());
|
||||
}));
|
||||
familyName.setHint(R.string.EditProfileFragment__group_description);
|
||||
familyName.setSingleLine(false);
|
||||
familyName.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
|
||||
|
||||
LearnMoreTextView descriptionText = view.findViewById(R.id.description_text);
|
||||
descriptionText.setLearnMoreVisible(false);
|
||||
descriptionText.setText(R.string.CreateProfileActivity_group_descriptions_will_be_visible_to_members_of_this_group_and_people_who_have_been_invited);
|
||||
} else {
|
||||
familyName.setVisibility(View.GONE);
|
||||
familyName.setEnabled(false);
|
||||
view.findViewById(R.id.description_text).setVisibility(View.GONE);
|
||||
}
|
||||
view.<ImageView>findViewById(R.id.avatar_placeholder).setImageResource(R.drawable.ic_group_outline_40);
|
||||
} else {
|
||||
EditTextUtil.addGraphemeClusterLimitFilter(givenName, EditProfileNameFragment.NAME_MAX_GLYPHS);
|
||||
|
|
|
@ -17,9 +17,13 @@ interface EditProfileRepository {
|
|||
|
||||
void getCurrentName(@NonNull Consumer<String> nameConsumer);
|
||||
|
||||
void getCurrentDescription(@NonNull Consumer<String> descriptionConsumer);
|
||||
|
||||
void uploadProfile(@NonNull ProfileName profileName,
|
||||
@NonNull String displayName,
|
||||
boolean displayNameChanged,
|
||||
@NonNull String description,
|
||||
boolean descriptionChanged,
|
||||
@Nullable byte[] avatar,
|
||||
boolean avatarChanged,
|
||||
@NonNull Consumer<UploadResult> uploadResultConsumer);
|
||||
|
|
|
@ -13,24 +13,24 @@ import org.thoughtcrime.securesms.groups.GroupId;
|
|||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
class EditProfileViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<String> givenName = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> familyName = new MutableLiveData<>();
|
||||
private final LiveData<String> trimmedGivenName = Transformations.map(givenName, StringUtil::trimToVisualBounds);
|
||||
private final LiveData<String> trimmedFamilyName = Transformations.map(familyName, StringUtil::trimToVisualBounds);
|
||||
private final LiveData<ProfileName> internalProfileName = LiveDataUtil.combineLatest(trimmedGivenName, trimmedFamilyName, ProfileName::fromParts);
|
||||
private final MutableLiveData<byte[]> internalAvatar = new MutableLiveData<>();
|
||||
private final MutableLiveData<byte[]> originalAvatar = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> originalDisplayName = new MutableLiveData<>();
|
||||
private final LiveData<Boolean> isFormValid;
|
||||
private final EditProfileRepository repository;
|
||||
private final GroupId groupId;
|
||||
private final MutableLiveData<String> givenName = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> familyName = new MutableLiveData<>();
|
||||
private final LiveData<String> trimmedGivenName = Transformations.map(givenName, StringUtil::trimToVisualBounds);
|
||||
private final LiveData<String> trimmedFamilyName = Transformations.map(familyName, StringUtil::trimToVisualBounds);
|
||||
private final LiveData<ProfileName> internalProfileName = LiveDataUtil.combineLatest(trimmedGivenName, trimmedFamilyName, ProfileName::fromParts);
|
||||
private final MutableLiveData<byte[]> internalAvatar = new MutableLiveData<>();
|
||||
private final MutableLiveData<byte[]> originalAvatar = new MutableLiveData<>();
|
||||
private final MutableLiveData<String> originalDisplayName = new MutableLiveData<>();
|
||||
private final LiveData<Boolean> isFormValid;
|
||||
private final EditProfileRepository repository;
|
||||
private final GroupId groupId;
|
||||
private String originalDescription;
|
||||
|
||||
private EditProfileViewModel(@NonNull EditProfileRepository repository, boolean hasInstanceState, @Nullable GroupId groupId) {
|
||||
this.repository = repository;
|
||||
|
@ -42,6 +42,10 @@ class EditProfileViewModel extends ViewModel {
|
|||
if (groupId != null) {
|
||||
repository.getCurrentDisplayName(originalDisplayName::setValue);
|
||||
repository.getCurrentName(givenName::setValue);
|
||||
repository.getCurrentDescription(d -> {
|
||||
originalDescription = d;
|
||||
familyName.setValue(d);
|
||||
});
|
||||
} else {
|
||||
repository.getCurrentProfileName(name -> {
|
||||
givenName.setValue(name.getGivenName());
|
||||
|
@ -103,6 +107,7 @@ class EditProfileViewModel extends ViewModel {
|
|||
public void submitProfile(Consumer<EditProfileRepository.UploadResult> uploadResultConsumer) {
|
||||
ProfileName profileName = isGroup() ? ProfileName.EMPTY : internalProfileName.getValue();
|
||||
String displayName = isGroup() ? givenName.getValue() : "";
|
||||
String description = isGroup() ? familyName.getValue() : "";
|
||||
|
||||
if (profileName == null || displayName == null) {
|
||||
return;
|
||||
|
@ -111,10 +116,13 @@ class EditProfileViewModel extends ViewModel {
|
|||
byte[] oldAvatar = originalAvatar.getValue();
|
||||
byte[] newAvatar = internalAvatar.getValue();
|
||||
String oldDisplayName = isGroup() ? originalDisplayName.getValue() : null;
|
||||
String oldDescription = isGroup() ? originalDescription : null;
|
||||
|
||||
repository.uploadProfile(profileName,
|
||||
displayName,
|
||||
!Objects.equals(StringUtil.stripBidiProtection(oldDisplayName), displayName),
|
||||
description,
|
||||
!Objects.equals(StringUtil.stripBidiProtection(oldDescription), description),
|
||||
newAvatar,
|
||||
!Arrays.equals(oldAvatar, newAvatar),
|
||||
uploadResultConsumer);
|
||||
|
|
|
@ -107,10 +107,16 @@ public class EditSelfProfileRepository implements EditProfileRepository {
|
|||
nameConsumer.accept("");
|
||||
}
|
||||
|
||||
@Override public void getCurrentDescription(@NonNull Consumer<String> descriptionConsumer) {
|
||||
descriptionConsumer.accept("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uploadProfile(@NonNull ProfileName profileName,
|
||||
@NonNull String displayName,
|
||||
boolean displayNameChanged,
|
||||
@NonNull String description,
|
||||
boolean descriptionChanged,
|
||||
@Nullable byte[] avatar,
|
||||
boolean avatarChanged,
|
||||
@NonNull Consumer<UploadResult> uploadResultConsumer)
|
||||
|
|
|
@ -104,7 +104,11 @@ public class EditProfileNameFragment extends Fragment {
|
|||
}
|
||||
|
||||
public static void trimFieldToMaxByteLength(Editable s) {
|
||||
int trimmedLength = StringUtil.trimToFit(s.toString(), ProfileName.MAX_PART_LENGTH).length();
|
||||
trimFieldToMaxByteLength(s, ProfileName.MAX_PART_LENGTH);
|
||||
}
|
||||
|
||||
public static void trimFieldToMaxByteLength(Editable s, int length) {
|
||||
int trimmedLength = StringUtil.trimToFit(s.toString(), length).length();
|
||||
|
||||
if (s.length() > trimmedLength) {
|
||||
s.delete(trimmedLength, s.length());
|
||||
|
|
|
@ -80,6 +80,7 @@ public final class FeatureFlags {
|
|||
private static final String NOTIFICATION_REWRITE = "android.notificationRewrite";
|
||||
private static final String MP4_GIF_SEND_SUPPORT = "android.mp4GifSendSupport";
|
||||
private static final String MEDIA_QUALITY_LEVELS = "android.mediaQuality.levels";
|
||||
private static final String GROUPS_V2_DESCRIPTION_VERSION = "android.groupsv2.descriptionVersion";
|
||||
|
||||
/**
|
||||
* 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 {
|
|||
MESSAGE_PROCESSOR_DELAY,
|
||||
NOTIFICATION_REWRITE,
|
||||
MP4_GIF_SEND_SUPPORT,
|
||||
MEDIA_QUALITY_LEVELS
|
||||
MEDIA_QUALITY_LEVELS,
|
||||
GROUPS_V2_DESCRIPTION_VERSION
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -159,7 +161,8 @@ public final class FeatureFlags {
|
|||
GV1_FORCED_MIGRATE,
|
||||
NOTIFICATION_REWRITE,
|
||||
MP4_GIF_SEND_SUPPORT,
|
||||
MEDIA_QUALITY_LEVELS
|
||||
MEDIA_QUALITY_LEVELS,
|
||||
GROUPS_V2_DESCRIPTION_VERSION
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -359,6 +362,10 @@ public final class FeatureFlags {
|
|||
return getString(MEDIA_QUALITY_LEVELS, "");
|
||||
}
|
||||
|
||||
public static boolean groupsV2Description() {
|
||||
return getVersionFlag(GROUPS_V2_DESCRIPTION_VERSION) == VersionFlag.ON;
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -126,6 +126,10 @@ public final class LiveDataUtil {
|
|||
return new MutableLiveData<>(item);
|
||||
}
|
||||
|
||||
public static <A> LiveData<A> empty() {
|
||||
return new MutableLiveData<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits {@param whileWaiting} until {@param main} starts emitting.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout 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:padding="?attr/dialogPreferredPadding">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/group_description_dialog_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||
tools:text="This is a test." />
|
||||
|
||||
</FrameLayout>
|
|
@ -28,7 +28,7 @@
|
|||
app:layout_constraintTop_toTopOf="@+id/group_join_recipient_avatar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/group_join_group_name"
|
||||
style="@style/TextAppearance.Signal.Body1.Bold"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -53,6 +53,24 @@
|
|||
app:layout_constraintTop_toBottomOf="@+id/group_join_group_name"
|
||||
tools:text="Group · 12 members" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/group_join_group_description"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_join_group_details"
|
||||
tools:text="Coordinate the folks to do the thing"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_join_explain"
|
||||
style="@style/TextAppearance.Signal.Body2"
|
||||
|
@ -63,9 +81,9 @@
|
|||
android:layout_marginEnd="16dp"
|
||||
android:textColor="@color/signal_text_primary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/group_join_group_details"
|
||||
app:layout_constraintTop_toBottomOf="@+id/group_join_group_description"
|
||||
tools:text="@string/GroupJoinBottomSheetDialogFragment_admin_approval_needed" />
|
||||
|
||||
<Button
|
||||
|
|
|
@ -54,6 +54,20 @@
|
|||
android:textColor="@color/signal_text_secondary"
|
||||
tools:text="12 members (4 invited)" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/manage_group_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:visibility="gone"
|
||||
tools:text="Group to plot the capture of Doc Oct"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
|
||||
android:id="@+id/manage_group_info_text"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
android:layout_weight="1"
|
||||
android:scrollIndicators="top|bottom">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
|
@ -163,6 +164,7 @@
|
|||
android:hint="@string/CreateProfileActivity_last_name_optional"
|
||||
android:inputType="textPersonName"
|
||||
android:singleLine="true" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
|
||||
|
|
|
@ -114,10 +114,11 @@
|
|||
<item name="android:windowAnimationStyle">@style/TextSecure.Animation.FullScreenDialog</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.ThemeOverlay.Dialog.Rounded" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog">
|
||||
<style name="Signal.ThemeOverlay.Dialog.Rounded" parent="@style/ThemeOverlay.MaterialComponents.MaterialAlertDialog">
|
||||
<item name="alertDialogStyle">@style/Signal.MaterialAlertDialog</item>
|
||||
<item name="android:background">@color/signal_background_dialog</item>
|
||||
<item name="materialAlertDialogBodyTextStyle">@style/Signal.Text.Body</item>
|
||||
<item name="materialAlertDialogTitleTextStyle">@style/TextAppearance.Signal.Title2.MaterialDialog</item>
|
||||
<item name="materialAlertDialogBodyTextStyle">@style/TextAppearance.Signal.Body1</item>
|
||||
<item name="buttonBarPositiveButtonStyle">@style/Signal.Widget.Button.Dialog</item>
|
||||
<item name="buttonBarNeutralButtonStyle">@style/Signal.Widget.Button.Dialog</item>
|
||||
<item name="buttonBarNegativeButtonStyle">@style/Signal.Widget.Button.Dialog</item>
|
||||
|
|
|
@ -744,6 +744,8 @@
|
|||
<string name="ManageGroupActivity_upgrade_this_group">upgrade this group.</string>
|
||||
<string name="ManageGroupActivity_this_is_an_insecure_mms_group">This is an insecure MMS Group. To chat privately, invite your contacts to Signal.</string>
|
||||
<string name="ManageGroupActivity_invite_now">Invite now</string>
|
||||
<string name="ManageGroupActivity_more">…more</string>
|
||||
<string name="ManageGroupActivity_add_group_description">Add group description...</string>
|
||||
|
||||
<!-- GroupMentionSettingDialog -->
|
||||
<string name="GroupMentionSettingDialog_notify_me_for_mentions">Notify me for Mentions</string>
|
||||
|
@ -1127,6 +1129,11 @@
|
|||
<string name="MessageRecord_s_changed_the_group_name_to_s">%1$s changed the group name to \"%2$s\".</string>
|
||||
<string name="MessageRecord_the_group_name_has_changed_to_s">The group name has changed to \"%1$s\".</string>
|
||||
|
||||
<!-- GV2 description change -->
|
||||
<string name="MessageRecord_you_changed_the_group_description">You changed the group description.</string>
|
||||
<string name="MessageRecord_s_changed_the_group_description">%1$s changed the group description.</string>
|
||||
<string name="MessageRecord_the_group_description_has_changed">The group description has changed.</string>
|
||||
|
||||
<!-- GV2 avatar change -->
|
||||
<string name="MessageRecord_you_changed_the_group_avatar">You changed the group avatar.</string>
|
||||
<string name="MessageRecord_s_changed_the_group_avatar">%1$s changed the group avatar.</string>
|
||||
|
@ -1894,6 +1901,7 @@
|
|||
<string name="ConversationUpdateItem_enable_call_notifications">Enable Call Notifications</string>
|
||||
<string name="ConversationUpdateItem_no_groups_in_common_review_requests_carefully">No groups in common. Review requests carefully.</string>
|
||||
<string name="ConversationUpdateItem_no_contacts_in_this_group_review_requests_carefully">No contacts in this group. Review requests carefully.</string>
|
||||
<string name="ConversationUpdateItem_view">View</string>
|
||||
|
||||
<!-- audio_view -->
|
||||
<string name="audio_view__play_pause_accessibility_description">Play … Pause</string>
|
||||
|
@ -2057,6 +2065,7 @@
|
|||
<string name="CreateProfileActivity__username">Username</string>
|
||||
<string name="CreateProfileActivity__create_a_username">Create a username</string>
|
||||
<string name="CreateProfileActivity_custom_mms_group_names_and_photos_will_only_be_visible_to_you">Custom MMS group names and photos will only be visible to you.</string>
|
||||
<string name="CreateProfileActivity_group_descriptions_will_be_visible_to_members_of_this_group_and_people_who_have_been_invited">Group descriptions will be visible to members of this group and people who have been invited.</string>
|
||||
|
||||
<!-- EditAboutFragment -->
|
||||
<string name="EditAboutFragment_about">About</string>
|
||||
|
@ -2073,6 +2082,7 @@
|
|||
<!-- EditProfileFragment -->
|
||||
<string name="EditProfileFragment__edit_group_name_and_photo">Edit group name and photo</string>
|
||||
<string name="EditProfileFragment__group_name">Group name</string>
|
||||
<string name="EditProfileFragment__group_description">Group description</string>
|
||||
<string name="EditProfileFragment__support_link" translatable="false">https://support.signal.org/hc/articles/360007459591</string>
|
||||
|
||||
<!-- EditProfileNameFragment -->
|
||||
|
@ -3362,6 +3372,9 @@
|
|||
<string name="GroupsInCommonMessageRequest__okay">Okay</string>
|
||||
<string name="GroupsInCommonMessageRequest__support_article" translatable="false">https://support.signal.org/hc/articles/360007459591</string>
|
||||
|
||||
<!-- GroupDescriptionDialog -->
|
||||
<string name="GroupDescriptionDialog__group_description">Group description</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -69,6 +69,11 @@
|
|||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Title2.MaterialDialog">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">match_parent</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Headline.Insights" parent="">
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textSize">28sp</item>
|
||||
|
|
|
@ -78,6 +78,9 @@ dependencyVerification {
|
|||
['androidx.drawerlayout:drawerlayout:1.0.0',
|
||||
'9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1'],
|
||||
|
||||
['androidx.dynamicanimation:dynamicanimation:1.0.0',
|
||||
'ce005162c229bf308d2d5b12fb6cad0874069cbbeaccee63a8193bd08d40de04'],
|
||||
|
||||
['androidx.exifinterface:exifinterface:1.0.0',
|
||||
'ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11'],
|
||||
|
||||
|
@ -288,8 +291,8 @@ dependencyVerification {
|
|||
['com.google.android.gms:play-services-tasks:17.0.0',
|
||||
'2e6d1738b73647f3fe7a038b9780b97717b3746eae258009197e36e7bf3112a5'],
|
||||
|
||||
['com.google.android.material:material:1.2.1',
|
||||
'd3d0cc776f2341da8e572586c7d390a5b356ce39a0deb2768071dc40b364ac80'],
|
||||
['com.google.android.material:material:1.3.0',
|
||||
'cbf1e7d69fc236cdadcbd1ec5f6c0a1a41aca6ad1ef7f8481058956270ab1f0a'],
|
||||
|
||||
['com.google.android:annotations:4.1.1.4',
|
||||
'ba734e1e84c09d615af6a09d33034b4f0442f8772dec120efb376d86a565ae15'],
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
org.gradle.jvmargs=-Xmx3072m
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=true
|
||||
kapt.incremental.apt=false
|
|
@ -36,4 +36,6 @@ public interface ChangeSetModifier {
|
|||
void removeDeleteRequestingMembers(int i);
|
||||
|
||||
void removePromoteRequestingMembers(int i);
|
||||
|
||||
void clearModifyDescription();
|
||||
}
|
||||
|
|
|
@ -123,6 +123,11 @@ final class DecryptedGroupChangeActionsBuilderChangeSetModifier implements Chang
|
|||
result.removePromoteRequestingMembers(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearModifyDescription() {
|
||||
result.clearNewDescription();
|
||||
}
|
||||
|
||||
private static List<ByteString> removeIndexFromByteStringList(List<ByteString> byteStrings, int i) {
|
||||
List<ByteString> modifiedList = new ArrayList<>(byteStrings);
|
||||
|
||||
|
|
|
@ -274,6 +274,8 @@ public final class DecryptedGroupUtil {
|
|||
|
||||
applyModifyTitleAction(builder, change);
|
||||
|
||||
applyModifyDescriptionAction(builder, change);
|
||||
|
||||
applyModifyAvatarAction(builder, change);
|
||||
|
||||
applyModifyDisappearingMessagesTimerAction(builder, change);
|
||||
|
@ -403,6 +405,12 @@ public final class DecryptedGroupUtil {
|
|||
}
|
||||
}
|
||||
|
||||
protected static void applyModifyDescriptionAction(DecryptedGroup.Builder builder, DecryptedGroupChange change) {
|
||||
if (change.hasNewDescription()) {
|
||||
builder.setDescription(change.getNewDescription().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
protected static void applyModifyAvatarAction(DecryptedGroup.Builder builder, DecryptedGroupChange change) {
|
||||
if (change.hasNewAvatar()) {
|
||||
builder.setAvatar(change.getNewAvatar().getValue());
|
||||
|
@ -578,7 +586,8 @@ public final class DecryptedGroupUtil {
|
|||
change.getNewRequestingMembersCount() == 0 && // field 16
|
||||
change.getDeleteRequestingMembersCount() == 0 && // field 17
|
||||
change.getPromoteRequestingMembersCount() == 0 && // field 18
|
||||
change.getNewInviteLinkPassword().size() == 0; // field 19
|
||||
change.getNewInviteLinkPassword().size() == 0 && // field 19
|
||||
!change.hasNewDescription(); // field 20
|
||||
}
|
||||
|
||||
static boolean isSet(AccessControl.AccessRequired newAttributeAccess) {
|
||||
|
|
|
@ -102,4 +102,9 @@ final class GroupChangeActionsBuilderChangeSetModifier implements ChangeSetModif
|
|||
public void removePromoteRequestingMembers(int i) {
|
||||
result.removePromoteRequestingMembers(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearModifyDescription() {
|
||||
result.clearModifyDescription();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ public final class GroupChangeReconstruct {
|
|||
builder.setNewTitle(DecryptedString.newBuilder().setValue(toState.getTitle()));
|
||||
}
|
||||
|
||||
if (!fromState.getDescription().equals(toState.getDescription())) {
|
||||
builder.setNewDescription(DecryptedString.newBuilder().setValue(toState.getDescription()));
|
||||
}
|
||||
|
||||
if (!fromState.getAvatar().equals(toState.getAvatar())) {
|
||||
builder.setNewAvatar(DecryptedString.newBuilder().setValue(toState.getAvatar()));
|
||||
}
|
||||
|
|
|
@ -40,7 +40,8 @@ public final class GroupChangeUtil {
|
|||
change.getAddRequestingMembersCount() == 0 && // field 16
|
||||
change.getDeleteRequestingMembersCount() == 0 && // field 17
|
||||
change.getPromoteRequestingMembersCount() == 0 && // field 18
|
||||
!change.hasModifyInviteLinkPassword(); // field 19
|
||||
!change.hasModifyInviteLinkPassword() && // field 19
|
||||
!change.hasModifyDescription(); // field 20
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,6 +134,7 @@ public final class GroupChangeUtil {
|
|||
resolveField16AddRequestingMembers (conflictingChange, changeSetModifier, fullMembersByUuid, pendingMembersByUuid);
|
||||
resolveField17DeleteMembers (conflictingChange, changeSetModifier, requestingMembersByUuid);
|
||||
resolveField18PromoteRequestingMembers (conflictingChange, changeSetModifier, requestingMembersByUuid);
|
||||
resolveField20ModifyDescription (groupState, conflictingChange, changeSetModifier);
|
||||
}
|
||||
|
||||
private static void resolveField3AddMembers(DecryptedGroupChange conflictingChange, ChangeSetModifier result, HashMap<ByteString, DecryptedMember> fullMembersByUuid, HashMap<ByteString, DecryptedPendingMember> pendingMembersByUuid) {
|
||||
|
@ -302,4 +304,10 @@ public final class GroupChangeUtil {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void resolveField20ModifyDescription(DecryptedGroup groupState, DecryptedGroupChange conflictingChange, ChangeSetModifier result) {
|
||||
if (conflictingChange.hasNewDescription() && conflictingChange.getNewDescription().getValue().equals(groupState.getDescription())) {
|
||||
result.clearModifyDescription();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,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 = 1;
|
||||
public static final int HIGHEST_KNOWN_EPOCH = 2;
|
||||
|
||||
private final ServerPublicParams serverPublicParams;
|
||||
private final ClientZkProfileOperations clientZkProfileOperations;
|
||||
|
@ -149,6 +149,10 @@ public final class GroupsV2Operations {
|
|||
.setTitle(encryptTitle(title)));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.ModifyDescriptionAction.Builder createModifyGroupDescription(final String description) {
|
||||
return GroupChange.Actions.ModifyDescriptionAction.newBuilder().setDescription(encryptDescription(description));
|
||||
}
|
||||
|
||||
public GroupChange.Actions.Builder createModifyGroupMembershipChange(Set<GroupCandidate> membersToAdd, UUID selfUuid) {
|
||||
final GroupOperations groupOperations = forGroup(groupSecretParams);
|
||||
|
||||
|
@ -381,6 +385,7 @@ public final class GroupsV2Operations {
|
|||
|
||||
return DecryptedGroup.newBuilder()
|
||||
.setTitle(decryptTitle(group.getTitle()))
|
||||
.setDescription(decryptDescription(group.getDescription()))
|
||||
.setAvatar(group.getAvatar())
|
||||
.setAccessControl(group.getAccessControl())
|
||||
.setRevision(group.getRevision())
|
||||
|
@ -565,6 +570,11 @@ public final class GroupsV2Operations {
|
|||
builder.setNewInviteLinkPassword(actions.getModifyInviteLinkPassword().getInviteLinkPassword());
|
||||
}
|
||||
|
||||
// Field 20
|
||||
if (actions.hasModifyDescription()) {
|
||||
builder.setNewDescription(DecryptedString.newBuilder().setValue(decryptDescription(actions.getModifyDescription().getDescription())));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
@ -576,6 +586,7 @@ public final class GroupsV2Operations {
|
|||
.setAddFromInviteLink(joinInfo.getAddFromInviteLink())
|
||||
.setRevision(joinInfo.getRevision())
|
||||
.setPendingAdminApproval(joinInfo.getPendingAdminApproval())
|
||||
.setDescription(decryptDescription(joinInfo.getDescription()))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -708,6 +719,20 @@ public final class GroupsV2Operations {
|
|||
return decryptBlob(cipherText).getTitle().trim();
|
||||
}
|
||||
|
||||
ByteString encryptDescription(String description) {
|
||||
try {
|
||||
GroupAttributeBlob blob = GroupAttributeBlob.newBuilder().setDescription(description).build();
|
||||
|
||||
return ByteString.copyFrom(clientZkGroupCipher.encryptBlob(blob.toByteArray()));
|
||||
} catch (VerificationFailedException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String decryptDescription(ByteString cipherText) {
|
||||
return decryptBlob(cipherText).getDescription().trim();
|
||||
}
|
||||
|
||||
private int decryptDisappearingMessagesTimer(ByteString encryptedTimerMessage) {
|
||||
return decryptBlob(encryptedTimerMessage).getDisappearingMessagesDuration();
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ message DecryptedGroup {
|
|||
repeated DecryptedPendingMember pendingMembers = 8;
|
||||
repeated DecryptedRequestingMember requestingMembers = 9;
|
||||
bytes inviteLinkPassword = 10;
|
||||
string description = 11;
|
||||
}
|
||||
|
||||
// Decrypted version of message GroupChange.Actions
|
||||
|
@ -84,6 +85,7 @@ message DecryptedGroupChange {
|
|||
repeated bytes deleteRequestingMembers = 17;
|
||||
repeated DecryptedApproveMember promoteRequestingMembers = 18;
|
||||
bytes newInviteLinkPassword = 19;
|
||||
DecryptedString newDescription = 20;
|
||||
}
|
||||
|
||||
message DecryptedString {
|
||||
|
@ -101,4 +103,5 @@ message DecryptedGroupJoinInfo {
|
|||
AccessControl.AccessRequired addFromInviteLink = 5;
|
||||
uint32 revision = 6;
|
||||
bool pendingAdminApproval = 7;
|
||||
string description = 8;
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ message Group {
|
|||
repeated PendingMember pendingMembers = 8;
|
||||
repeated RequestingMember requestingMembers = 9;
|
||||
bytes inviteLinkPassword = 10;
|
||||
bytes description = 11;
|
||||
}
|
||||
|
||||
message GroupChange {
|
||||
|
@ -123,6 +124,10 @@ message GroupChange {
|
|||
bytes title = 1;
|
||||
}
|
||||
|
||||
message ModifyDescriptionAction {
|
||||
bytes description = 1;
|
||||
}
|
||||
|
||||
message ModifyAvatarAction {
|
||||
string avatar = 1;
|
||||
}
|
||||
|
@ -166,6 +171,7 @@ message GroupChange {
|
|||
repeated DeleteRequestingMemberAction deleteRequestingMembers = 17;
|
||||
repeated PromoteRequestingMemberAction promoteRequestingMembers = 18;
|
||||
ModifyInviteLinkPasswordAction modifyInviteLinkPassword = 19;
|
||||
ModifyDescriptionAction modifyDescription = 20;
|
||||
}
|
||||
|
||||
bytes actions = 1;
|
||||
|
@ -187,6 +193,7 @@ message GroupAttributeBlob {
|
|||
string title = 1;
|
||||
bytes avatar = 2;
|
||||
uint32 disappearingMessagesDuration = 3;
|
||||
string description = 4;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,6 +216,7 @@ message GroupJoinInfo {
|
|||
AccessControl.AccessRequired addFromInviteLink = 5;
|
||||
uint32 revision = 6;
|
||||
bool pendingAdminApproval = 7;
|
||||
bytes description = 8;
|
||||
}
|
||||
|
||||
message GroupExternalCredential {
|
||||
|
|
|
@ -45,7 +45,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(),
|
||||
19, maxFieldFound);
|
||||
20, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -576,6 +576,24 @@ public final class DecryptedGroupUtil_apply_Test {
|
|||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void description() throws NotAbleToApplyGroupV2ChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
.setRevision(10)
|
||||
.setDescription("Old description")
|
||||
.build(),
|
||||
DecryptedGroupChange.newBuilder()
|
||||
.setRevision(11)
|
||||
.setNewDescription(DecryptedString.newBuilder().setValue("New Description").build())
|
||||
.build());
|
||||
|
||||
assertEquals(DecryptedGroup.newBuilder()
|
||||
.setRevision(11)
|
||||
.setDescription("New Description")
|
||||
.build(),
|
||||
newGroup);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void avatar() throws NotAbleToApplyGroupV2ChangeException {
|
||||
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
|
||||
|
|
|
@ -35,7 +35,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(),
|
||||
19, maxFieldFound);
|
||||
20, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -203,7 +203,7 @@ public final class DecryptedGroupUtil_empty_Test {
|
|||
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test
|
||||
public void not_empty_with_a_new_invite_link_password_19() {
|
||||
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
|
||||
.setNewInviteLinkPassword(ByteString.copyFrom(new byte[16]))
|
||||
|
@ -212,4 +212,14 @@ public final class DecryptedGroupUtil_empty_Test {
|
|||
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_description_field_20() {
|
||||
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
|
||||
.setNewDescription(DecryptedString.newBuilder().setValue("New description"))
|
||||
.build();
|
||||
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
|
||||
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,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(),
|
||||
10, maxFieldFound);
|
||||
11, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -74,6 +74,16 @@ public final class GroupChangeReconstructTest {
|
|||
assertEquals(DecryptedGroupChange.newBuilder().setNewTitle(DecryptedString.newBuilder().setValue("B")).build(), decryptedGroupChange);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void description_change() {
|
||||
DecryptedGroup from = DecryptedGroup.newBuilder().setDescription("A").build();
|
||||
DecryptedGroup to = DecryptedGroup.newBuilder().setDescription("B").build();
|
||||
|
||||
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
|
||||
|
||||
assertEquals(DecryptedGroupChange.newBuilder().setNewDescription(DecryptedString.newBuilder().setValue("B")).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(),
|
||||
19, maxFieldFound);
|
||||
20, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -180,4 +180,13 @@ public final class GroupChangeUtil_changeIsEmpty_Test {
|
|||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void not_empty_with_modify_description_field_20() {
|
||||
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
|
||||
.setModifyDescription(GroupChange.Actions.ModifyDescriptionAction.getDefaultInstance())
|
||||
.build();
|
||||
|
||||
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,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(),
|
||||
19, maxFieldFound);
|
||||
20, maxFieldFound);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,7 +59,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(),
|
||||
19, maxFieldFound);
|
||||
20, maxFieldFound);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -72,7 +72,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(),
|
||||
10, maxFieldFound);
|
||||
11, maxFieldFound);
|
||||
}
|
||||
|
||||
|
||||
|
@ -672,4 +672,38 @@ public final class GroupChangeUtil_resolveConflict_Test {
|
|||
.build();
|
||||
assertEquals(expected, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_20__description_change_is_preserved() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setDescription("Existing title")
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewDescription(DecryptedString.newBuilder().setValue("New title").build())
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyDescription(GroupChange.Actions.ModifyDescriptionAction.newBuilder().setDescription(ByteString.copyFrom("New title encrypted".getBytes())))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertEquals(change, resolvedActions);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_20__no_description_change_is_removed() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setDescription("Existing title")
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewDescription(DecryptedString.newBuilder().setValue("Existing title").build())
|
||||
.build();
|
||||
GroupChange.Actions change = GroupChange.Actions.newBuilder()
|
||||
.setModifyDescription(GroupChange.Actions.ModifyDescriptionAction.newBuilder().setDescription(ByteString.copyFrom("Existing title encrypted".getBytes())))
|
||||
.build();
|
||||
|
||||
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
|
||||
|
||||
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
|
||||
}
|
||||
}
|
|
@ -39,7 +39,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(),
|
||||
19, maxFieldFound);
|
||||
20, maxFieldFound);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -52,7 +52,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(),
|
||||
10, maxFieldFound);
|
||||
11, maxFieldFound);
|
||||
}
|
||||
|
||||
|
||||
|
@ -543,4 +543,32 @@ public final class GroupChangeUtil_resolveConflict_decryptedOnly_Test {
|
|||
|
||||
assertEquals(decryptedChange, resolvedChanges);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_20__description_change_is_preserved() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setDescription("Existing description")
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewDescription(DecryptedString.newBuilder().setValue("New description").build())
|
||||
.build();
|
||||
|
||||
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
|
||||
|
||||
assertEquals(decryptedChange, resolvedChanges);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void field_20__no_description_change_is_removed() {
|
||||
DecryptedGroup groupState = DecryptedGroup.newBuilder()
|
||||
.setDescription("Existing description")
|
||||
.build();
|
||||
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
|
||||
.setNewDescription(DecryptedString.newBuilder().setValue("Existing description").build())
|
||||
.build();
|
||||
|
||||
DecryptedGroupChange resolvedChanges = GroupChangeUtil.resolveConflict(groupState, decryptedChange).build();
|
||||
|
||||
assertTrue(DecryptedGroupUtil.changeIsEmpty(resolvedChanges));
|
||||
}
|
||||
}
|
|
@ -41,7 +41,7 @@ public final class GroupsV2Operations_decrypt_groupJoinInfo_Test {
|
|||
int maxFieldFound = getMaxDeclaredFieldNumber(GroupJoinInfo.class);
|
||||
|
||||
assertEquals("GroupOperations and its tests need updating to account for new fields on " + GroupJoinInfo.class.getName(),
|
||||
7, maxFieldFound);
|
||||
8, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -131,4 +131,15 @@ public final class GroupsV2Operations_decrypt_groupJoinInfo_Test {
|
|||
|
||||
assertFalse(decryptedGroupJoinInfo.getPendingAdminApproval());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decrypt_description_field_8() {
|
||||
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
|
||||
.setDescription(groupOperations.encryptDescription("Description!"))
|
||||
.build();
|
||||
|
||||
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
|
||||
|
||||
assertEquals("Description!", decryptedGroupJoinInfo.getDescription());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,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(),
|
||||
10, maxFieldFound);
|
||||
11, maxFieldFound);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -272,6 +272,17 @@ public final class GroupsV2Operations_decrypt_group_Test {
|
|||
assertEquals(password, decryptedGroup.getInviteLinkPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decrypt_description_field_11() throws VerificationFailedException, InvalidGroupStateException {
|
||||
Group group = Group.newBuilder()
|
||||
.setDescription(groupOperations.encryptDescription("Description!"))
|
||||
.build();
|
||||
|
||||
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
|
||||
|
||||
assertEquals("Description!", decryptedGroup.getDescription());
|
||||
}
|
||||
|
||||
private ByteString encryptProfileKey(UUID uuid, ProfileKey profileKey) {
|
||||
return ByteString.copyFrom(new ClientZkGroupCipher(groupSecretParams).encryptProfileKey(profileKey, uuid).serialize());
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue