Add support for Group V2 description field.

fork-5.53.8
Cody Henthorne 2021-05-07 13:43:31 -04:00 zatwierdzone przez Greyson Parrelli
rodzic b3aec58e69
commit 8c9df8d3be
56 zmienionych plików z 892 dodań i 117 usunięć

Wyświetl plik

@ -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'

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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)}.
*/

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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];
}
};
}

Wyświetl plik

@ -21,6 +21,10 @@ public final class GroupDetails {
return joinInfo.getTitle();
}
public @NonNull String getGroupDescription() {
return joinInfo.getDescription();
}
public @Nullable byte[] getAvatarBytes() {
return avatarBytes;
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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.
*/

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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"

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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'],

Wyświetl plik

@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx3072m
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=true
kapt.incremental.apt=false

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -102,4 +102,9 @@ final class GroupChangeActionsBuilderChangeSetModifier implements ChangeSetModif
public void removePromoteRequestingMembers(int i) {
result.removePromoteRequestingMembers(i);
}
@Override
public void clearModifyDescription() {
result.clearModifyDescription();
}
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -20,7 +20,7 @@ public final class GroupChangeUtil_changeIsEmpty_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(GroupChange.Actions.class);
assertEquals("GroupChangeUtil and its tests need updating to account for new fields on " + GroupChange.Actions.class.getName(),
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));
}
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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