diff --git a/app/build.gradle b/app/build.gradle
index 52bcba900..3b1d28c15 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -11,6 +11,8 @@ apply plugin: 'witness'
apply plugin: 'org.jlleitschuh.gradle.ktlint'
apply from: 'translations.gradle'
apply from: 'witness-verifications.gradle'
+apply plugin: 'org.jetbrains.kotlin.android'
+apply plugin: 'app.cash.exhaustive'
repositories {
maven {
@@ -75,6 +77,7 @@ android {
useLibrary 'org.apache.http.legacy'
kotlinOptions {
+ jvmTarget = "1.8"
freeCompilerArgs = ["-Xallow-result-return-type"]
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9dbb93c53..6fa0306e6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -308,14 +308,6 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
-
-
-
-
+
+
+
+
diff --git a/app/src/main/java/org/thoughtcrime/securesms/animation/transitions/CircleAvatarTransition.kt b/app/src/main/java/org/thoughtcrime/securesms/animation/transitions/CircleAvatarTransition.kt
new file mode 100644
index 000000000..1fda6fe7d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/animation/transitions/CircleAvatarTransition.kt
@@ -0,0 +1,97 @@
+package org.thoughtcrime.securesms.animation.transitions
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
+import android.animation.TypeEvaluator
+import android.content.Context
+import android.transition.Transition
+import android.transition.TransitionValues
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import androidx.annotation.RequiresApi
+import org.thoughtcrime.securesms.components.AvatarImageView
+
+private const val POSITION_ON_SCREEN = "signal.circleavatartransition.positiononscreen"
+private const val WIDTH = "signal.circleavatartransition.width"
+private const val HEIGHT = "signal.circleavatartransition.height"
+
+/**
+ * Custom transition for Circular avatars, because once you have multiple things animating stuff was getting broken and weird.
+ */
+@RequiresApi(21)
+class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
+ override fun captureStartValues(transitionValues: TransitionValues) {
+ captureValues(transitionValues)
+ }
+
+ override fun captureEndValues(transitionValues: TransitionValues) {
+ captureValues(transitionValues)
+ }
+
+ private fun captureValues(transitionValues: TransitionValues) {
+ val view: View = transitionValues.view
+
+ if (view is AvatarImageView) {
+ val topLeft = intArrayOf(0, 0)
+ view.getLocationOnScreen(topLeft)
+ transitionValues.values[POSITION_ON_SCREEN] = topLeft
+ transitionValues.values[WIDTH] = view.measuredWidth
+ transitionValues.values[HEIGHT] = view.measuredHeight
+ }
+ }
+
+ override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
+ if (startValues == null || endValues == null) {
+ return null
+ }
+
+ val view: View = endValues.view
+ if (view !is AvatarImageView || view.transitionName != "avatar") {
+ return null
+ }
+
+ val startCoords: IntArray = startValues.values[POSITION_ON_SCREEN] as? IntArray ?: intArrayOf(0, 0).apply { view.getLocationOnScreen(this) }
+ val endCoords: IntArray = endValues.values[POSITION_ON_SCREEN] as? IntArray ?: intArrayOf(0, 0).apply { view.getLocationOnScreen(this) }
+
+ val startWidth: Int = startValues.values[WIDTH] as? Int ?: view.measuredWidth
+ val endWidth: Int = endValues.values[WIDTH] as? Int ?: view.measuredWidth
+
+ val startHeight: Int = startValues.values[HEIGHT] as? Int ?: view.measuredHeight
+ val endHeight: Int = endValues.values[HEIGHT] as? Int ?: view.measuredHeight
+
+ val startHeightOffset = (endHeight - startHeight) / 2f
+ val startWidthOffset = (endWidth - startWidth) / 2f
+
+ val translateXHolder = PropertyValuesHolder.ofFloat("translationX", startCoords[0] - endCoords[0] - startWidthOffset, 0f).apply {
+ setEvaluator(FloatInterpolatorEvaluator(DecelerateInterpolator()))
+ }
+ val translateYHolder = PropertyValuesHolder.ofFloat("translationY", startCoords[1] - endCoords[1] - startHeightOffset, 0f).apply {
+ setEvaluator(FloatInterpolatorEvaluator(AccelerateInterpolator()))
+ }
+
+ val widthRatio = startWidth.toFloat() / endWidth
+ val scaleXHolder = PropertyValuesHolder.ofFloat("scaleX", widthRatio, 1f)
+
+ val heightRatio = startHeight.toFloat() / endHeight
+ val scaleYHolder = PropertyValuesHolder.ofFloat("scaleY", heightRatio, 1f)
+
+ return ObjectAnimator.ofPropertyValuesHolder(view, translateXHolder, translateYHolder, scaleXHolder, scaleYHolder)
+ }
+
+ private class FloatInterpolatorEvaluator(
+ private val interpolator: Interpolator
+ ) : TypeEvaluator {
+
+ override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
+ val interpolatedFraction = interpolator.getInterpolation(fraction)
+ val delta = endValue - startValue
+
+ return delta * interpolatedFraction + startValue
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/animation/transitions/WipeDownTransition.kt b/app/src/main/java/org/thoughtcrime/securesms/animation/transitions/WipeDownTransition.kt
new file mode 100644
index 000000000..bc1fb2bcc
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/animation/transitions/WipeDownTransition.kt
@@ -0,0 +1,56 @@
+package org.thoughtcrime.securesms.animation.transitions
+
+import android.animation.Animator
+import android.animation.ObjectAnimator
+import android.animation.RectEvaluator
+import android.content.Context
+import android.graphics.Rect
+import android.transition.Transition
+import android.transition.TransitionValues
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.RequiresApi
+import androidx.fragment.app.FragmentContainerView
+
+private const val BOUNDS = "signal.wipedowntransition.bottom"
+
+/**
+ * WipeDownTransition will animate the bottom position of a view such that it "wipes" down the screen to a final position.
+ */
+@RequiresApi(21)
+class WipeDownTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
+ override fun captureStartValues(transitionValues: TransitionValues) {
+ captureValues(transitionValues)
+ }
+
+ override fun captureEndValues(transitionValues: TransitionValues) {
+ captureValues(transitionValues)
+ }
+
+ private fun captureValues(transitionValues: TransitionValues) {
+ val view: View = transitionValues.view
+
+ if (view is ViewGroup) {
+ val rect = Rect()
+ view.getLocalVisibleRect(rect)
+ transitionValues.values[BOUNDS] = rect
+ }
+ }
+
+ override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
+ if (startValues == null || endValues == null) {
+ return null
+ }
+
+ val view: View = endValues.view
+ if (view !is FragmentContainerView) {
+ return null
+ }
+
+ val startBottom: Rect = startValues.values[BOUNDS] as? Rect ?: Rect().apply { view.getLocalVisibleRect(this) }
+ val endBottom: Rect = endValues.values[BOUNDS] as? Rect ?: Rect().apply { view.getLocalVisibleRect(this) }
+
+ return ObjectAnimator.ofObject(view, "clipBounds", RectEvaluator(), startBottom, endBottom)
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java
index db0ab2350..8025dc393 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java
@@ -6,12 +6,15 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import androidx.annotation.DrawableRes;
+import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.Px;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.FragmentActivity;
@@ -20,21 +23,23 @@ import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
+import com.bumptech.glide.request.target.SimpleTarget;
+import com.bumptech.glide.request.transition.Transition;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
-import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
-import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.BlurTransformation;
import org.thoughtcrime.securesms.util.ThemeUtil;
@@ -74,6 +79,7 @@ public final class AvatarImageView extends AppCompatImageView {
private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
private boolean blurred;
private ChatColors chatColors;
+ private FixedSizeTarget fixedSizeTarget;
private @Nullable RecipientContactPhoto recipientContactPhoto;
private @NonNull Drawable unknownRecipientDrawable;
@@ -93,8 +99,8 @@ public final class AvatarImageView extends AppCompatImageView {
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AvatarImageView, 0, 0);
- inverted = typedArray.getBoolean(R.styleable.AvatarImageView_inverted, false);
- size = typedArray.getInt(R.styleable.AvatarImageView_fallbackImageSize, SIZE_LARGE);
+ inverted = typedArray.getBoolean(R.styleable.AvatarImageView_inverted, false);
+ size = typedArray.getInt(R.styleable.AvatarImageView_fallbackImageSize, SIZE_LARGE);
typedArray.recycle();
}
@@ -105,6 +111,11 @@ public final class AvatarImageView extends AppCompatImageView {
chatColors = null;
}
+ @Override
+ public void setClipBounds(Rect clipBounds) {
+ super.setClipBounds(clipBounds);
+ }
+
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
@@ -148,6 +159,10 @@ public final class AvatarImageView extends AppCompatImageView {
}
}
+ public AvatarOptions.Builder buildOptions() {
+ return new AvatarOptions.Builder(this);
+ }
+
/**
* Shows self as the note to self icon.
*/
@@ -167,11 +182,22 @@ public final class AvatarImageView extends AppCompatImageView {
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) {
+ setAvatar(requestManager, recipient, new AvatarOptions.Builder(this)
+ .withUseSelfProfileAvatar(useSelfProfileAvatar)
+ .withQuickContactEnabled(quickContactEnabled)
+ .build());
+ }
+
+ private void setAvatar(@Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
+ setAvatar(GlideApp.with(this), recipient, avatarOptions);
+ }
+
+ private void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
if (recipient != null) {
- RecipientContactPhoto photo = (recipient.isSelf() && useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
- new ProfileContactPhoto(Recipient.self(),
- Recipient.self().getProfileAvatar()))
- : new RecipientContactPhoto(recipient);
+ RecipientContactPhoto photo = (recipient.isSelf() && avatarOptions.useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
+ new ProfileContactPhoto(Recipient.self(),
+ Recipient.self().getProfileAvatar()))
+ : new RecipientContactPhoto(recipient);
boolean shouldBlur = recipient.shouldBlurAvatar();
ChatColors chatColors = recipient.getChatColors();
@@ -184,6 +210,10 @@ public final class AvatarImageView extends AppCompatImageView {
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
+ if (fixedSizeTarget != null) {
+ requestManager.clear(fixedSizeTarget);
+ }
+
if (photo.contactPhoto != null) {
List> transforms = new ArrayList<>();
@@ -193,19 +223,26 @@ public final class AvatarImageView extends AppCompatImageView {
transforms.add(new CircleCrop());
blurred = shouldBlur;
- requestManager.load(photo.contactPhoto)
- .fallback(fallbackContactPhotoDrawable)
- .error(fallbackContactPhotoDrawable)
- .diskCacheStrategy(DiskCacheStrategy.ALL)
- .downsample(DownsampleStrategy.CENTER_INSIDE)
- .transform(new MultiTransformation<>(transforms))
- .into(this);
+ GlideRequest request = requestManager.load(photo.contactPhoto)
+ .fallback(fallbackContactPhotoDrawable)
+ .error(fallbackContactPhotoDrawable)
+ .diskCacheStrategy(DiskCacheStrategy.ALL)
+ .downsample(DownsampleStrategy.CENTER_INSIDE)
+ .transform(new MultiTransformation<>(transforms));
+
+ if (avatarOptions.fixedSize > 0) {
+ fixedSizeTarget = new FixedSizeTarget(avatarOptions.fixedSize);
+ request.into(fixedSizeTarget);
+ } else {
+ request.into(this);
+ }
+
} else {
setImageDrawable(fallbackContactPhotoDrawable);
}
}
- setAvatarClickHandler(recipient, quickContactEnabled);
+ setAvatarClickHandler(recipient, avatarOptions.quickContactEnabled);
} else {
recipientContactPhoto = null;
requestManager.clear(this);
@@ -225,15 +262,15 @@ public final class AvatarImageView extends AppCompatImageView {
super.setOnClickListener(v -> {
Context context = getContext();
if (recipient.isPushGroup()) {
- context.startActivity(ManageGroupActivity.newIntent(context, recipient.requireGroupId().requirePush()),
- ManageGroupActivity.createTransitionBundle(context, this));
+ context.startActivity(ConversationSettingsActivity.forGroup(context, recipient.requireGroupId().requirePush()),
+ ConversationSettingsActivity.createTransitionBundle(context, this));
} else {
if (context instanceof FragmentActivity) {
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
.show(((FragmentActivity) context).getSupportFragmentManager(), "BOTTOM");
} else {
- context.startActivity(ManageRecipientActivity.newIntent(context, recipient.getId()),
- ManageRecipientActivity.createTransitionBundle(context, this));
+ context.startActivity(ConversationSettingsActivity.forRecipient(context, recipient.getId()),
+ ConversationSettingsActivity.createTransitionBundle(context, this));
}
}
});
@@ -294,4 +331,65 @@ public final class AvatarImageView extends AppCompatImageView {
Objects.equals(other.contactPhoto, contactPhoto);
}
}
+
+ private final class FixedSizeTarget extends SimpleTarget {
+
+ FixedSizeTarget(int size) {
+ super(size, size);
+ }
+
+ @Override
+ public void onResourceReady(@NonNull Drawable resource, @Nullable Transition super Drawable> transition) {
+ setImageDrawable(resource);
+ }
+ }
+
+ public static final class AvatarOptions {
+
+ private final boolean quickContactEnabled;
+ private final boolean useSelfProfileAvatar;
+ private final int fixedSize;
+
+ private AvatarOptions(@NonNull Builder builder) {
+ this.quickContactEnabled = builder.quickContactEnabled;
+ this.useSelfProfileAvatar = builder.useSelfProfileAvatar;
+ this.fixedSize = builder.fixedSize;
+ }
+
+ public static final class Builder {
+
+ private final AvatarImageView avatarImageView;
+
+ private boolean quickContactEnabled = false;
+ private boolean useSelfProfileAvatar = false;
+ private int fixedSize = -1;
+
+ private Builder(@NonNull AvatarImageView avatarImageView) {
+ this.avatarImageView = avatarImageView;
+ }
+
+ public @NonNull Builder withQuickContactEnabled(boolean quickContactEnabled) {
+ this.quickContactEnabled = quickContactEnabled;
+ return this;
+ }
+
+ public @NonNull Builder withUseSelfProfileAvatar(boolean useSelfProfileAvatar) {
+ this.useSelfProfileAvatar = useSelfProfileAvatar;
+ return this;
+ }
+
+ public @NonNull Builder withFixedSize(@Px @IntRange(from = 1) int fixedSize) {
+ this.fixedSize = fixedSize;
+ return this;
+ }
+
+ public AvatarOptions build() {
+ return new AvatarOptions(this);
+ }
+
+ public void load(@Nullable Recipient recipient) {
+ avatarImageView.setAvatar(recipient, build());
+ }
+ }
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java
index e6b8b2eb4..218bfa42a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/FromTextView.java
@@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
@@ -9,11 +12,15 @@ import android.text.style.StyleSpan;
import android.util.AttributeSet;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.util.ViewUtil;
+
+import java.util.Objects;
public class FromTextView extends EmojiTextView {
@@ -66,9 +73,16 @@ public class FromTextView extends EmojiTextView {
setText(builder);
if (recipient.isBlocked()) setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
- else if (recipient.isMuted()) setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_volume_off_grey600_18dp, 0, 0, 0);
+ else if (recipient.isMuted()) setCompoundDrawablesWithIntrinsicBounds(getMuted(), null, null, null);
else setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
+ private Drawable getMuted() {
+ Drawable mutedDrawable = Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.ic_bell_disabled_16));
+ mutedDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18));
+ mutedDrawable.setColorFilter(new PorterDuffColorFilter(ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary), PorterDuff.Mode.SRC_IN));
+
+ return mutedDrawable;
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java
index 8874598ba..753604dce 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java
@@ -3,6 +3,12 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.graphics.drawable.shapes.Shape;
import android.net.Uri;
import android.util.AttributeSet;
import android.view.View;
@@ -12,6 +18,7 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
+import androidx.core.content.ContextCompat;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
@@ -41,6 +48,7 @@ import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.video.VideoPlayer;
import org.whispersystems.libsignal.util.guava.Optional;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import java.util.Objects;
@@ -93,6 +101,7 @@ public class ThumbnailView extends FrameLayout {
this.blurhash = findViewById(R.id.thumbnail_blurhash);
this.playOverlay = findViewById(R.id.play_overlay);
this.captionIcon = findViewById(R.id.thumbnail_caption_icon);
+
super.setOnClickListener(new ThumbnailClickDispatcher());
if (attrs != null) {
@@ -103,9 +112,18 @@ public class ThumbnailView extends FrameLayout {
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius));
fit = typedArray.getInt(R.styleable.ThumbnailView_thumbnail_fit, 0) == 1 ? new FitCenter() : new CenterCrop();
+
+ int transparentOverlayColor = typedArray.getColor(R.styleable.ThumbnailView_transparent_overlay_color, -1);
+ if (transparentOverlayColor > 0) {
+ image.setColorFilter(new PorterDuffColorFilter(transparentOverlayColor, PorterDuff.Mode.SRC_ATOP));
+ } else {
+ image.setColorFilter(null);
+ }
+
typedArray.recycle();
} else {
radius = getResources().getDimensionPixelSize(R.dimen.message_corner_collapse_radius);
+ image.setColorFilter(null);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsActivity.kt
index 2ba0a5331..136427f88 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsActivity.kt
@@ -8,10 +8,11 @@ import androidx.navigation.fragment.NavHostFragment
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
+import org.thoughtcrime.securesms.util.DynamicTheme
open class DSLSettingsActivity : PassphraseRequiredActivity() {
- private val dynamicTheme = DynamicNoActionBarTheme()
+ protected open val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
protected lateinit var navController: NavController
private set
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt
index dd10151d1..bea5a85f4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsAdapter.kt
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.MappingAdapter
import org.thoughtcrime.securesms.util.MappingViewHolder
import org.thoughtcrime.securesms.util.ViewUtil
+import org.thoughtcrime.securesms.util.visible
class DSLSettingsAdapter : MappingAdapter() {
init {
@@ -42,13 +43,9 @@ abstract class PreferenceViewHolder>(itemView: View) : Ma
it.isEnabled = model.isEnabled
}
- if (model.iconId != -1) {
- iconView.setImageResource(model.iconId)
- iconView.visibility = View.VISIBLE
- } else {
- iconView.setImageDrawable(null)
- iconView.visibility = View.GONE
- }
+ val icon = model.icon?.resolve(context)
+ iconView.setImageDrawable(icon)
+ iconView.visible = icon != null
val title = model.title?.resolve(context)
if (title != null) {
@@ -93,13 +90,31 @@ class RadioListPreferenceViewHolder(itemView: View) : PreferenceViewHolder
- model.onSelected(which)
- dialog.dismiss()
+ if (model.confirmAction) {
+ selection = which
+ } else {
+ model.onSelected(which)
+ dialog.dismiss()
+ }
}
- .show()
+
+ if (model.confirmAction) {
+ builder
+ .setPositiveButton(android.R.string.ok) { dialog, _ ->
+ model.onSelected(selection)
+ dialog.dismiss()
+ }
+ .setNegativeButton(android.R.string.cancel) { dialog, _ ->
+ dialog.dismiss()
+ }
+ .show()
+ } else {
+ builder.show()
+ }
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt
index a29b6431e..6a82f1ebe 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsFragment.kt
@@ -14,19 +14,21 @@ import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
abstract class DSLSettingsFragment(
- @StringRes private val titleId: Int,
+ @StringRes private val titleId: Int = -1,
@MenuRes private val menuId: Int = -1,
@LayoutRes layoutId: Int = R.layout.dsl_settings_fragment
) : Fragment(layoutId) {
private lateinit var recyclerView: RecyclerView
- private lateinit var toolbarShadowHelper: ToolbarShadowHelper
+ private lateinit var scrollAnimationHelper: OnScrollAnimationHelper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
val toolbarShadow: View = view.findViewById(R.id.toolbar_shadow)
- toolbar.setTitle(titleId)
+ if (titleId != -1) {
+ toolbar.setTitle(titleId)
+ }
toolbar.setNavigationOnClickListener {
requireActivity().onBackPressed()
@@ -39,18 +41,22 @@ abstract class DSLSettingsFragment(
recyclerView = view.findViewById(R.id.recycler)
recyclerView.edgeEffectFactory = EdgeEffectFactory()
- toolbarShadowHelper = ToolbarShadowHelper(toolbarShadow)
+ scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
val adapter = DSLSettingsAdapter()
recyclerView.adapter = adapter
- recyclerView.addOnScrollListener(toolbarShadowHelper)
+ recyclerView.addOnScrollListener(scrollAnimationHelper)
bindAdapter(adapter)
}
override fun onResume() {
super.onResume()
- toolbarShadowHelper.onScrolled(recyclerView, 0, 0)
+ scrollAnimationHelper.onScrolled(recyclerView, 0, 0)
+ }
+
+ protected open fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
+ return ToolbarShadowAnimationHelper(toolbarShadow)
}
abstract fun bindAdapter(adapter: DSLSettingsAdapter)
@@ -66,31 +72,54 @@ abstract class DSLSettingsFragment(
}
}
- class ToolbarShadowHelper(private val toolbarShadow: View) : RecyclerView.OnScrollListener() {
+ abstract class OnScrollAnimationHelper : RecyclerView.OnScrollListener() {
+ private var lastAnimationState = AnimationState.NONE
- private var lastAnimationState = ToolbarAnimationState.NONE
+ protected open val duration: Long = 250L
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
- val newAnimationState =
- if (recyclerView.canScrollVertically(-1)) ToolbarAnimationState.SHOW else ToolbarAnimationState.HIDE
+ val newAnimationState = getAnimationState(recyclerView)
if (newAnimationState == lastAnimationState) {
return
}
when (newAnimationState) {
- ToolbarAnimationState.NONE -> throw AssertionError()
- ToolbarAnimationState.HIDE -> toolbarShadow.animate().alpha(0f)
- ToolbarAnimationState.SHOW -> toolbarShadow.animate().alpha(1f)
+ AnimationState.NONE -> throw AssertionError()
+ AnimationState.HIDE -> hide()
+ AnimationState.SHOW -> show()
}
lastAnimationState = newAnimationState
}
+
+ protected open fun getAnimationState(recyclerView: RecyclerView): AnimationState {
+ return if (recyclerView.canScrollVertically(-1)) AnimationState.SHOW else AnimationState.HIDE
+ }
+
+ protected abstract fun show()
+
+ protected abstract fun hide()
+
+ enum class AnimationState {
+ NONE,
+ HIDE,
+ SHOW
+ }
}
- private enum class ToolbarAnimationState {
- NONE,
- HIDE,
- SHOW
+ open class ToolbarShadowAnimationHelper(private val toolbarShadow: View) : OnScrollAnimationHelper() {
+
+ override fun show() {
+ toolbarShadow.animate()
+ .setDuration(duration)
+ .alpha(1f)
+ }
+
+ override fun hide() {
+ toolbarShadow.animate()
+ .setDuration(duration)
+ .alpha(0f)
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsIcon.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsIcon.kt
new file mode 100644
index 000000000..81ba8622e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/DSLSettingsIcon.kt
@@ -0,0 +1,42 @@
+package org.thoughtcrime.securesms.components.settings
+
+import android.content.Context
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.drawable.Drawable
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.core.content.ContextCompat
+import org.thoughtcrime.securesms.R
+
+const val NO_TINT = -1
+
+sealed class DSLSettingsIcon {
+
+ private data class FromResource(
+ @DrawableRes private val iconId: Int,
+ @ColorRes private val iconTintId: Int
+ ) : DSLSettingsIcon() {
+ override fun resolve(context: Context) = requireNotNull(ContextCompat.getDrawable(context, iconId)).apply {
+ if (iconTintId != NO_TINT) {
+ colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, iconTintId), PorterDuff.Mode.SRC_IN)
+ }
+ }
+ }
+
+ private data class FromDrawable(
+ private val drawable: Drawable
+ ) : DSLSettingsIcon() {
+ override fun resolve(context: Context): Drawable = drawable
+ }
+
+ abstract fun resolve(context: Context): Drawable
+
+ companion object {
+ @JvmStatic
+ fun from(@DrawableRes iconId: Int, @ColorRes iconTintId: Int = R.color.signal_icon_tint_primary): DSLSettingsIcon = FromResource(iconId, iconTintId)
+
+ @JvmStatic
+ fun from(drawable: Drawable): DSLSettingsIcon = FromDrawable(drawable)
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt
index 2a60bcd7c..b4860bf02 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
+import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
@@ -44,7 +45,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
clickPref(
title = DSLSettingsText.from(R.string.AccountSettingsFragment__account),
- iconId = R.drawable.ic_profile_circle_24,
+ icon = DSLSettingsIcon.from(R.drawable.ic_profile_circle_24),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_accountSettingsFragment)
}
@@ -52,7 +53,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
clickPref(
title = DSLSettingsText.from(R.string.preferences__linked_devices),
- iconId = R.drawable.ic_linked_devices_24,
+ icon = DSLSettingsIcon.from(R.drawable.ic_linked_devices_24),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_deviceActivity)
}
@@ -72,7 +73,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
clickPref(
title = DSLSettingsText.from(R.string.preferences__appearance),
- iconId = R.drawable.ic_appearance_24,
+ icon = DSLSettingsIcon.from(R.drawable.ic_appearance_24),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment)
}
@@ -80,7 +81,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__chats),
- iconId = R.drawable.ic_message_tinted_bitmap_24,
+ icon = DSLSettingsIcon.from(R.drawable.ic_message_tinted_bitmap_24),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment)
}
@@ -88,7 +89,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
clickPref(
title = DSLSettingsText.from(R.string.preferences__notifications),
- iconId = R.drawable.ic_bell_24,
+ icon = DSLSettingsIcon.from(R.drawable.ic_bell_24),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment)
}
@@ -96,7 +97,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
clickPref(
title = DSLSettingsText.from(R.string.preferences__privacy),
- iconId = R.drawable.ic_lock_24,
+ icon = DSLSettingsIcon.from(R.drawable.ic_lock_24),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_privacySettingsFragment)
}
@@ -104,7 +105,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
clickPref(
title = DSLSettingsText.from(R.string.preferences__data_and_storage),
- iconId = R.drawable.ic_archive_24dp,
+ icon = DSLSettingsIcon.from(R.drawable.ic_archive_24dp),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment)
}
@@ -114,7 +115,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
clickPref(
title = DSLSettingsText.from(R.string.preferences__help),
- iconId = R.drawable.ic_help_24,
+ icon = DSLSettingsIcon.from(R.drawable.ic_help_24),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_helpSettingsFragment)
}
@@ -122,7 +123,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
clickPref(
title = DSLSettingsText.from(R.string.AppSettingsFragment__invite_your_friends),
- iconId = R.drawable.ic_invite_24,
+ icon = DSLSettingsIcon.from(R.drawable.ic_invite_24),
onClick = {
Navigation.findNavController(requireView()).navigate(R.id.action_appSettingsFragment_to_inviteActivity)
}
@@ -130,7 +131,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
externalLinkPref(
title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
- iconId = R.drawable.ic_heart_24,
+ icon = DSLSettingsIcon.from(R.drawable.ic_heart_24),
linkId = R.string.donate_url
)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt
index f9aa9f59b..52f47a29e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt
@@ -289,7 +289,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
val radioListPreference: RadioListPreference
) : PreferenceModel(
title = radioListPreference.title,
- iconId = radioListPreference.iconId,
+ icon = radioListPreference.icon,
summary = radioListPreference.summary
) {
override fun areContentsTheSame(newItem: LedColorPreference): Boolean {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt
index 947d3fb42..50d524648 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/PrivacySettingsFragment.kt
@@ -430,7 +430,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
) : PreferenceModel(
title = clickPreference.title,
summary = clickPreference.summary,
- iconId = clickPreference.iconId,
+ icon = clickPreference.icon,
isEnabled = clickPreference.isEnabled
) {
override fun areContentsTheSame(newItem: ValueClickPreference): Boolean {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsActivity.kt
new file mode 100644
index 000000000..d200a50d3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsActivity.kt
@@ -0,0 +1,90 @@
+package org.thoughtcrime.securesms.components.settings.conversation
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.core.app.ActivityCompat
+import androidx.core.app.ActivityOptionsCompat
+import androidx.core.util.Pair
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.ParcelableGroupId
+import org.thoughtcrime.securesms.recipients.RecipientId
+import org.thoughtcrime.securesms.util.DynamicConversationSettingsTheme
+import org.thoughtcrime.securesms.util.DynamicTheme
+
+class ConversationSettingsActivity : DSLSettingsActivity(), ConversationSettingsFragment.Callback {
+
+ override val dynamicTheme: DynamicTheme = DynamicConversationSettingsTheme()
+
+ override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
+ ActivityCompat.postponeEnterTransition(this)
+ super.onCreate(savedInstanceState, ready)
+ }
+
+ override fun onContentWillRender() {
+ ActivityCompat.startPostponedEnterTransition(this)
+ }
+
+ override fun finish() {
+ super.finish()
+ overridePendingTransition(0, R.anim.fade_out)
+ }
+
+ companion object {
+
+ @JvmStatic
+ fun createTransitionBundle(context: Context, avatar: View, windowContent: View): Bundle? {
+ return if (context is Activity) {
+ ActivityOptionsCompat.makeSceneTransitionAnimation(
+ context,
+ Pair.create(avatar, "avatar"),
+ Pair.create(windowContent, "window_content")
+ ).toBundle()
+ } else {
+ null
+ }
+ }
+
+ @JvmStatic
+ fun createTransitionBundle(context: Context, avatar: View): Bundle? {
+ return if (context is Activity) {
+ ActivityOptionsCompat.makeSceneTransitionAnimation(
+ context,
+ avatar,
+ "avatar",
+ ).toBundle()
+ } else {
+ null
+ }
+ }
+
+ @JvmStatic
+ fun forGroup(context: Context, groupId: GroupId): Intent {
+ val startBundle = ConversationSettingsFragmentArgs.Builder(null, ParcelableGroupId.from(groupId))
+ .build()
+ .toBundle()
+
+ return getIntent(context)
+ .putExtra(ARG_START_BUNDLE, startBundle)
+ }
+
+ @JvmStatic
+ fun forRecipient(context: Context, recipientId: RecipientId): Intent {
+ val startBundle = ConversationSettingsFragmentArgs.Builder(recipientId, null)
+ .build()
+ .toBundle()
+
+ return getIntent(context)
+ .putExtra(ARG_START_BUNDLE, startBundle)
+ }
+
+ private fun getIntent(context: Context): Intent {
+ return Intent(context, ConversationSettingsActivity::class.java)
+ .putExtra(ARG_NAV_GRAPH, R.navigation.conversation_settings)
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsEvent.kt
new file mode 100644
index 000000000..749afc8eb
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsEvent.kt
@@ -0,0 +1,38 @@
+package org.thoughtcrime.securesms.components.settings.conversation
+
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.recipients.RecipientId
+
+sealed class ConversationSettingsEvent {
+ class AddToAGroup(
+ val recipientId: RecipientId,
+ val groupMembership: List
+ ) : ConversationSettingsEvent()
+
+ class AddMembersToGroup(
+ val groupId: GroupId,
+ val selectionWarning: Int,
+ val selectionLimit: Int,
+ val groupMembersWithoutSelf: List
+ ) : ConversationSettingsEvent()
+
+ object ShowGroupHardLimitDialog : ConversationSettingsEvent()
+
+ class ShowAddMembersToGroupError(
+ val failureReason: GroupChangeFailureReason
+ ) : ConversationSettingsEvent()
+
+ class ShowGroupInvitesSentDialog(
+ val invitesSentTo: List
+ ) : ConversationSettingsEvent()
+
+ class ShowMembersAdded(
+ val membersAddedCount: Int
+ ) : ConversationSettingsEvent()
+
+ class InitiateGroupMigration(
+ val recipientId: RecipientId
+ ) : ConversationSettingsEvent()
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt
new file mode 100644
index 000000000..411f893e8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt
@@ -0,0 +1,772 @@
+package org.thoughtcrime.securesms.components.settings.conversation
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.Rect
+import android.os.Bundle
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import android.widget.Toast
+import androidx.appcompat.widget.Toolbar
+import androidx.core.content.ContextCompat
+import androidx.core.view.doOnPreDraw
+import androidx.fragment.app.viewModels
+import androidx.navigation.Navigation
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import app.cash.exhaustive.Exhaustive
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
+import org.thoughtcrime.securesms.AvatarPreviewActivity
+import org.thoughtcrime.securesms.BlockUnblockDialog
+import org.thoughtcrime.securesms.InviteActivity
+import org.thoughtcrime.securesms.MediaPreviewActivity
+import org.thoughtcrime.securesms.MuteDialog
+import org.thoughtcrime.securesms.PushContactSelectionActivity
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.VerifyIdentityActivity
+import org.thoughtcrime.securesms.components.AvatarImageView
+import org.thoughtcrime.securesms.components.settings.DSLConfiguration
+import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
+import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
+import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
+import org.thoughtcrime.securesms.components.settings.DSLSettingsText
+import org.thoughtcrime.securesms.components.settings.NO_TINT
+import org.thoughtcrime.securesms.components.settings.configure
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.AvatarPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.BioTextPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.GroupDescriptionPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.InternalPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.LargeIconClickPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.SharedMediaPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
+import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
+import org.thoughtcrime.securesms.conversation.ConversationIntents
+import org.thoughtcrime.securesms.groups.ParcelableGroupId
+import org.thoughtcrime.securesms.groups.ui.GroupErrors
+import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog
+import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog
+import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity
+import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity
+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.GroupsLearnMoreBottomSheetDialogFragment
+import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment
+import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
+import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.recipients.RecipientExporter
+import org.thoughtcrime.securesms.recipients.RecipientId
+import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
+import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkDialogFragment
+import org.thoughtcrime.securesms.util.CommunicationActions
+import org.thoughtcrime.securesms.util.ContextUtil
+import org.thoughtcrime.securesms.util.ExpirationUtil
+import org.thoughtcrime.securesms.util.ThemeUtil
+import org.thoughtcrime.securesms.util.ViewUtil
+import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
+import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity
+
+private const val REQUEST_CODE_VIEW_CONTACT = 1
+private const val REQUEST_CODE_ADD_CONTACT = 2
+private const val REQUEST_CODE_ADD_MEMBERS_TO_GROUP = 3
+private const val REQUEST_CODE_RETURN_FROM_MEDIA = 4
+
+class ConversationSettingsFragment : DSLSettingsFragment(
+ layoutId = R.layout.conversation_settings_fragment,
+ menuId = R.menu.conversation_settings
+) {
+
+ private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
+ private val blockIcon by lazy {
+ ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24).apply {
+ colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
+ }
+ }
+
+ private val unblockIcon by lazy {
+ ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24)
+ }
+
+ private val leaveIcon by lazy {
+ ContextUtil.requireDrawable(requireContext(), R.drawable.ic_leave_tinted_24).apply {
+ colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
+ }
+ }
+
+ private val viewModel by viewModels(
+ factoryProducer = {
+ val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
+ val groupId = args.groupId as? ParcelableGroupId
+
+ ConversationSettingsViewModel.Factory(
+ recipientId = args.recipientId,
+ groupId = ParcelableGroupId.get(groupId),
+ repository = ConversationSettingsRepository(requireContext())
+ )
+ }
+ )
+
+ private lateinit var callback: Callback
+
+ private lateinit var toolbar: Toolbar
+ private lateinit var toolbarAvatar: AvatarImageView
+ private lateinit var toolbarTitle: TextView
+ private lateinit var toolbarBackground: View
+
+ private val navController get() = Navigation.findNavController(requireView())
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ callback = context as Callback
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ toolbar = view.findViewById(R.id.toolbar)
+ toolbarAvatar = view.findViewById(R.id.toolbar_avatar)
+ toolbarTitle = view.findViewById(R.id.toolbar_title)
+ toolbarBackground = view.findViewById(R.id.toolbar_background)
+
+ super.onViewCreated(view, savedInstanceState)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ when (requestCode) {
+ REQUEST_CODE_ADD_MEMBERS_TO_GROUP -> if (data != null) {
+ val selected: List = requireNotNull(data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS))
+ val progress: SimpleProgressDialog.DismissibleDialog = SimpleProgressDialog.showDelayed(requireContext())
+
+ viewModel.onAddToGroupComplete(selected) {
+ progress.dismiss()
+ }
+ }
+ REQUEST_CODE_RETURN_FROM_MEDIA -> viewModel.refreshSharedMedia()
+ REQUEST_CODE_ADD_CONTACT -> viewModel.refreshRecipient()
+ REQUEST_CODE_VIEW_CONTACT -> viewModel.refreshRecipient()
+ }
+ }
+
+ override fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
+ return ConversationSettingsOnUserScrolledAnimationHelper(toolbarAvatar, toolbarTitle, toolbarBackground, toolbarShadow)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return if (item.itemId == R.id.action_edit) {
+ val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
+ val groupId = args.groupId as ParcelableGroupId
+
+ startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), requireNotNull(ParcelableGroupId.get(groupId))))
+ true
+ } else {
+ super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun bindAdapter(adapter: DSLSettingsAdapter) {
+ BioTextPreference.register(adapter)
+ AvatarPreference.register(adapter)
+ ButtonStripPreference.register(adapter)
+ LargeIconClickPreference.register(adapter)
+ SharedMediaPreference.register(adapter)
+ RecipientPreference.register(adapter)
+ InternalPreference.register(adapter)
+ GroupDescriptionPreference.register(adapter)
+ LegacyGroupPreference.register(adapter)
+
+ viewModel.state.observe(viewLifecycleOwner) { state ->
+
+ if (state.recipient != Recipient.UNKNOWN) {
+ toolbarAvatar.buildOptions()
+ .withQuickContactEnabled(false)
+ .withUseSelfProfileAvatar(false)
+ .withFixedSize(ViewUtil.dpToPx(80))
+ .load(state.recipient)
+
+ state.withRecipientSettingsState {
+ toolbarTitle.text = state.recipient.getDisplayName(requireContext())
+ }
+
+ state.withGroupSettingsState {
+ toolbarTitle.text = it.groupTitle
+ toolbar.menu.findItem(R.id.action_edit).isVisible = it.canEditGroupAttributes
+ }
+ }
+
+ adapter.submitList(getConfiguration(state).toMappingModelList())
+
+ if (state.isLoaded) {
+ (requireView().parent as? ViewGroup)?.doOnPreDraw {
+ callback.onContentWillRender()
+ }
+ }
+ }
+
+ viewModel.events.observe(viewLifecycleOwner) { event ->
+ @Exhaustive
+ when (event) {
+ is ConversationSettingsEvent.AddToAGroup -> handleAddToAGroup(event)
+ is ConversationSettingsEvent.AddMembersToGroup -> handleAddMembersToGroup(event)
+ ConversationSettingsEvent.ShowGroupHardLimitDialog -> showGroupHardLimitDialog()
+ is ConversationSettingsEvent.ShowAddMembersToGroupError -> showAddMembersToGroupError(event)
+ is ConversationSettingsEvent.ShowGroupInvitesSentDialog -> showGroupInvitesSentDialog(event)
+ is ConversationSettingsEvent.ShowMembersAdded -> showMembersAdded(event)
+ is ConversationSettingsEvent.InitiateGroupMigration -> GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(parentFragmentManager, event.recipientId)
+ }
+ }
+ }
+
+ private fun getConfiguration(state: ConversationSettingsState): DSLConfiguration {
+ return configure {
+ if (state.recipient == Recipient.UNKNOWN) {
+ return@configure
+ }
+
+ customPref(
+ AvatarPreference.Model(
+ recipient = state.recipient,
+ onAvatarClick = { avatar ->
+ requireActivity().apply {
+ startActivity(
+ AvatarPreviewActivity.intentFromRecipientId(this, state.recipient.id),
+ AvatarPreviewActivity.createTransitionBundle(this, avatar)
+ )
+ }
+ }
+ )
+ )
+
+ state.withRecipientSettingsState {
+ customPref(BioTextPreference.RecipientModel(recipient = state.recipient))
+ }
+
+ state.withGroupSettingsState { groupState ->
+
+ val groupMembershipDescription = if (groupState.groupId.isV1) {
+ String.format("%s ยท %s", groupState.membershipCountDescription, getString(R.string.ManageGroupActivity_legacy_group))
+ } else if (!groupState.canEditGroupAttributes && groupState.groupDescription.isNullOrEmpty()) {
+ groupState.membershipCountDescription
+ } else {
+ null
+ }
+
+ customPref(
+ BioTextPreference.GroupModel(
+ groupTitle = groupState.groupTitle,
+ groupMembershipDescription = groupMembershipDescription
+ )
+ )
+
+ if (groupState.groupId.isV2) {
+ customPref(
+ GroupDescriptionPreference.Model(
+ groupId = groupState.groupId,
+ groupDescription = groupState.groupDescription,
+ descriptionShouldLinkify = groupState.groupDescriptionShouldLinkify,
+ canEditGroupAttributes = groupState.canEditGroupAttributes,
+ onEditGroupDescription = {
+ startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), groupState.groupId))
+ },
+ onViewGroupDescription = {
+ GroupDescriptionDialog.show(childFragmentManager, groupState.groupId, null, groupState.groupDescriptionShouldLinkify)
+ }
+ )
+ )
+ } else if (groupState.legacyGroupState != LegacyGroupPreference.State.NONE) {
+ customPref(
+ LegacyGroupPreference.Model(
+ state = groupState.legacyGroupState,
+ onLearnMoreClick = { GroupsLearnMoreBottomSheetDialogFragment.show(parentFragmentManager) },
+ onUpgradeClick = { viewModel.initiateGroupUpgrade() },
+ onMmsWarningClick = { startActivity(Intent(requireContext(), InviteActivity::class.java)) }
+ )
+ )
+ }
+ }
+
+ state.withRecipientSettingsState { recipientState ->
+ if (recipientState.displayInternalRecipientDetails) {
+ customPref(
+ InternalPreference.Model(
+ recipient = state.recipient,
+ onDisableProfileSharingClick = {
+ viewModel.disableProfileSharing()
+ }
+ )
+ )
+ }
+ }
+
+ customPref(
+ ButtonStripPreference.Model(
+ state = state.buttonStripState,
+ onVideoClick = {
+ CommunicationActions.startVideoCall(requireActivity(), state.recipient)
+ },
+ onAudioClick = {
+ CommunicationActions.startVoiceCall(requireActivity(), state.recipient)
+ },
+ onMuteClick = {
+ if (!state.buttonStripState.isMuted) {
+ MuteDialog.show(requireContext(), viewModel::setMuteUntil)
+ } else {
+ MaterialAlertDialogBuilder(requireContext())
+ .setMessage(state.recipient.muteUntil.formatMutedUntil(requireContext()))
+ .setPositiveButton(R.string.ConversationSettingsFragment__unmute) { dialog, _ ->
+ viewModel.unmute()
+ dialog.dismiss()
+ }
+ .setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
+ .show()
+ }
+ },
+ onSearchClick = {
+ val intent = ConversationIntents.createBuilder(requireContext(), state.recipient.id, state.threadId)
+ .withSearchOpen(true)
+ .build()
+
+ startActivity(intent)
+ requireActivity().finish()
+ }
+ )
+ )
+
+ dividerPref()
+
+ val summary = DSLSettingsText.from(formatDisappearingMessagesLifespan(state.disappearingMessagesLifespan))
+ val icon = if (state.disappearingMessagesLifespan <= 0) {
+ R.drawable.ic_update_timer_disabled_16
+ } else {
+ R.drawable.ic_update_timer_16
+ }
+
+ var enabled = true
+ state.withGroupSettingsState {
+ enabled = it.canEditGroupAttributes
+ }
+
+ clickPref(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__disappearing_messages),
+ summary = summary,
+ icon = DSLSettingsIcon.from(icon),
+ isEnabled = enabled,
+ onClick = {
+ val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToAppSettingsExpireTimer()
+ .setInitialValue(state.disappearingMessagesLifespan)
+ .setRecipientId(state.recipient.id)
+ .setForResultMode(false)
+
+ navController.navigate(action)
+ }
+ )
+
+ clickPref(
+ title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
+ icon = DSLSettingsIcon.from(R.drawable.ic_wallpaper_24),
+ onClick = {
+ startActivity(ChatWallpaperActivity.createIntent(requireContext(), state.recipient.id))
+ }
+ )
+
+ if (!state.recipient.isSelf) {
+ clickPref(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__sounds_and_notifications),
+ icon = DSLSettingsIcon.from(R.drawable.ic_speaker_24),
+ onClick = {
+ val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment(state.recipient.id)
+
+ navController.navigate(action)
+ }
+ )
+ }
+
+ state.withRecipientSettingsState { recipientState ->
+ when (recipientState.contactLinkState) {
+ ContactLinkState.OPEN -> {
+ clickPref(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__contact_details),
+ icon = DSLSettingsIcon.from(R.drawable.ic_profile_circle_24),
+ onClick = {
+ startActivityForResult(Intent(Intent.ACTION_VIEW, state.recipient.contactUri), REQUEST_CODE_VIEW_CONTACT)
+ }
+ )
+ }
+ ContactLinkState.ADD -> {
+ clickPref(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_as_a_contact),
+ icon = DSLSettingsIcon.from(R.drawable.ic_plus_24),
+ onClick = {
+ startActivityForResult(RecipientExporter.export(state.recipient).asAddContactIntent(), REQUEST_CODE_ADD_CONTACT)
+ }
+ )
+ }
+ ContactLinkState.NONE -> {
+ }
+ }
+
+ if (recipientState.identityRecord != null) {
+ clickPref(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__view_safety_number),
+ icon = DSLSettingsIcon.from(R.drawable.ic_safety_number_24),
+ onClick = {
+ startActivity(VerifyIdentityActivity.newIntent(requireActivity(), recipientState.identityRecord))
+ }
+ )
+ }
+ }
+
+ if (state.sharedMedia != null && state.sharedMedia.count > 0) {
+ dividerPref()
+
+ sectionHeaderPref(R.string.recipient_preference_activity__shared_media)
+
+ customPref(
+ SharedMediaPreference.Model(
+ mediaCursor = state.sharedMedia,
+ onMediaRecordClick = { mediaRecord, isLtr ->
+ startActivityForResult(
+ MediaPreviewActivity.intentFromMediaRecord(requireContext(), mediaRecord, isLtr),
+ REQUEST_CODE_RETURN_FROM_MEDIA
+ )
+ }
+ )
+ )
+
+ clickPref(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
+ onClick = {
+ startActivity(MediaOverviewActivity.forThread(requireContext(), state.threadId))
+ }
+ )
+ }
+
+ state.withRecipientSettingsState { groupState ->
+ if (groupState.selfHasGroups) {
+
+ dividerPref()
+
+ val groupsInCommonCount = groupState.allGroupsInCommon.size
+ sectionHeaderPref(
+ DSLSettingsText.from(
+ if (groupsInCommonCount == 0) {
+ getString(R.string.ManageRecipientActivity_no_groups_in_common)
+ } else {
+ resources.getQuantityString(
+ R.plurals.ManageRecipientActivity_d_groups_in_common,
+ groupsInCommonCount,
+ groupsInCommonCount
+ )
+ }
+ )
+ )
+
+ customPref(
+ LargeIconClickPreference.Model(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_to_a_group),
+ icon = DSLSettingsIcon.from(R.drawable.add_to_a_group, NO_TINT),
+ onClick = {
+ viewModel.onAddToGroup()
+ }
+ )
+ )
+
+ for (group in groupState.groupsInCommon) {
+ customPref(
+ RecipientPreference.Model(
+ recipient = group,
+ onClick = {
+ CommunicationActions.startConversation(requireActivity(), group, null)
+ requireActivity().finish()
+ }
+ )
+ )
+ }
+
+ if (groupState.canShowMoreGroupsInCommon) {
+ customPref(
+ LargeIconClickPreference.Model(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
+ icon = DSLSettingsIcon.from(R.drawable.show_more, NO_TINT),
+ onClick = {
+ viewModel.revealAllMembers()
+ }
+ )
+ )
+ }
+ }
+ }
+
+ state.withGroupSettingsState { groupState ->
+ val memberCount = groupState.allMembers.size
+
+ if (groupState.canAddToGroup || memberCount > 0) {
+ dividerPref()
+
+ sectionHeaderPref(DSLSettingsText.from(resources.getQuantityString(R.plurals.ContactSelectionListFragment_d_members, memberCount, memberCount)))
+ }
+
+ if (groupState.canAddToGroup) {
+ customPref(
+ LargeIconClickPreference.Model(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_members),
+ icon = DSLSettingsIcon.from(R.drawable.add_to_a_group, NO_TINT),
+ onClick = {
+ viewModel.onAddToGroup()
+ }
+ )
+ )
+ }
+
+ for (member in groupState.members) {
+ customPref(
+ RecipientPreference.Model(
+ recipient = member.member,
+ isAdmin = member.isAdmin,
+ onClick = {
+ RecipientBottomSheetDialogFragment.create(member.member.id, groupState.groupId).show(parentFragmentManager, "BOTTOM")
+ }
+ )
+ )
+ }
+
+ if (groupState.canShowMoreGroupMembers) {
+ customPref(
+ LargeIconClickPreference.Model(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
+ icon = DSLSettingsIcon.from(R.drawable.show_more, NO_TINT),
+ onClick = {
+ viewModel.revealAllMembers()
+ }
+ )
+ )
+ }
+
+ if (state.recipient.isPushV2Group) {
+ dividerPref()
+
+ clickPref(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__group_link),
+ summary = DSLSettingsText.from(if (groupState.groupLinkEnabled) R.string.preferences_on else R.string.preferences_off),
+ icon = DSLSettingsIcon.from(R.drawable.ic_link_16),
+ onClick = {
+ ShareableGroupLinkDialogFragment.create(groupState.groupId.requireV2()).show(parentFragmentManager, "DIALOG")
+ }
+ )
+
+ clickPref(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__requests_and_invites),
+ icon = DSLSettingsIcon.from(R.drawable.ic_update_group_add_16),
+ onClick = {
+ startActivity(ManagePendingAndRequestingMembersActivity.newIntent(requireContext(), groupState.groupId.requireV2()))
+ }
+ )
+
+ if (groupState.isSelfAdmin) {
+ clickPref(
+ title = DSLSettingsText.from(R.string.ConversationSettingsFragment__permissions),
+ icon = DSLSettingsIcon.from(R.drawable.ic_lock_24),
+ onClick = {
+ val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToPermissionsSettingsFragment(ParcelableGroupId.from(groupState.groupId))
+ navController.navigate(action)
+ }
+ )
+ }
+ }
+
+ if (groupState.canLeave) {
+ dividerPref()
+
+ clickPref(
+ title = DSLSettingsText.from(R.string.conversation__menu_leave_group, alertTint),
+ icon = DSLSettingsIcon.from(leaveIcon),
+ onClick = {
+ LeaveGroupDialog.handleLeavePushGroup(requireActivity(), groupState.groupId.requirePush(), null)
+ }
+ )
+ }
+ }
+
+ if (state.canModifyBlockedState) {
+ state.withRecipientSettingsState {
+ dividerPref()
+ }
+
+ state.withGroupSettingsState {
+ if (!it.canLeave) {
+ dividerPref()
+ }
+ }
+
+ val isBlocked = state.recipient.isBlocked
+ val isGroup = state.recipient.isPushGroup
+
+ val title = when {
+ isBlocked && isGroup -> R.string.ConversationSettingsFragment__unblock_group
+ isBlocked -> R.string.ConversationSettingsFragment__unblock
+ isGroup -> R.string.ConversationSettingsFragment__block_group
+ else -> R.string.ConversationSettingsFragment__block
+ }
+
+ val titleTint = if (isBlocked) null else alertTint
+ val blockUnblockIcon = if (isBlocked) unblockIcon else blockIcon
+
+ clickPref(
+ title = DSLSettingsText.from(title, titleTint),
+ icon = DSLSettingsIcon.from(blockUnblockIcon),
+ onClick = {
+ if (state.recipient.isBlocked) {
+ BlockUnblockDialog.showUnblockFor(requireContext(), viewLifecycleOwner.lifecycle, state.recipient) {
+ viewModel.unblock()
+ }
+ } else {
+ BlockUnblockDialog.showBlockFor(requireContext(), viewLifecycleOwner.lifecycle, state.recipient) {
+ viewModel.block()
+ }
+ }
+ }
+ )
+ }
+ }
+ }
+
+ private fun formatDisappearingMessagesLifespan(disappearingMessagesLifespan: Int): String {
+ return if (disappearingMessagesLifespan <= 0) {
+ getString(R.string.preferences_off)
+ } else {
+ ExpirationUtil.getExpirationDisplayValue(requireContext(), disappearingMessagesLifespan)
+ }
+ }
+
+ private fun handleAddToAGroup(addToAGroup: ConversationSettingsEvent.AddToAGroup) {
+ startActivity(AddToGroupsActivity.newIntent(requireContext(), addToAGroup.recipientId, addToAGroup.groupMembership))
+ }
+
+ private fun handleAddMembersToGroup(addMembersToGroup: ConversationSettingsEvent.AddMembersToGroup) {
+ startActivityForResult(
+ AddMembersActivity.createIntent(
+ requireContext(),
+ addMembersToGroup.groupId,
+ ContactsCursorLoader.DisplayMode.FLAG_PUSH,
+ addMembersToGroup.selectionWarning,
+ addMembersToGroup.selectionLimit,
+ addMembersToGroup.groupMembersWithoutSelf
+ ),
+ REQUEST_CODE_ADD_MEMBERS_TO_GROUP
+ )
+ }
+
+ private fun showGroupHardLimitDialog() {
+ GroupLimitDialog.showHardLimitMessage(requireContext())
+ }
+
+ private fun showAddMembersToGroupError(showAddMembersToGroupError: ConversationSettingsEvent.ShowAddMembersToGroupError) {
+ Toast.makeText(requireContext(), GroupErrors.getUserDisplayMessage(showAddMembersToGroupError.failureReason), Toast.LENGTH_LONG).show()
+ }
+
+ private fun showGroupInvitesSentDialog(showGroupInvitesSentDialog: ConversationSettingsEvent.ShowGroupInvitesSentDialog) {
+ GroupInviteSentDialog.showInvitesSent(requireContext(), showGroupInvitesSentDialog.invitesSentTo)
+ }
+
+ private fun showMembersAdded(showMembersAdded: ConversationSettingsEvent.ShowMembersAdded) {
+ val string = resources.getQuantityString(
+ R.plurals.ManageGroupActivity_added,
+ showMembersAdded.membersAddedCount,
+ showMembersAdded.membersAddedCount
+ )
+
+ Snackbar.make(requireView(), string, Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show()
+ }
+
+ private class ConversationSettingsOnUserScrolledAnimationHelper(
+ private val toolbarAvatar: View,
+ private val toolbarTitle: View,
+ private val toolbarBackground: View,
+ toolbarShadow: View
+ ) : ToolbarShadowAnimationHelper(toolbarShadow) {
+
+ override val duration: Long = 200L
+
+ private val actionBarSize = ThemeUtil.getThemedDimen(toolbarShadow.context, R.attr.actionBarSize)
+ private val rect = Rect()
+
+ override fun getAnimationState(recyclerView: RecyclerView): AnimationState {
+ val layoutManager = recyclerView.layoutManager as LinearLayoutManager
+
+ // If first visible item position is 0
+ // If less than actionbarsize is visible
+ // SHOW
+ // else
+ // HIDE
+ // else
+ // HIDE
+
+ return if (layoutManager.findFirstVisibleItemPosition() == 0) {
+ val firstChild = requireNotNull(layoutManager.getChildAt(0))
+ firstChild.getDrawingRect(rect)
+
+ if (rect.height() <= actionBarSize) {
+ AnimationState.SHOW
+ } else {
+ AnimationState.HIDE
+ }
+ } else {
+ AnimationState.SHOW
+ }
+ }
+
+ override fun show() {
+ super.show()
+
+ toolbarAvatar
+ .animate()
+ .setDuration(duration)
+ .translationY(0f)
+ .alpha(1f)
+
+ toolbarTitle
+ .animate()
+ .setDuration(duration)
+ .translationY(0f)
+ .alpha(1f)
+
+ toolbarBackground
+ .animate()
+ .setDuration(duration)
+ .alpha(1f)
+ }
+
+ override fun hide() {
+ super.hide()
+
+ toolbarAvatar
+ .animate()
+ .setDuration(duration)
+ .translationY(ViewUtil.dpToPx(56).toFloat())
+ .alpha(0f)
+
+ toolbarTitle
+ .animate()
+ .setDuration(duration)
+ .translationY(ViewUtil.dpToPx(56).toFloat())
+ .alpha(0f)
+
+ toolbarBackground
+ .animate()
+ .setDuration(duration)
+ .alpha(0f)
+ }
+ }
+
+ interface Callback {
+ fun onContentWillRender()
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt
new file mode 100644
index 000000000..ffd6213bc
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt
@@ -0,0 +1,203 @@
+package org.thoughtcrime.securesms.components.settings.conversation
+
+import android.content.Context
+import android.database.Cursor
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.LiveData
+import org.signal.core.util.concurrent.SignalExecutors
+import org.signal.core.util.logging.Log
+import org.signal.storageservice.protos.groups.local.DecryptedGroup
+import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
+import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper
+import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.database.GroupDatabase
+import org.thoughtcrime.securesms.database.IdentityDatabase
+import org.thoughtcrime.securesms.database.MediaDatabase
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.GroupManager
+import org.thoughtcrime.securesms.groups.GroupProtoUtil
+import org.thoughtcrime.securesms.groups.LiveGroup
+import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
+import org.thoughtcrime.securesms.keyvalue.SignalStore
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.recipients.RecipientId
+import org.thoughtcrime.securesms.recipients.RecipientUtil
+import org.thoughtcrime.securesms.util.FeatureFlags
+import java.io.IOException
+
+private val TAG = Log.tag(ConversationSettingsRepository::class.java)
+
+class ConversationSettingsRepository(
+ private val context: Context
+) {
+
+ @WorkerThread
+ fun getThreadMedia(threadId: Long): Cursor {
+ return DatabaseFactory.getMediaDatabase(context).getGalleryMediaForThread(threadId, MediaDatabase.Sorting.Newest)
+ }
+
+ fun getThreadId(recipientId: RecipientId, consumer: (Long) -> Unit) {
+ SignalExecutors.BOUNDED.execute {
+ consumer(DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipientId))
+ }
+ }
+
+ fun getThreadId(groupId: GroupId, consumer: (Long) -> Unit) {
+ SignalExecutors.BOUNDED.execute {
+ val recipientId = Recipient.externalGroupExact(context, groupId).id
+ consumer(DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipientId))
+ }
+ }
+
+ fun isInternalRecipientDetailsEnabled(): Boolean = SignalStore.internalValues().recipientDetails()
+
+ fun hasGroups(consumer: (Boolean) -> Unit) {
+ SignalExecutors.BOUNDED.execute { consumer(DatabaseFactory.getGroupDatabase(context).activeGroupCount > 0) }
+ }
+
+ fun getIdentity(recipientId: RecipientId, consumer: (IdentityDatabase.IdentityRecord?) -> Unit) {
+ SignalExecutors.BOUNDED.execute {
+ consumer(
+ DatabaseFactory.getIdentityDatabase(context)
+ .getIdentity(recipientId)
+ .orNull()
+ )
+ }
+ }
+
+ fun getGroupsInCommon(recipientId: RecipientId, consumer: (List) -> Unit) {
+ SignalExecutors.BOUNDED.execute {
+ consumer(
+ DatabaseFactory
+ .getGroupDatabase(context)
+ .getPushGroupsContainingMember(recipientId)
+ .asSequence()
+ .filter { it.members.contains(Recipient.self().id) }
+ .map(GroupDatabase.GroupRecord::getRecipientId)
+ .map(Recipient::resolved)
+ .sortedBy { gr -> gr.getDisplayName(context) }
+ .toList()
+ )
+ }
+ }
+
+ fun getGroupMembership(recipientId: RecipientId, consumer: (List) -> Unit) {
+ SignalExecutors.BOUNDED.execute {
+ val groupDatabase = DatabaseFactory.getGroupDatabase(context)
+ val groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId)
+ val groupRecipients = ArrayList(groupRecords.size)
+ for (groupRecord in groupRecords) {
+ groupRecipients.add(groupRecord.recipientId)
+ }
+ consumer(groupRecipients)
+ }
+ }
+
+ fun refreshRecipient(recipientId: RecipientId) {
+ SignalExecutors.UNBOUNDED.execute {
+ try {
+ DirectoryHelper.refreshDirectoryFor(context, Recipient.resolved(recipientId), false)
+ } catch (e: IOException) {
+ Log.w(TAG, "Failed to refresh user after adding to contacts.")
+ }
+ }
+ }
+
+ fun setMuteUntil(recipientId: RecipientId, until: Long) {
+ SignalExecutors.BOUNDED.execute {
+ DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until)
+ }
+ }
+
+ fun getGroupCapacity(groupId: GroupId, consumer: (GroupCapacityResult) -> Unit) {
+ SignalExecutors.BOUNDED.execute {
+ val groupRecord: GroupDatabase.GroupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get()
+ consumer(
+ if (groupRecord.isV2Group) {
+ val decryptedGroup: DecryptedGroup = groupRecord.requireV2GroupProperties().decryptedGroup
+ val pendingMembers: List = decryptedGroup.pendingMembersList
+ .map(DecryptedPendingMember::getUuid)
+ .map(GroupProtoUtil::uuidByteStringToRecipientId)
+
+ val members = mutableListOf()
+
+ members.addAll(groupRecord.members)
+ members.addAll(pendingMembers)
+
+ GroupCapacityResult(Recipient.self().id, members, FeatureFlags.groupLimits())
+ } else {
+ GroupCapacityResult(Recipient.self().id, groupRecord.members, FeatureFlags.groupLimits())
+ }
+ )
+ }
+ }
+
+ fun addMembers(groupId: GroupId, selected: List, consumer: (GroupAddMembersResult) -> Unit) {
+ SignalExecutors.BOUNDED.execute {
+ consumer(
+ try {
+ val groupActionResult = GroupManager.addMembers(context, groupId.requirePush(), selected)
+ GroupAddMembersResult.Success(groupActionResult.addedMemberCount, Recipient.resolvedList(groupActionResult.invitedMembers))
+ } catch (e: Exception) {
+ GroupAddMembersResult.Failure(GroupChangeFailureReason.fromException(e))
+ }
+ )
+ }
+ }
+
+ fun setMuteUntil(groupId: GroupId, until: Long) {
+ SignalExecutors.BOUNDED.execute {
+ val recipientId = Recipient.externalGroupExact(context, groupId).id
+ DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until)
+ }
+ }
+
+ fun block(recipientId: RecipientId) {
+ SignalExecutors.BOUNDED.execute {
+ val recipient = Recipient.resolved(recipientId)
+ RecipientUtil.blockNonGroup(context, recipient)
+ }
+ }
+
+ fun unblock(recipientId: RecipientId) {
+ SignalExecutors.BOUNDED.execute {
+ val recipient = Recipient.resolved(recipientId)
+ RecipientUtil.unblock(context, recipient)
+ }
+ }
+
+ fun block(groupId: GroupId) {
+ SignalExecutors.BOUNDED.execute {
+ val recipient = Recipient.externalGroupExact(context, groupId)
+ RecipientUtil.block(context, recipient)
+ }
+ }
+
+ fun unblock(groupId: GroupId) {
+ SignalExecutors.BOUNDED.execute {
+ val recipient = Recipient.externalGroupExact(context, groupId)
+ RecipientUtil.unblock(context, recipient)
+ }
+ }
+
+ fun disableProfileSharing(recipientId: RecipientId) {
+ SignalExecutors.BOUNDED.execute {
+ DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipientId, false)
+ }
+ }
+
+ @WorkerThread
+ fun isMessageRequestAccepted(recipient: Recipient): Boolean {
+ return RecipientUtil.isMessageRequestAccepted(context, recipient)
+ }
+
+ fun getMembershipCountDescription(liveGroup: LiveGroup): LiveData {
+ return liveGroup.getMembershipCountDescription(context.resources)
+ }
+
+ fun getExternalPossiblyMigratedGroupRecipientId(groupId: GroupId, consumer: (RecipientId) -> Unit) {
+ SignalExecutors.BOUNDED.execute {
+ consumer(Recipient.externalPossiblyMigratedGroup(context, groupId).id)
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsState.kt
new file mode 100644
index 000000000..69e1e2ccb
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsState.kt
@@ -0,0 +1,92 @@
+package org.thoughtcrime.securesms.components.settings.conversation
+
+import android.database.Cursor
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
+import org.thoughtcrime.securesms.database.IdentityDatabase
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
+import org.thoughtcrime.securesms.recipients.Recipient
+
+data class ConversationSettingsState(
+ val threadId: Long = -1,
+ val recipient: Recipient = Recipient.UNKNOWN,
+ val buttonStripState: ButtonStripPreference.State = ButtonStripPreference.State(),
+ val disappearingMessagesLifespan: Int = 0,
+ val canModifyBlockedState: Boolean = false,
+ val sharedMedia: Cursor? = null,
+ private val specificSettingsState: SpecificSettingsState,
+) {
+
+ val isLoaded: Boolean = recipient != Recipient.UNKNOWN && sharedMedia != null && specificSettingsState.isLoaded
+
+ fun withRecipientSettingsState(consumer: (SpecificSettingsState.RecipientSettingsState) -> Unit) {
+ if (specificSettingsState is SpecificSettingsState.RecipientSettingsState) {
+ consumer(specificSettingsState)
+ }
+ }
+
+ fun withGroupSettingsState(consumer: (SpecificSettingsState.GroupSettingsState) -> Unit) {
+ if (specificSettingsState is SpecificSettingsState.GroupSettingsState) {
+ consumer(specificSettingsState)
+ }
+ }
+
+ fun requireRecipientSettingsState(): SpecificSettingsState.RecipientSettingsState = specificSettingsState.requireRecipientSettingsState()
+ fun requireGroupSettingsState(): SpecificSettingsState.GroupSettingsState = specificSettingsState.requireGroupSettingsState()
+}
+
+sealed class SpecificSettingsState {
+
+ abstract val isLoaded: Boolean
+
+ data class RecipientSettingsState(
+ val identityRecord: IdentityDatabase.IdentityRecord? = null,
+ val allGroupsInCommon: List = listOf(),
+ val groupsInCommon: List = listOf(),
+ val selfHasGroups: Boolean = false,
+ val canShowMoreGroupsInCommon: Boolean = false,
+ val groupsInCommonExpanded: Boolean = false,
+ val contactLinkState: ContactLinkState = ContactLinkState.NONE,
+ val displayInternalRecipientDetails: Boolean
+ ) : SpecificSettingsState() {
+
+ override val isLoaded: Boolean = true
+
+ override fun requireRecipientSettingsState() = this
+ }
+
+ data class GroupSettingsState(
+ val groupId: GroupId,
+ val allMembers: List = listOf(),
+ val members: List = listOf(),
+ val isSelfAdmin: Boolean = false,
+ val canAddToGroup: Boolean = false,
+ val canEditGroupAttributes: Boolean = false,
+ val canLeave: Boolean = false,
+ val canShowMoreGroupMembers: Boolean = false,
+ val groupMembersExpanded: Boolean = false,
+ val groupTitle: String = "",
+ private val groupTitleLoaded: Boolean = false,
+ val groupDescription: String? = null,
+ val groupDescriptionShouldLinkify: Boolean = false,
+ private val groupDescriptionLoaded: Boolean = false,
+ val groupLinkEnabled: Boolean = false,
+ val membershipCountDescription: String = "",
+ val legacyGroupState: LegacyGroupPreference.State = LegacyGroupPreference.State.NONE
+ ) : SpecificSettingsState() {
+
+ override val isLoaded: Boolean = groupTitleLoaded && groupDescriptionLoaded
+
+ override fun requireGroupSettingsState(): GroupSettingsState = this
+ }
+
+ open fun requireRecipientSettingsState(): RecipientSettingsState = error("Not a recipient settings state")
+ open fun requireGroupSettingsState(): GroupSettingsState = error("Not a group settings state")
+}
+
+enum class ContactLinkState {
+ OPEN,
+ ADD,
+ NONE
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt
new file mode 100644
index 000000000..cfb2f5675
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsViewModel.kt
@@ -0,0 +1,454 @@
+package org.thoughtcrime.securesms.components.settings.conversation
+
+import android.database.Cursor
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.signal.core.util.ThreadUtil
+import org.signal.core.util.concurrent.SignalExecutors
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
+import org.thoughtcrime.securesms.database.RecipientDatabase
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.LiveGroup
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.recipients.RecipientId
+import org.thoughtcrime.securesms.recipients.RecipientUtil
+import org.thoughtcrime.securesms.util.FeatureFlags
+import org.thoughtcrime.securesms.util.SingleLiveEvent
+import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
+import org.thoughtcrime.securesms.util.livedata.Store
+
+sealed class ConversationSettingsViewModel(
+ private val repository: ConversationSettingsRepository,
+ specificSettingsState: SpecificSettingsState,
+) : ViewModel() {
+
+ private val openedMediaCursors = HashSet()
+
+ @Volatile
+ private var cleared = false
+
+ protected val store = Store(
+ ConversationSettingsState(
+ specificSettingsState = specificSettingsState
+ )
+ )
+ protected val internalEvents = SingleLiveEvent()
+
+ private val sharedMediaUpdateTrigger = MutableLiveData(Unit)
+
+ val state: LiveData = store.stateLiveData
+ val events: LiveData = internalEvents
+
+ init {
+ val threadId: LiveData = Transformations.distinctUntilChanged(Transformations.map(state) { it.threadId })
+ val updater: LiveData = LiveDataUtil.combineLatest(threadId, sharedMediaUpdateTrigger) { tId, _ -> tId }
+
+ val sharedMedia: LiveData = LiveDataUtil.mapAsync(SignalExecutors.BOUNDED, updater) { tId ->
+ repository.getThreadMedia(tId)
+ }
+
+ store.update(sharedMedia) { cursor, state ->
+ if (!cleared) {
+ openedMediaCursors.add(cursor)
+ state.copy(sharedMedia = cursor)
+ } else {
+ cursor.ensureClosed()
+ state.copy(sharedMedia = null)
+ }
+ }
+ }
+
+ fun refreshSharedMedia() {
+ sharedMediaUpdateTrigger.postValue(Unit)
+ }
+
+ open fun refreshRecipient(): Unit = error("This ViewModel does not support this interaction")
+
+ abstract fun setMuteUntil(muteUntil: Long)
+
+ abstract fun unmute()
+
+ abstract fun block()
+
+ abstract fun unblock()
+
+ abstract fun onAddToGroup()
+
+ abstract fun onAddToGroupComplete(selected: List, onComplete: () -> Unit)
+
+ abstract fun revealAllMembers()
+
+ override fun onCleared() {
+ cleared = true
+ store.update { state ->
+ openedMediaCursors.forEach { it.ensureClosed() }
+ state.copy(sharedMedia = null)
+ }
+ }
+
+ private fun Cursor?.ensureClosed() {
+ if (this != null && !this.isClosed) {
+ this.close()
+ }
+ }
+
+ open fun disableProfileSharing(): Unit = error("This ViewModel does not support this interaction")
+
+ open fun initiateGroupUpgrade(): Unit = error("This ViewModel does not support this interaction")
+
+ private class RecipientSettingsViewModel(
+ private val recipientId: RecipientId,
+ private val repository: ConversationSettingsRepository
+ ) : ConversationSettingsViewModel(
+ repository,
+ SpecificSettingsState.RecipientSettingsState(
+ displayInternalRecipientDetails = repository.isInternalRecipientDetailsEnabled()
+ )
+ ) {
+
+ private val liveRecipient = Recipient.live(recipientId)
+
+ init {
+ store.update(liveRecipient.liveData) { recipient, state ->
+ state.copy(
+ recipient = recipient,
+ buttonStripState = ButtonStripPreference.State(
+ isVideoAvailable = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED && !recipient.isSelf,
+ isAudioAvailable = !recipient.isGroup && !recipient.isSelf,
+ isAudioSecure = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED,
+ isMuted = recipient.isMuted,
+ isMuteAvailable = true,
+ isSearchAvailable = true
+ ),
+ disappearingMessagesLifespan = recipient.expireMessages,
+ canModifyBlockedState = !recipient.isSelf,
+ specificSettingsState = state.requireRecipientSettingsState().copy(
+ contactLinkState = when {
+ recipient.isSelf -> ContactLinkState.NONE
+ recipient.isSystemContact -> ContactLinkState.OPEN
+ else -> ContactLinkState.ADD
+ }
+ )
+ )
+ }
+
+ repository.getThreadId(recipientId) { threadId ->
+ store.update { state ->
+ state.copy(threadId = threadId)
+ }
+ }
+
+ if (recipientId != Recipient.self().id) {
+ repository.getGroupsInCommon(recipientId) { groupsInCommon ->
+ store.update { state ->
+ val recipientSettings = state.requireRecipientSettingsState()
+ val expanded = recipientSettings.groupsInCommonExpanded
+ state.copy(
+ specificSettingsState = recipientSettings.copy(
+ allGroupsInCommon = groupsInCommon,
+ groupsInCommon = if (expanded) groupsInCommon else groupsInCommon.take(5),
+ canShowMoreGroupsInCommon = !expanded && groupsInCommon.size > 5
+ )
+ )
+ }
+ }
+
+ repository.hasGroups { hasGroups ->
+ store.update { state ->
+ val recipientSettings = state.requireRecipientSettingsState()
+ state.copy(
+ specificSettingsState = recipientSettings.copy(
+ selfHasGroups = hasGroups
+ )
+ )
+ }
+ }
+
+ repository.getIdentity(recipientId) { identityRecord ->
+ store.update { state ->
+ state.copy(specificSettingsState = state.requireRecipientSettingsState().copy(identityRecord = identityRecord))
+ }
+ }
+ }
+ }
+
+ override fun onAddToGroup() {
+ repository.getGroupMembership(recipientId) {
+ internalEvents.postValue(ConversationSettingsEvent.AddToAGroup(recipientId, it))
+ }
+ }
+
+ override fun onAddToGroupComplete(selected: List, onComplete: () -> Unit) {
+ }
+
+ override fun revealAllMembers() {
+ store.update { state ->
+ state.copy(
+ specificSettingsState = state.requireRecipientSettingsState().copy(
+ groupsInCommon = state.requireRecipientSettingsState().allGroupsInCommon,
+ groupsInCommonExpanded = true,
+ canShowMoreGroupsInCommon = false
+ )
+ )
+ }
+ }
+
+ override fun refreshRecipient() {
+ repository.refreshRecipient(recipientId)
+ }
+
+ override fun setMuteUntil(muteUntil: Long) {
+ repository.setMuteUntil(recipientId, muteUntil)
+ }
+
+ override fun unmute() {
+ repository.setMuteUntil(recipientId, 0)
+ }
+
+ override fun block() {
+ repository.block(recipientId)
+ }
+
+ override fun unblock() {
+ repository.unblock(recipientId)
+ }
+
+ override fun disableProfileSharing() {
+ repository.disableProfileSharing(recipientId)
+ }
+ }
+
+ private class GroupSettingsViewModel(
+ private val groupId: GroupId,
+ private val repository: ConversationSettingsRepository
+ ) : ConversationSettingsViewModel(repository, SpecificSettingsState.GroupSettingsState(groupId)) {
+
+ private val liveGroup = LiveGroup(groupId)
+
+ init {
+ store.update(liveGroup.groupRecipient) { recipient, state ->
+ state.copy(
+ recipient = recipient,
+ buttonStripState = ButtonStripPreference.State(
+ isVideoAvailable = recipient.isPushV2Group,
+ isAudioAvailable = false,
+ isAudioSecure = recipient.isPushV2Group,
+ isMuted = recipient.isMuted,
+ isMuteAvailable = true,
+ isSearchAvailable = true
+ ),
+ canModifyBlockedState = RecipientUtil.isBlockable(recipient),
+ specificSettingsState = state.requireGroupSettingsState().copy(
+ legacyGroupState = getLegacyGroupState(recipient)
+ )
+ )
+ }
+
+ repository.getThreadId(groupId) { threadId ->
+ store.update { state ->
+ state.copy(threadId = threadId)
+ }
+ }
+
+ store.update(liveGroup.selfCanEditGroupAttributes()) { selfCanEditGroupAttributes, state ->
+ state.copy(
+ specificSettingsState = state.requireGroupSettingsState().copy(
+ canEditGroupAttributes = selfCanEditGroupAttributes
+ )
+ )
+ }
+
+ store.update(liveGroup.isSelfAdmin) { isSelfAdmin, state ->
+ state.copy(
+ specificSettingsState = state.requireGroupSettingsState().copy(
+ isSelfAdmin = isSelfAdmin
+ )
+ )
+ }
+
+ store.update(liveGroup.expireMessages) { expireMessages, state ->
+ state.copy(
+ disappearingMessagesLifespan = expireMessages
+ )
+ }
+
+ store.update(liveGroup.selfCanAddMembers()) { canAddMembers, state ->
+ state.copy(
+ specificSettingsState = state.requireGroupSettingsState().copy(
+ canAddToGroup = canAddMembers
+ )
+ )
+ }
+
+ store.update(liveGroup.fullMembers) { fullMembers, state ->
+ val groupState = state.requireGroupSettingsState()
+
+ state.copy(
+ specificSettingsState = groupState.copy(
+ allMembers = fullMembers,
+ members = if (groupState.groupMembersExpanded) fullMembers else fullMembers.take(5),
+ canShowMoreGroupMembers = !groupState.groupMembersExpanded && fullMembers.size > 5
+ )
+ )
+ }
+
+ val isMessageRequestAccepted: LiveData = LiveDataUtil.mapAsync(liveGroup.groupRecipient) { r -> repository.isMessageRequestAccepted(r) }
+ val descriptionState: LiveData = LiveDataUtil.combineLatest(liveGroup.description, isMessageRequestAccepted, ::DescriptionState)
+
+ store.update(descriptionState) { d, state ->
+ state.copy(
+ specificSettingsState = state.requireGroupSettingsState().copy(
+ groupDescription = d.description,
+ groupDescriptionShouldLinkify = d.canLinkify,
+ groupDescriptionLoaded = true
+ )
+ )
+ }
+
+ store.update(liveGroup.isActive) { isActive, state ->
+ state.copy(
+ specificSettingsState = state.requireGroupSettingsState().copy(
+ canLeave = isActive && groupId.isPush
+ )
+ )
+ }
+
+ store.update(liveGroup.title) { title, state ->
+ state.copy(
+ specificSettingsState = state.requireGroupSettingsState().copy(
+ groupTitle = title,
+ groupTitleLoaded = true
+ )
+ )
+ }
+
+ store.update(liveGroup.groupLink) { groupLink, state ->
+ state.copy(
+ specificSettingsState = state.requireGroupSettingsState().copy(
+ groupLinkEnabled = groupLink.isEnabled
+ )
+ )
+ }
+
+ store.update(repository.getMembershipCountDescription(liveGroup)) { description, state ->
+ state.copy(
+ specificSettingsState = state.requireGroupSettingsState().copy(
+ membershipCountDescription = description
+ )
+ )
+ }
+ }
+
+ private fun getLegacyGroupState(recipient: Recipient): LegacyGroupPreference.State {
+ val showLegacyInfo = recipient.requireGroupId().isV1
+
+ return if (showLegacyInfo && recipient.participants.size > FeatureFlags.groupLimits().hardLimit) {
+ LegacyGroupPreference.State.TOO_LARGE
+ } else if (showLegacyInfo) {
+ LegacyGroupPreference.State.UPGRADE
+ } else if (groupId.isMms) {
+ LegacyGroupPreference.State.MMS_WARNING
+ } else {
+ LegacyGroupPreference.State.NONE
+ }
+ }
+
+ override fun onAddToGroup() {
+ repository.getGroupCapacity(groupId) { capacityResult ->
+ if (capacityResult.getRemainingCapacity() > 0) {
+ internalEvents.postValue(
+ ConversationSettingsEvent.AddMembersToGroup(
+ groupId,
+ capacityResult.getSelectionWarning(),
+ capacityResult.getSelectionLimit(),
+ capacityResult.getMembersWithoutSelf()
+ )
+ )
+ } else {
+ internalEvents.postValue(ConversationSettingsEvent.ShowGroupHardLimitDialog)
+ }
+ }
+ }
+
+ override fun onAddToGroupComplete(selected: List, onComplete: () -> Unit) {
+ repository.addMembers(groupId, selected) {
+ ThreadUtil.runOnMain { onComplete() }
+
+ when (it) {
+ is GroupAddMembersResult.Success -> {
+ if (it.newMembersInvited.isNotEmpty()) {
+ internalEvents.postValue(ConversationSettingsEvent.ShowGroupInvitesSentDialog(it.newMembersInvited))
+ }
+
+ if (it.numberOfMembersAdded > 0) {
+ internalEvents.postValue(ConversationSettingsEvent.ShowMembersAdded(it.numberOfMembersAdded))
+ }
+ }
+ is GroupAddMembersResult.Failure -> internalEvents.postValue(ConversationSettingsEvent.ShowAddMembersToGroupError(it.reason))
+ }
+ }
+ }
+
+ override fun revealAllMembers() {
+ store.update { state ->
+ state.copy(
+ specificSettingsState = state.requireGroupSettingsState().copy(
+ members = state.requireGroupSettingsState().allMembers,
+ groupMembersExpanded = true,
+ canShowMoreGroupMembers = false
+ )
+ )
+ }
+ }
+
+ override fun setMuteUntil(muteUntil: Long) {
+ repository.setMuteUntil(groupId, muteUntil)
+ }
+
+ override fun unmute() {
+ repository.setMuteUntil(groupId, 0)
+ }
+
+ override fun block() {
+ repository.block(groupId)
+ }
+
+ override fun unblock() {
+ repository.unblock(groupId)
+ }
+
+ override fun initiateGroupUpgrade() {
+ repository.getExternalPossiblyMigratedGroupRecipientId(groupId) {
+ internalEvents.postValue(ConversationSettingsEvent.InitiateGroupMigration(it))
+ }
+ }
+ }
+
+ class Factory(
+ private val recipientId: RecipientId? = null,
+ private val groupId: GroupId? = null,
+ private val repository: ConversationSettingsRepository,
+ ) : ViewModelProvider.Factory {
+
+ override fun create(modelClass: Class): T {
+ return requireNotNull(
+ modelClass.cast(
+ when {
+ recipientId != null -> RecipientSettingsViewModel(recipientId, repository)
+ groupId != null -> GroupSettingsViewModel(groupId, repository)
+ else -> error("One of RecipientId or GroupId required.")
+ }
+ )
+ )
+ }
+ }
+
+ private class DescriptionState(
+ val description: String?,
+ val canLinkify: Boolean
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/GroupAddMembersResult.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/GroupAddMembersResult.kt
new file mode 100644
index 000000000..44661b0fe
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/GroupAddMembersResult.kt
@@ -0,0 +1,15 @@
+package org.thoughtcrime.securesms.components.settings.conversation
+
+import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
+import org.thoughtcrime.securesms.recipients.Recipient
+
+sealed class GroupAddMembersResult {
+ class Success(
+ val numberOfMembersAdded: Int,
+ val newMembersInvited: List
+ ) : GroupAddMembersResult()
+
+ class Failure(
+ val reason: GroupChangeFailureReason
+ ) : GroupAddMembersResult()
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/GroupCapacityResult.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/GroupCapacityResult.kt
new file mode 100644
index 000000000..03a4fb048
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/GroupCapacityResult.kt
@@ -0,0 +1,46 @@
+package org.thoughtcrime.securesms.components.settings.conversation
+
+import org.thoughtcrime.securesms.ContactSelectionListFragment
+import org.thoughtcrime.securesms.groups.SelectionLimits
+import org.thoughtcrime.securesms.recipients.RecipientId
+
+class GroupCapacityResult(
+ private val selfId: RecipientId,
+ private val members: List,
+ private val selectionLimits: SelectionLimits
+) {
+ fun getMembers(): List {
+ return members
+ }
+
+ fun getSelectionLimit(): Int {
+ if (!selectionLimits.hasHardLimit()) {
+ return ContactSelectionListFragment.NO_LIMIT
+ }
+ val containsSelf = members.indexOf(selfId) != -1
+ return selectionLimits.hardLimit - if (containsSelf) 1 else 0
+ }
+
+ fun getSelectionWarning(): Int {
+ if (!selectionLimits.hasRecommendedLimit()) {
+ return ContactSelectionListFragment.NO_LIMIT
+ }
+
+ val containsSelf = members.indexOf(selfId) != -1
+ return selectionLimits.recommendedLimit - if (containsSelf) 1 else 0
+ }
+
+ fun getRemainingCapacity(): Int {
+ return selectionLimits.hardLimit - members.size
+ }
+
+ fun getMembersWithoutSelf(): List {
+ val recipientIds = ArrayList(members.size)
+ for (recipientId in members) {
+ if (recipientId != selfId) {
+ recipientIds.add(recipientId)
+ }
+ }
+ return recipientIds
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsEvents.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsEvents.kt
new file mode 100644
index 000000000..e721632ae
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsEvents.kt
@@ -0,0 +1,7 @@
+package org.thoughtcrime.securesms.components.settings.conversation.permissions
+
+import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
+
+sealed class PermissionsSettingsEvents {
+ class GroupChangeError(val reason: GroupChangeFailureReason) : PermissionsSettingsEvents()
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsFragment.kt
new file mode 100644
index 000000000..ae36098d3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsFragment.kt
@@ -0,0 +1,86 @@
+package org.thoughtcrime.securesms.components.settings.conversation.permissions
+
+import android.widget.Toast
+import androidx.annotation.StringRes
+import androidx.fragment.app.viewModels
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.settings.DSLConfiguration
+import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
+import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
+import org.thoughtcrime.securesms.components.settings.DSLSettingsText
+import org.thoughtcrime.securesms.components.settings.configure
+import org.thoughtcrime.securesms.groups.ParcelableGroupId
+import org.thoughtcrime.securesms.groups.ui.GroupErrors
+
+class PermissionsSettingsFragment : DSLSettingsFragment(
+ titleId = R.string.ConversationSettingsFragment__permissions
+) {
+
+ private val permissionsOptions: Array by lazy {
+ resources.getStringArray(R.array.PermissionsSettingsFragment__editor_labels)
+ }
+
+ private val viewModel: PermissionsSettingsViewModel by viewModels(
+ factoryProducer = {
+ val args = PermissionsSettingsFragmentArgs.fromBundle(requireArguments())
+ val groupId = requireNotNull(ParcelableGroupId.get(args.groupId as ParcelableGroupId))
+ val repository = PermissionsSettingsRepository(requireContext())
+
+ PermissionsSettingsViewModel.Factory(groupId, repository)
+ }
+ )
+
+ override fun bindAdapter(adapter: DSLSettingsAdapter) {
+ viewModel.state.observe(viewLifecycleOwner) { state ->
+ adapter.submitList(getConfiguration(state).toMappingModelList())
+ }
+
+ viewModel.events.observe(viewLifecycleOwner) { event ->
+ when (event) {
+ is PermissionsSettingsEvents.GroupChangeError -> handleGroupChangeError(event)
+ }
+ }
+ }
+
+ private fun handleGroupChangeError(groupChangeError: PermissionsSettingsEvents.GroupChangeError) {
+ Toast.makeText(context, GroupErrors.getUserDisplayMessage(groupChangeError.reason), Toast.LENGTH_LONG).show()
+ }
+
+ private fun getConfiguration(state: PermissionsSettingsState): DSLConfiguration {
+ return configure {
+
+ radioListPref(
+ title = DSLSettingsText.from(R.string.PermissionsSettingsFragment__add_members),
+ isEnabled = state.selfCanEditSettings,
+ listItems = permissionsOptions,
+ dialogTitle = DSLSettingsText.from(R.string.PermissionsSettingsFragment__who_can_add_new_members),
+ selected = getSelected(state.nonAdminCanAddMembers),
+ confirmAction = true,
+ onSelected = {
+ viewModel.setNonAdminCanAddMembers(it == 1)
+ }
+ )
+
+ radioListPref(
+ title = DSLSettingsText.from(R.string.PermissionsSettingsFragment__edit_group_info),
+ isEnabled = state.selfCanEditSettings,
+ listItems = permissionsOptions,
+ dialogTitle = DSLSettingsText.from(R.string.PermissionsSettingsFragment__who_can_edit_this_groups_info),
+ selected = getSelected(state.nonAdminCanEditGroupInfo),
+ confirmAction = true,
+ onSelected = {
+ viewModel.setNonAdminCanEditGroupInfo(it == 1)
+ }
+ )
+ }
+ }
+
+ @StringRes
+ private fun getSelected(isNonAdminAllowed: Boolean): Int {
+ return if (isNonAdminAllowed) {
+ 1
+ } else {
+ 0
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsRepository.kt
new file mode 100644
index 000000000..fad256e5d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsRepository.kt
@@ -0,0 +1,45 @@
+package org.thoughtcrime.securesms.components.settings.conversation.permissions
+
+import android.content.Context
+import org.signal.core.util.concurrent.SignalExecutors
+import org.signal.core.util.logging.Log
+import org.thoughtcrime.securesms.groups.GroupAccessControl
+import org.thoughtcrime.securesms.groups.GroupChangeException
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.GroupManager
+import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback
+import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
+import java.io.IOException
+
+private val TAG = Log.tag(PermissionsSettingsRepository::class.java)
+
+class PermissionsSettingsRepository(private val context: Context) {
+
+ fun applyMembershipRightsChange(groupId: GroupId, newRights: GroupAccessControl, error: GroupChangeErrorCallback) {
+ SignalExecutors.UNBOUNDED.execute {
+ try {
+ GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights)
+ } catch (e: GroupChangeException) {
+ Log.w(TAG, e)
+ error.onError(GroupChangeFailureReason.fromException(e))
+ } catch (e: IOException) {
+ Log.w(TAG, e)
+ error.onError(GroupChangeFailureReason.fromException(e))
+ }
+ }
+ }
+
+ fun applyAttributesRightsChange(groupId: GroupId, newRights: GroupAccessControl, error: GroupChangeErrorCallback) {
+ SignalExecutors.UNBOUNDED.execute {
+ try {
+ GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights)
+ } catch (e: GroupChangeException) {
+ Log.w(TAG, e)
+ error.onError(GroupChangeFailureReason.fromException(e))
+ } catch (e: IOException) {
+ Log.w(TAG, e)
+ error.onError(GroupChangeFailureReason.fromException(e))
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsState.kt
new file mode 100644
index 000000000..822cc44b6
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsState.kt
@@ -0,0 +1,7 @@
+package org.thoughtcrime.securesms.components.settings.conversation.permissions
+
+data class PermissionsSettingsState(
+ val selfCanEditSettings: Boolean = false,
+ val nonAdminCanAddMembers: Boolean = false,
+ val nonAdminCanEditGroupInfo: Boolean = false
+)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsViewModel.kt
new file mode 100644
index 000000000..025483a91
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/permissions/PermissionsSettingsViewModel.kt
@@ -0,0 +1,66 @@
+package org.thoughtcrime.securesms.components.settings.conversation.permissions
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.thoughtcrime.securesms.groups.GroupAccessControl
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.LiveGroup
+import org.thoughtcrime.securesms.util.SingleLiveEvent
+import org.thoughtcrime.securesms.util.livedata.Store
+
+class PermissionsSettingsViewModel(
+ private val groupId: GroupId,
+ private val repository: PermissionsSettingsRepository
+) : ViewModel() {
+
+ private val store = Store(PermissionsSettingsState())
+ private val liveGroup = LiveGroup(groupId)
+ private val internalEvents = SingleLiveEvent()
+
+ val state: LiveData = store.stateLiveData
+ val events: LiveData = internalEvents
+
+ init {
+ store.update(liveGroup.isSelfAdmin) { isSelfAdmin, state ->
+ state.copy(selfCanEditSettings = isSelfAdmin)
+ }
+
+ store.update(liveGroup.membershipAdditionAccessControl) { membershipAdditionAccessControl, state ->
+ state.copy(nonAdminCanAddMembers = membershipAdditionAccessControl == GroupAccessControl.ALL_MEMBERS)
+ }
+
+ store.update(liveGroup.attributesAccessControl) { attributesAccessControl, state ->
+ state.copy(nonAdminCanEditGroupInfo = attributesAccessControl == GroupAccessControl.ALL_MEMBERS)
+ }
+ }
+
+ fun setNonAdminCanAddMembers(nonAdminCanAddMembers: Boolean) {
+ repository.applyMembershipRightsChange(groupId, nonAdminCanAddMembers.asGroupAccessControl()) { reason ->
+ internalEvents.postValue(PermissionsSettingsEvents.GroupChangeError(reason))
+ }
+ }
+
+ fun setNonAdminCanEditGroupInfo(nonAdminCanEditGroupInfo: Boolean) {
+ repository.applyAttributesRightsChange(groupId, nonAdminCanEditGroupInfo.asGroupAccessControl()) { reason ->
+ internalEvents.postValue(PermissionsSettingsEvents.GroupChangeError(reason))
+ }
+ }
+
+ private fun Boolean.asGroupAccessControl(): GroupAccessControl {
+ return if (this) {
+ GroupAccessControl.ALL_MEMBERS
+ } else {
+ GroupAccessControl.ONLY_ADMINS
+ }
+ }
+
+ class Factory(
+ private val groupId: GroupId,
+ private val repository: PermissionsSettingsRepository
+ ) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return requireNotNull(modelClass.cast(PermissionsSettingsViewModel(groupId, repository)))
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/AvatarPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/AvatarPreference.kt
new file mode 100644
index 000000000..3d599501b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/AvatarPreference.kt
@@ -0,0 +1,45 @@
+package org.thoughtcrime.securesms.components.settings.conversation.preferences
+
+import android.view.View
+import androidx.core.view.ViewCompat
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.AvatarImageView
+import org.thoughtcrime.securesms.components.settings.PreferenceModel
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingViewHolder
+
+/**
+ * Renders a large avatar (80dp) for a given Recipient.
+ */
+object AvatarPreference {
+
+ fun register(adapter: MappingAdapter) {
+ adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_avatar_preference_item))
+ }
+
+ class Model(
+ val recipient: Recipient,
+ val onAvatarClick: (View) -> Unit
+ ) : PreferenceModel() {
+ override fun areItemsTheSame(newItem: Model): Boolean {
+ return recipient == newItem.recipient
+ }
+
+ override fun areContentsTheSame(newItem: Model): Boolean {
+ return super.areContentsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
+ }
+ }
+
+ private class ViewHolder(itemView: View) : MappingViewHolder(itemView) {
+ private val avatar: AvatarImageView = itemView.findViewById(R.id.bio_preference_avatar).apply {
+ ViewCompat.setTransitionName(this, "avatar")
+ }
+
+ override fun bind(model: Model) {
+ avatar.setAvatar(model.recipient)
+ avatar.disableQuickContact()
+ avatar.setOnClickListener { model.onAvatarClick(avatar) }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt
new file mode 100644
index 000000000..61d9d2827
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt
@@ -0,0 +1,91 @@
+package org.thoughtcrime.securesms.components.settings.conversation.preferences
+
+import android.content.Context
+import android.view.View
+import android.widget.TextView
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.settings.PreferenceModel
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingViewHolder
+
+/**
+ * Renders name, description, about, etc. for a given group or recipient.
+ */
+object BioTextPreference {
+
+ fun register(adapter: MappingAdapter) {
+ adapter.registerFactory(RecipientModel::class.java, MappingAdapter.LayoutFactory(::RecipientViewHolder, R.layout.conversation_settings_bio_preference_item))
+ adapter.registerFactory(GroupModel::class.java, MappingAdapter.LayoutFactory(::GroupViewHolder, R.layout.conversation_settings_bio_preference_item))
+ }
+
+ abstract class BioTextPreferenceModel> : PreferenceModel() {
+ abstract fun getHeadlineText(context: Context): String
+ abstract fun getSubhead1Text(): String?
+ abstract fun getSubhead2Text(): String?
+ }
+
+ class RecipientModel(
+ private val recipient: Recipient,
+ ) : BioTextPreferenceModel() {
+
+ override fun getHeadlineText(context: Context): String = recipient.getDisplayNameOrUsername(context)
+
+ override fun getSubhead1Text(): String? = recipient.combinedAboutAndEmoji
+
+ override fun getSubhead2Text(): String? = recipient.e164.orNull()
+
+ override fun areContentsTheSame(newItem: RecipientModel): Boolean {
+ return super.areContentsTheSame(newItem) && newItem.recipient.hasSameContent(recipient)
+ }
+
+ override fun areItemsTheSame(newItem: RecipientModel): Boolean {
+ return newItem.recipient.id == recipient.id
+ }
+ }
+
+ class GroupModel(
+ val groupTitle: String,
+ val groupMembershipDescription: String?
+ ) : BioTextPreferenceModel() {
+ override fun getHeadlineText(context: Context): String = groupTitle
+
+ override fun getSubhead1Text(): String? = groupMembershipDescription
+
+ override fun getSubhead2Text(): String? = null
+
+ override fun areContentsTheSame(newItem: GroupModel): Boolean {
+ return super.areContentsTheSame(newItem) &&
+ groupTitle == newItem.groupTitle &&
+ groupMembershipDescription == newItem.groupMembershipDescription
+ }
+
+ override fun areItemsTheSame(newItem: GroupModel): Boolean {
+ return true
+ }
+ }
+
+ private abstract class BioTextViewHolder>(itemView: View) : MappingViewHolder(itemView) {
+
+ private val headline: TextView = itemView.findViewById(R.id.bio_preference_headline)
+ private val subhead1: TextView = itemView.findViewById(R.id.bio_preference_subhead_1)
+ private val subhead2: TextView = itemView.findViewById(R.id.bio_preference_subhead_2)
+
+ override fun bind(model: T) {
+ headline.text = model.getHeadlineText(context)
+
+ model.getSubhead1Text().let {
+ subhead1.text = it
+ subhead1.visibility = if (it == null) View.GONE else View.VISIBLE
+ }
+
+ model.getSubhead2Text().let {
+ subhead2.text = it
+ subhead2.visibility = if (it == null) View.GONE else View.VISIBLE
+ }
+ }
+ }
+
+ private class RecipientViewHolder(itemView: View) : BioTextViewHolder(itemView)
+ private class GroupViewHolder(itemView: View) : BioTextViewHolder(itemView)
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/ButtonStripPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/ButtonStripPreference.kt
new file mode 100644
index 000000000..7dc6b1413
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/ButtonStripPreference.kt
@@ -0,0 +1,105 @@
+package org.thoughtcrime.securesms.components.settings.conversation.preferences
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.appcompat.content.res.AppCompatResources
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
+import org.thoughtcrime.securesms.components.settings.PreferenceModel
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingViewHolder
+import org.thoughtcrime.securesms.util.visible
+
+/**
+ * Renders a configurable strip of buttons
+ */
+object ButtonStripPreference {
+
+ fun register(adapter: MappingAdapter) {
+ adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_button_strip))
+ }
+
+ class Model(
+ val state: State,
+ val background: DSLSettingsIcon? = null,
+ val onMessageClick: () -> Unit = {},
+ val onVideoClick: () -> Unit = {},
+ val onAudioClick: () -> Unit = {},
+ val onMuteClick: () -> Unit = {},
+ val onSearchClick: () -> Unit = {}
+ ) : PreferenceModel() {
+ override fun areContentsTheSame(newItem: Model): Boolean {
+ return super.areContentsTheSame(newItem) && state == newItem.state
+ }
+
+ override fun areItemsTheSame(newItem: Model): Boolean {
+ return true
+ }
+ }
+
+ class ViewHolder(itemView: View) : MappingViewHolder(itemView) {
+
+ private val message: View = itemView.findViewById(R.id.message)
+ private val messageLabel: View = itemView.findViewById(R.id.message_label)
+ private val videoCall: View = itemView.findViewById(R.id.start_video)
+ private val videoLabel: View = itemView.findViewById(R.id.start_video_label)
+ private val audioCall: ImageView = itemView.findViewById(R.id.start_audio)
+ private val audioLabel: TextView = itemView.findViewById(R.id.start_audio_label)
+ private val mute: ImageView = itemView.findViewById(R.id.mute)
+ private val muteLabel: TextView = itemView.findViewById(R.id.mute_label)
+ private val search: View = itemView.findViewById(R.id.search)
+ private val searchLabel: View = itemView.findViewById(R.id.search_label)
+
+ override fun bind(model: Model) {
+ message.visible = model.state.isMessageAvailable
+ messageLabel.visible = model.state.isMessageAvailable
+ videoCall.visible = model.state.isVideoAvailable
+ videoLabel.visible = model.state.isVideoAvailable
+ audioCall.visible = model.state.isAudioAvailable
+ audioLabel.visible = model.state.isAudioAvailable
+ mute.visible = model.state.isMuteAvailable
+ muteLabel.visible = model.state.isMuteAvailable
+ search.visible = model.state.isSearchAvailable
+ searchLabel.visible = model.state.isSearchAvailable
+
+ if (model.state.isAudioSecure) {
+ audioLabel.setText(R.string.ConversationSettingsFragment__audio)
+ audioCall.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_phone_right_24))
+ } else {
+ audioLabel.setText(R.string.ConversationSettingsFragment__call)
+ audioCall.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_phone_right_unlock_primary_accent_24))
+ }
+
+ if (model.state.isMuted) {
+ mute.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_bell_disabled_24))
+ muteLabel.setText(R.string.ConversationSettingsFragment__muted)
+ } else {
+ mute.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.ic_bell_24))
+ muteLabel.setText(R.string.ConversationSettingsFragment__mute)
+ }
+
+ if (model.background != null) {
+ listOf(message, videoCall, audioCall, mute, search).forEach {
+ it.background = model.background.resolve(context)
+ }
+ }
+
+ message.setOnClickListener { model.onMessageClick() }
+ videoCall.setOnClickListener { model.onVideoClick() }
+ audioCall.setOnClickListener { model.onAudioClick() }
+ mute.setOnClickListener { model.onMuteClick() }
+ search.setOnClickListener { model.onSearchClick() }
+ }
+ }
+
+ data class State(
+ val isMessageAvailable: Boolean = false,
+ val isVideoAvailable: Boolean = false,
+ val isAudioAvailable: Boolean = false,
+ val isMuteAvailable: Boolean = false,
+ val isSearchAvailable: Boolean = false,
+ val isAudioSecure: Boolean = false,
+ val isMuted: Boolean = false,
+ )
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/GroupDescriptionPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/GroupDescriptionPreference.kt
new file mode 100644
index 000000000..f34f9068f
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/GroupDescriptionPreference.kt
@@ -0,0 +1,65 @@
+package org.thoughtcrime.securesms.components.settings.conversation.preferences
+
+import android.view.View
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.emoji.EmojiTextView
+import org.thoughtcrime.securesms.components.settings.PreferenceModel
+import org.thoughtcrime.securesms.groups.GroupId
+import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil
+import org.thoughtcrime.securesms.util.LongClickMovementMethod
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingViewHolder
+
+object GroupDescriptionPreference {
+
+ fun register(adapter: MappingAdapter) {
+ adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_group_description_preference))
+ }
+
+ class Model(
+ private val groupId: GroupId,
+ val groupDescription: String?,
+ val descriptionShouldLinkify: Boolean,
+ val canEditGroupAttributes: Boolean,
+ val onEditGroupDescription: () -> Unit,
+ val onViewGroupDescription: () -> Unit
+ ) : PreferenceModel() {
+ override fun areItemsTheSame(newItem: Model): Boolean {
+ return groupId == newItem.groupId
+ }
+
+ override fun areContentsTheSame(newItem: Model): Boolean {
+ return super.areContentsTheSame(newItem) &&
+ groupDescription == newItem.groupDescription &&
+ descriptionShouldLinkify == newItem.descriptionShouldLinkify &&
+ canEditGroupAttributes == newItem.canEditGroupAttributes
+ }
+ }
+
+ class ViewHolder(itemView: View) : MappingViewHolder(itemView) {
+
+ private val groupDescriptionTextView: EmojiTextView = findViewById(R.id.manage_group_description)
+
+ override fun bind(model: Model) {
+ groupDescriptionTextView.movementMethod = LongClickMovementMethod.getInstance(context)
+
+ if (model.groupDescription.isNullOrEmpty()) {
+ if (model.canEditGroupAttributes) {
+ groupDescriptionTextView.setOverflowText(null)
+ groupDescriptionTextView.setText(R.string.ManageGroupActivity_add_group_description)
+ groupDescriptionTextView.setOnClickListener { model.onEditGroupDescription() }
+ }
+ } else {
+ groupDescriptionTextView.setOnClickListener(null)
+ GroupDescriptionUtil.setText(
+ context,
+ groupDescriptionTextView,
+ model.groupDescription,
+ model.descriptionShouldLinkify
+ ) {
+ model.onViewGroupDescription()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/InternalPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/InternalPreference.kt
new file mode 100644
index 000000000..1c139dc30
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/InternalPreference.kt
@@ -0,0 +1,67 @@
+package org.thoughtcrime.securesms.components.settings.conversation.preferences
+
+import android.view.View
+import android.widget.TextView
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.settings.PreferenceModel
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.util.Base64
+import org.thoughtcrime.securesms.util.Hex
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingViewHolder
+import java.util.UUID
+
+object InternalPreference {
+
+ fun register(adapter: MappingAdapter) {
+ adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_internal_preference))
+ }
+
+ class Model(
+ private val recipient: Recipient,
+ val onDisableProfileSharingClick: () -> Unit
+ ) : PreferenceModel() {
+
+ val body: String get() {
+ return String.format(
+ """
+ -- Profile Name --
+ [${recipient.profileName.givenName}] [${recipient.profileName.familyName}]
+
+ -- Profile Sharing --
+ ${recipient.isProfileSharing}
+
+ -- Profile Key (Base64) --
+ ${recipient.profileKey?.let(Base64::encodeBytes) ?: "None"}
+
+ -- Profile Key (Hex) --
+ ${recipient.profileKey?.let(Hex::toStringCondensed) ?: "None"}
+
+ -- Sealed Sender Mode --
+ ${recipient.unidentifiedAccessMode}
+
+ -- UUID --
+ ${recipient.uuid.transform { obj: UUID -> obj.toString() }.or("None")}
+
+ -- RecipientId --
+ ${recipient.id.serialize()}
+ """.trimIndent(),
+ )
+ }
+
+ override fun areItemsTheSame(newItem: Model): Boolean {
+ return recipient == newItem.recipient
+ }
+ }
+
+ private class ViewHolder(itemView: View) : MappingViewHolder(itemView) {
+
+ private val body: TextView = itemView.findViewById(R.id.internal_preference_body)
+ private val disableProfileSharing: View = itemView.findViewById(R.id.internal_disable_profile_sharing)
+
+ override fun bind(model: Model) {
+ body.text = model.body
+ disableProfileSharing.setOnClickListener { model.onDisableProfileSharingClick() }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/LargeIconClickPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/LargeIconClickPreference.kt
new file mode 100644
index 000000000..4befc03c1
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/LargeIconClickPreference.kt
@@ -0,0 +1,32 @@
+package org.thoughtcrime.securesms.components.settings.conversation.preferences
+
+import android.view.View
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
+import org.thoughtcrime.securesms.components.settings.DSLSettingsText
+import org.thoughtcrime.securesms.components.settings.PreferenceModel
+import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
+import org.thoughtcrime.securesms.util.MappingAdapter
+
+/**
+ * Renders a preference line item with a larger (40dp) icon
+ */
+object LargeIconClickPreference {
+
+ fun register(adapter: MappingAdapter) {
+ adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.large_icon_preference_item))
+ }
+
+ class Model(
+ override val title: DSLSettingsText?,
+ override val icon: DSLSettingsIcon,
+ val onClick: () -> Unit
+ ) : PreferenceModel()
+
+ private class ViewHolder(itemView: View) : PreferenceViewHolder(itemView) {
+ override fun bind(model: Model) {
+ super.bind(model)
+ itemView.setOnClickListener { model.onClick() }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/LegacyGroupPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/LegacyGroupPreference.kt
new file mode 100644
index 000000000..b353147dd
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/LegacyGroupPreference.kt
@@ -0,0 +1,67 @@
+package org.thoughtcrime.securesms.components.settings.conversation.preferences
+
+import android.view.View
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.settings.PreferenceModel
+import org.thoughtcrime.securesms.util.FeatureFlags
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingViewHolder
+import org.thoughtcrime.securesms.util.views.LearnMoreTextView
+
+object LegacyGroupPreference {
+
+ fun register(adapter: MappingAdapter) {
+ adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_legacy_group_preference))
+ }
+
+ class Model(
+ val state: State,
+ val onLearnMoreClick: () -> Unit,
+ val onUpgradeClick: () -> Unit,
+ val onMmsWarningClick: () -> Unit
+ ) : PreferenceModel() {
+ override fun areItemsTheSame(newItem: Model): Boolean {
+ return state == newItem.state
+ }
+ }
+
+ private class ViewHolder(itemView: View) : MappingViewHolder(itemView) {
+
+ private val groupInfoText: LearnMoreTextView = findViewById(R.id.manage_group_info_text)
+
+ override fun bind(model: Model) {
+ itemView.visibility = View.VISIBLE
+
+ when (model.state) {
+ State.LEARN_MORE -> {
+ groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_learn_more)
+ groupInfoText.setOnLinkClickListener { model.onLearnMoreClick() }
+ groupInfoText.setLearnMoreVisible(true)
+ }
+ State.UPGRADE -> {
+ groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_upgrade)
+ groupInfoText.setOnLinkClickListener { model.onUpgradeClick() }
+ groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_upgrade_this_group)
+ }
+ State.TOO_LARGE -> {
+ groupInfoText.text = context.getString(R.string.ManageGroupActivity_legacy_group_too_large, FeatureFlags.groupLimits().hardLimit - 1)
+ groupInfoText.setLearnMoreVisible(false)
+ }
+ State.MMS_WARNING -> {
+ groupInfoText.setText(R.string.ManageGroupActivity_this_is_an_insecure_mms_group)
+ groupInfoText.setOnLinkClickListener { model.onMmsWarningClick() }
+ groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_invite_now)
+ }
+ State.NONE -> itemView.visibility = View.GONE
+ }
+ }
+ }
+
+ enum class State {
+ LEARN_MORE,
+ UPGRADE,
+ TOO_LARGE,
+ MMS_WARNING,
+ NONE
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/RecipientPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/RecipientPreference.kt
new file mode 100644
index 000000000..b67bbd426
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/RecipientPreference.kt
@@ -0,0 +1,63 @@
+package org.thoughtcrime.securesms.components.settings.conversation.preferences
+
+import android.view.View
+import android.widget.TextView
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.AvatarImageView
+import org.thoughtcrime.securesms.components.settings.PreferenceModel
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingViewHolder
+import org.thoughtcrime.securesms.util.visible
+
+/**
+ * Renders a Recipient as a row item with an icon, avatar, status, and admin state
+ */
+object RecipientPreference {
+
+ fun register(adapter: MappingAdapter) {
+ adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.group_recipient_list_item))
+ }
+
+ class Model(
+ val recipient: Recipient,
+ val isAdmin: Boolean = false,
+ val onClick: () -> Unit
+ ) : PreferenceModel() {
+ override fun areItemsTheSame(newItem: Model): Boolean {
+ return recipient.id == newItem.recipient.id
+ }
+
+ override fun areContentsTheSame(newItem: Model): Boolean {
+ return super.areContentsTheSame(newItem) && recipient.hasSameContent(newItem.recipient)
+ }
+ }
+
+ private class ViewHolder(itemView: View) : MappingViewHolder(itemView) {
+ private val avatar: AvatarImageView = itemView.findViewById(R.id.recipient_avatar)
+ private val name: TextView = itemView.findViewById(R.id.recipient_name)
+ private val about: TextView = itemView.findViewById(R.id.recipient_about)
+ private val admin: View = itemView.findViewById(R.id.admin)
+
+ override fun bind(model: Model) {
+ itemView.setOnClickListener { model.onClick() }
+
+ avatar.setRecipient(model.recipient)
+ name.text = if (model.recipient.isSelf) {
+ context.getString(R.string.Recipient_you)
+ } else {
+ model.recipient.getDisplayName(context)
+ }
+
+ val aboutText = model.recipient.combinedAboutAndEmoji
+ if (aboutText.isNullOrEmpty()) {
+ about.visibility = View.GONE
+ } else {
+ about.text = model.recipient.combinedAboutAndEmoji
+ about.visibility = View.VISIBLE
+ }
+
+ admin.visible = model.isAdmin
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/SharedMediaPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/SharedMediaPreference.kt
new file mode 100644
index 000000000..7b7c3fd69
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/SharedMediaPreference.kt
@@ -0,0 +1,43 @@
+package org.thoughtcrime.securesms.components.settings.conversation.preferences
+
+import android.database.Cursor
+import android.view.View
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.ThreadPhotoRailView
+import org.thoughtcrime.securesms.components.settings.PreferenceModel
+import org.thoughtcrime.securesms.database.MediaDatabase
+import org.thoughtcrime.securesms.mms.GlideApp
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingViewHolder
+import org.thoughtcrime.securesms.util.ViewUtil
+
+/**
+ * Renders the shared media photo rail.
+ */
+object SharedMediaPreference {
+
+ fun register(adapter: MappingAdapter) {
+ adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory(::ViewHolder, R.layout.conversation_settings_shared_media))
+ }
+
+ class Model(
+ val mediaCursor: Cursor,
+ val onMediaRecordClick: (MediaDatabase.MediaRecord, Boolean) -> Unit
+ ) : PreferenceModel() {
+ override fun areItemsTheSame(newItem: Model): Boolean {
+ return newItem.mediaCursor == mediaCursor
+ }
+ }
+
+ private class ViewHolder(itemView: View) : MappingViewHolder(itemView) {
+
+ private val rail: ThreadPhotoRailView = itemView.findViewById(R.id.rail_view)
+
+ override fun bind(model: Model) {
+ rail.setCursor(GlideApp.with(rail), model.mediaCursor)
+ rail.setListener {
+ model.onMediaRecordClick(it, ViewUtil.isLtr(rail))
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/Utils.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/Utils.kt
new file mode 100644
index 000000000..527f16e10
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/Utils.kt
@@ -0,0 +1,20 @@
+package org.thoughtcrime.securesms.components.settings.conversation.preferences
+
+import android.content.Context
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.util.DateUtils
+import java.util.Locale
+
+object Utils {
+
+ fun Long.formatMutedUntil(context: Context): String {
+ return if (this == Long.MAX_VALUE) {
+ context.getString(R.string.ConversationSettingsFragment__conversation_muted_forever)
+ } else {
+ context.getString(
+ R.string.ConversationSettingsFragment__conversation_muted_until_s,
+ DateUtils.getTimeString(context, Locale.getDefault(), this)
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment.kt
new file mode 100644
index 000000000..5d1f02028
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment.kt
@@ -0,0 +1,118 @@
+package org.thoughtcrime.securesms.components.settings.conversation.sounds
+
+import androidx.fragment.app.viewModels
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.thoughtcrime.securesms.MuteDialog
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.settings.DSLConfiguration
+import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
+import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
+import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
+import org.thoughtcrime.securesms.components.settings.DSLSettingsText
+import org.thoughtcrime.securesms.components.settings.configure
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
+import org.thoughtcrime.securesms.database.RecipientDatabase
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment
+
+class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment(
+ titleId = R.string.ConversationSettingsFragment__sounds_and_notifications
+) {
+
+ private val mentionLabels: Array by lazy {
+ resources.getStringArray(R.array.SoundsAndNotificationsSettingsFragment__mention_labels)
+ }
+
+ private val viewModel: SoundsAndNotificationsSettingsViewModel by viewModels(
+ factoryProducer = {
+ val recipientId = SoundsAndNotificationsSettingsFragmentArgs.fromBundle(requireArguments()).recipientId
+ val repository = SoundsAndNotificationsSettingsRepository(requireContext())
+
+ SoundsAndNotificationsSettingsViewModel.Factory(recipientId, repository)
+ }
+ )
+
+ override fun bindAdapter(adapter: DSLSettingsAdapter) {
+ viewModel.state.observe(viewLifecycleOwner) { state ->
+ if (state.recipientId != Recipient.UNKNOWN.id) {
+ adapter.submitList(getConfiguration(state).toMappingModelList())
+ }
+ }
+ }
+
+ private fun getConfiguration(state: SoundsAndNotificationsSettingsState): DSLConfiguration {
+ return configure {
+
+ val muteSummary = if (state.muteUntil > 0) {
+ state.muteUntil.formatMutedUntil(requireContext())
+ } else {
+ getString(R.string.SoundsAndNotificationsSettingsFragment__not_muted)
+ }
+
+ val muteIcon = if (state.muteUntil > 0) {
+ R.drawable.ic_bell_disabled_24
+ } else {
+ R.drawable.ic_bell_24
+ }
+
+ clickPref(
+ title = DSLSettingsText.from(R.string.SoundsAndNotificationsSettingsFragment__mute_notifications),
+ icon = DSLSettingsIcon.from(muteIcon),
+ summary = DSLSettingsText.from(muteSummary),
+ onClick = {
+ if (state.muteUntil <= 0) {
+ MuteDialog.show(requireContext(), viewModel::setMuteUntil)
+ } else {
+ MaterialAlertDialogBuilder(requireContext())
+ .setMessage(muteSummary)
+ .setPositiveButton(R.string.ConversationSettingsFragment__unmute) { dialog, _ ->
+ viewModel.unmute()
+ dialog.dismiss()
+ }
+ .setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
+ .show()
+ }
+ }
+ )
+
+ if (state.hasMentionsSupport) {
+ val mentionSelection = if (state.mentionSetting == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY) {
+ 0
+ } else {
+ 1
+ }
+
+ radioListPref(
+ title = DSLSettingsText.from(R.string.SoundsAndNotificationsSettingsFragment__mentions),
+ icon = DSLSettingsIcon.from(R.drawable.ic_at_24),
+ selected = mentionSelection,
+ listItems = mentionLabels,
+ onSelected = {
+ viewModel.setMentionSetting(
+ if (it == 0) {
+ RecipientDatabase.MentionSetting.ALWAYS_NOTIFY
+ } else {
+ RecipientDatabase.MentionSetting.DO_NOT_NOTIFY
+ }
+ )
+ }
+ )
+ }
+
+ val customSoundSummary = if (state.hasCustomNotificationSettings) {
+ R.string.preferences_on
+ } else {
+ R.string.preferences_off
+ }
+
+ clickPref(
+ title = DSLSettingsText.from(R.string.SoundsAndNotificationsSettingsFragment__custom_notifications),
+ icon = DSLSettingsIcon.from(R.drawable.ic_speaker_24),
+ summary = DSLSettingsText.from(customSoundSummary),
+ onClick = {
+ CustomNotificationsDialogFragment.create(state.recipientId).show(parentFragmentManager, null)
+ }
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsRepository.kt
new file mode 100644
index 000000000..58d133b19
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsRepository.kt
@@ -0,0 +1,37 @@
+package org.thoughtcrime.securesms.components.settings.conversation.sounds
+
+import android.content.Context
+import org.signal.core.util.concurrent.SignalExecutors
+import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.database.RecipientDatabase
+import org.thoughtcrime.securesms.notifications.NotificationChannels
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.recipients.RecipientId
+
+class SoundsAndNotificationsSettingsRepository(private val context: Context) {
+
+ fun setMuteUntil(recipientId: RecipientId, muteUntil: Long) {
+ SignalExecutors.BOUNDED.execute {
+ DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, muteUntil)
+ }
+ }
+
+ fun setMentionSetting(recipientId: RecipientId, mentionSetting: RecipientDatabase.MentionSetting) {
+ SignalExecutors.BOUNDED.execute {
+ DatabaseFactory.getRecipientDatabase(context).setMentionSetting(recipientId, mentionSetting)
+ }
+ }
+
+ fun hasCustomNotificationSettings(recipientId: RecipientId, consumer: (Boolean) -> Unit) {
+ SignalExecutors.BOUNDED.execute {
+ val recipient = Recipient.resolved(recipientId)
+ consumer(
+ if (recipient.notificationChannel != null || !NotificationChannels.supported()) {
+ true
+ } else {
+ NotificationChannels.updateWithShortcutBasedChannel(context, recipient)
+ }
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState.kt
new file mode 100644
index 000000000..11ff87414
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState.kt
@@ -0,0 +1,13 @@
+package org.thoughtcrime.securesms.components.settings.conversation.sounds
+
+import org.thoughtcrime.securesms.database.RecipientDatabase
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.recipients.RecipientId
+
+data class SoundsAndNotificationsSettingsState(
+ val recipientId: RecipientId = Recipient.UNKNOWN.id,
+ val muteUntil: Long = 0L,
+ val mentionSetting: RecipientDatabase.MentionSetting = RecipientDatabase.MentionSetting.DO_NOT_NOTIFY,
+ val hasCustomNotificationSettings: Boolean = false,
+ val hasMentionsSupport: Boolean = false
+)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel.kt
new file mode 100644
index 000000000..f8806d116
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel.kt
@@ -0,0 +1,51 @@
+package org.thoughtcrime.securesms.components.settings.conversation.sounds
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.thoughtcrime.securesms.database.RecipientDatabase
+import org.thoughtcrime.securesms.recipients.Recipient
+import org.thoughtcrime.securesms.recipients.RecipientId
+import org.thoughtcrime.securesms.util.livedata.Store
+
+class SoundsAndNotificationsSettingsViewModel(
+ private val recipientId: RecipientId,
+ private val repository: SoundsAndNotificationsSettingsRepository
+) : ViewModel() {
+
+ private val store = Store(SoundsAndNotificationsSettingsState())
+
+ val state: LiveData = store.stateLiveData
+
+ init {
+ store.update(Recipient.live(recipientId).liveData) { recipient, state ->
+ state.copy(
+ recipientId = recipientId,
+ muteUntil = recipient.muteUntil,
+ mentionSetting = recipient.mentionSetting,
+ hasMentionsSupport = recipient.isPushV2Group
+ )
+ }
+ }
+
+ fun setMuteUntil(muteUntil: Long) {
+ repository.setMuteUntil(recipientId, muteUntil)
+ }
+
+ fun unmute() {
+ repository.setMuteUntil(recipientId, 0L)
+ }
+
+ fun setMentionSetting(mentionSetting: RecipientDatabase.MentionSetting) {
+ repository.setMentionSetting(recipientId, mentionSetting)
+ }
+
+ class Factory(
+ private val recipientId: RecipientId,
+ private val repository: SoundsAndNotificationsSettingsRepository
+ ) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return requireNotNull(modelClass.cast(SoundsAndNotificationsSettingsViewModel(recipientId, repository)))
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt
index d35f5df3e..04cd62df3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt
@@ -1,13 +1,10 @@
package org.thoughtcrime.securesms.components.settings
import androidx.annotation.CallSuper
-import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.thoughtcrime.securesms.util.MappingModel
import org.thoughtcrime.securesms.util.MappingModelList
-private const val UNSET = -1
-
fun configure(init: DSLConfiguration.() -> Unit): DSLConfiguration {
val configuration = DSLConfiguration()
configuration.init()
@@ -23,13 +20,24 @@ class DSLConfiguration {
fun radioListPref(
title: DSLSettingsText,
- @DrawableRes iconId: Int = UNSET,
+ icon: DSLSettingsIcon? = null,
+ dialogTitle: DSLSettingsText = title,
isEnabled: Boolean = true,
listItems: Array,
selected: Int,
+ confirmAction: Boolean = false,
onSelected: (Int) -> Unit
) {
- val preference = RadioListPreference(title, iconId, isEnabled, listItems, selected, onSelected)
+ val preference = RadioListPreference(
+ title = title,
+ icon = icon,
+ isEnabled = isEnabled,
+ dialogTitle = dialogTitle,
+ listItems = listItems,
+ selected = selected,
+ confirmAction = confirmAction,
+ onSelected = onSelected
+ )
children.add(preference)
}
@@ -47,12 +55,12 @@ class DSLConfiguration {
fun switchPref(
title: DSLSettingsText,
summary: DSLSettingsText? = null,
- @DrawableRes iconId: Int = UNSET,
+ icon: DSLSettingsIcon? = null,
isEnabled: Boolean = true,
isChecked: Boolean,
onClick: () -> Unit
) {
- val preference = SwitchPreference(title, summary, iconId, isEnabled, isChecked, onClick)
+ val preference = SwitchPreference(title, summary, icon, isEnabled, isChecked, onClick)
children.add(preference)
}
@@ -70,20 +78,20 @@ class DSLConfiguration {
fun clickPref(
title: DSLSettingsText,
summary: DSLSettingsText? = null,
- @DrawableRes iconId: Int = UNSET,
+ icon: DSLSettingsIcon? = null,
isEnabled: Boolean = true,
onClick: () -> Unit
) {
- val preference = ClickPreference(title, summary, iconId, isEnabled, onClick)
+ val preference = ClickPreference(title, summary, icon, isEnabled, onClick)
children.add(preference)
}
fun externalLinkPref(
title: DSLSettingsText,
- @DrawableRes iconId: Int = UNSET,
+ icon: DSLSettingsIcon? = null,
@StringRes linkId: Int
) {
- val preference = ExternalLinkPreference(title, iconId, linkId)
+ val preference = ExternalLinkPreference(title, icon, linkId)
children.add(preference)
}
@@ -116,8 +124,8 @@ class DSLConfiguration {
abstract class PreferenceModel>(
open val title: DSLSettingsText? = null,
open val summary: DSLSettingsText? = null,
- @DrawableRes open val iconId: Int = UNSET,
- open val isEnabled: Boolean = true
+ open val icon: DSLSettingsIcon? = null,
+ open val isEnabled: Boolean = true,
) : MappingModel {
override fun areItemsTheSame(newItem: T): Boolean {
return when {
@@ -131,7 +139,7 @@ abstract class PreferenceModel>(
override fun areContentsTheSame(newItem: T): Boolean {
return areItemsTheSame(newItem) &&
newItem.summary == summary &&
- newItem.iconId == iconId &&
+ newItem.icon == icon &&
newItem.isEnabled == isEnabled
}
}
@@ -147,12 +155,14 @@ class DividerPreference : PreferenceModel() {
class RadioListPreference(
override val title: DSLSettingsText,
- @DrawableRes override val iconId: Int = UNSET,
+ override val icon: DSLSettingsIcon? = null,
override val isEnabled: Boolean,
+ val dialogTitle: DSLSettingsText = title,
val listItems: Array,
val selected: Int,
- val onSelected: (Int) -> Unit
-) : PreferenceModel(title = title, iconId = iconId, isEnabled = isEnabled) {
+ val onSelected: (Int) -> Unit,
+ val confirmAction: Boolean = false
+) : PreferenceModel() {
override fun areContentsTheSame(newItem: RadioListPreference): Boolean {
return super.areContentsTheSame(newItem) && listItems.contentEquals(newItem.listItems) && selected == newItem.selected
@@ -176,11 +186,11 @@ class MultiSelectListPreference(
class SwitchPreference(
override val title: DSLSettingsText,
override val summary: DSLSettingsText? = null,
- @DrawableRes override val iconId: Int = UNSET,
- isEnabled: Boolean,
+ override val icon: DSLSettingsIcon? = null,
+ override val isEnabled: Boolean,
val isChecked: Boolean,
val onClick: () -> Unit
-) : PreferenceModel(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled) {
+) : PreferenceModel() {
override fun areContentsTheSame(newItem: SwitchPreference): Boolean {
return super.areContentsTheSame(newItem) && isChecked == newItem.isChecked
}
@@ -201,15 +211,15 @@ class RadioPreference(
class ClickPreference(
override val title: DSLSettingsText,
override val summary: DSLSettingsText? = null,
- @DrawableRes override val iconId: Int = UNSET,
- isEnabled: Boolean = true,
+ override val icon: DSLSettingsIcon? = null,
+ override val isEnabled: Boolean = true,
val onClick: () -> Unit
-) : PreferenceModel(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled)
+) : PreferenceModel()
class ExternalLinkPreference(
override val title: DSLSettingsText,
- @DrawableRes override val iconId: Int,
+ override val icon: DSLSettingsIcon?,
@StringRes val linkId: Int
-) : PreferenceModel(title = title, iconId = iconId)
+) : PreferenceModel()
-class SectionHeaderPreference(override val title: DSLSettingsText) : PreferenceModel(title = title)
+class SectionHeaderPreference(override val title: DSLSettingsText) : PreferenceModel()
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 9ee832d53..20b471414 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -44,7 +44,6 @@ import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
-import android.view.Display;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
@@ -66,12 +65,12 @@ import android.widget.Toast;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
+import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
@@ -130,6 +129,7 @@ import org.thoughtcrime.securesms.components.reminder.Reminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
+import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
@@ -142,7 +142,6 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationM
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
-import org.thoughtcrime.securesms.search.MessageResult;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -174,7 +173,6 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
-import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog;
import org.thoughtcrime.securesms.insights.InsightsLauncher;
@@ -239,8 +237,8 @@ import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
-import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
+import org.thoughtcrime.securesms.search.MessageResult;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
@@ -338,6 +336,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
private static final String TAG = Log.tag(ConversationActivity.class);
private static final String STATE_REACT_WITH_ANY_PAGE = "STATE_REACT_WITH_ANY_PAGE";
+ private static final String STATE_HANDLED_INIT_SEARCH = "STATE_HANDLED_INIT_SEARCH";
+
+ private static final int REQUEST_CODE_SETTINGS = 1000;
private static final int PICK_GALLERY = 1;
private static final int PICK_DOCUMENT = 2;
@@ -496,13 +497,15 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
});
initializeInsightObserver();
+
+ handleStartWithSearchOpen();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.i(TAG, "onNewIntent()");
-
+
if (isFinishing()) {
Log.w(TAG, "Activity is finishing...");
return;
@@ -549,6 +552,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
searchNav.setVisibility(View.GONE);
+
+ handleStartWithSearchOpen();
}
@Override
@@ -774,6 +779,16 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
}
+ private void handleStartWithSearchOpen() {
+ if (viewModel.getArgs().isWithSearchOpen()) {
+ toolbar.postDelayed(() -> {
+ if (searchViewItem.expandActionView()) {
+ searchViewModel.onSearchOpened();
+ }
+ }, 500);
+ }
+ }
+
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
@@ -1178,8 +1193,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (isInMessageRequest()) return;
- Intent intent = ManageRecipientActivity.newIntentFromConversation(this, recipient.getId());
- startActivitySceneTransition(intent, titleView.findViewById(R.id.contact_photo_image), "avatar");
+ Intent intent = ConversationSettingsActivity.forRecipient(this, recipient.getId());
+ Bundle bundle = ConversationSettingsActivity.createTransitionBundle(this, titleView.findViewById(R.id.contact_photo_image), toolbar);
+
+ ActivityCompat.startActivity(this, intent, bundle);
}
private void handleUnmuteNotifications() {
@@ -1338,9 +1355,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
private void handleManageGroup() {
- startActivityForResult(ManageGroupActivity.newIntent(ConversationActivity.this, recipient.get().requireGroupId()),
- GROUP_EDIT,
- ManageGroupActivity.createTransitionBundle(this, titleView.findViewById(R.id.contact_photo_image)));
+ Intent intent = ConversationSettingsActivity.forGroup(this, recipient.get().requireGroupId());
+ Bundle bundle = ConversationSettingsActivity.createTransitionBundle(this, titleView.findViewById(R.id.contact_photo_image), toolbar);
+
+ ActivityCompat.startActivity(this, intent, bundle);
}
private void handleDistributionBroadcastEnabled(MenuItem item) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java
index 05ae0956c..2aa13bb13 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java
@@ -32,6 +32,7 @@ public class ConversationIntents {
private static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
private static final String EXTRA_STARTING_POSITION = "starting_position";
private static final String EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP = "first_time_in_group";
+ private static final String EXTRA_WITH_SEARCH_OPEN = "with_search_open";
private ConversationIntents() {
}
@@ -70,6 +71,7 @@ public class ConversationIntents {
private final int distributionType;
private final int startingPosition;
private final boolean firstTimeInSelfCreatedGroup;
+ private final boolean withSearchOpen;
static Args from(@NonNull Intent intent) {
if (isBubbleIntent(intent)) {
@@ -81,6 +83,7 @@ public class ConversationIntents {
false,
ThreadDatabase.DistributionTypes.DEFAULT,
-1,
+ false,
false);
}
@@ -92,7 +95,8 @@ public class ConversationIntents {
intent.getBooleanExtra(EXTRA_BORDERLESS, false),
intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, ThreadDatabase.DistributionTypes.DEFAULT),
intent.getIntExtra(EXTRA_STARTING_POSITION, -1),
- intent.getBooleanExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, false));
+ intent.getBooleanExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, false),
+ intent.getBooleanExtra(EXTRA_WITH_SEARCH_OPEN, false));
}
private Args(@NonNull RecipientId recipientId,
@@ -103,7 +107,8 @@ public class ConversationIntents {
boolean isBorderless,
int distributionType,
int startingPosition,
- boolean firstTimeInSelfCreatedGroup)
+ boolean firstTimeInSelfCreatedGroup,
+ boolean withSearchOpen)
{
this.recipientId = recipientId;
this.threadId = threadId;
@@ -114,6 +119,7 @@ public class ConversationIntents {
this.distributionType = distributionType;
this.startingPosition = startingPosition;
this.firstTimeInSelfCreatedGroup = firstTimeInSelfCreatedGroup;
+ this.withSearchOpen = withSearchOpen;
}
public @NonNull RecipientId getRecipientId() {
@@ -160,6 +166,10 @@ public class ConversationIntents {
public @NonNull ChatColors getChatColors() {
return Recipient.resolved(recipientId).getChatColors();
}
+
+ public boolean isWithSearchOpen() {
+ return withSearchOpen;
+ }
}
public final static class Builder {
@@ -177,6 +187,7 @@ public class ConversationIntents {
private Uri dataUri;
private String dataType;
private boolean firstTimeInSelfCreatedGroup;
+ private boolean withSearchOpen;
private Builder(@NonNull Context context,
@NonNull RecipientId recipientId,
@@ -236,6 +247,11 @@ public class ConversationIntents {
return this;
}
+ public @NonNull Builder withSearchOpen(boolean withSearchOpen) {
+ this.withSearchOpen = withSearchOpen;
+ return this;
+ }
+
public Builder firstTimeInSelfCreatedGroup() {
this.firstTimeInSelfCreatedGroup = true;
return this;
@@ -265,6 +281,7 @@ public class ConversationIntents {
intent.putExtra(EXTRA_STARTING_POSITION, startingPosition);
intent.putExtra(EXTRA_BORDERLESS, isBorderless);
intent.putExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, firstTimeInSelfCreatedGroup);
+ intent.putExtra(EXTRA_WITH_SEARCH_OPEN, withSearchOpen);
if (draftText != null) {
intent.putExtra(EXTRA_TEXT, draftText);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java
index 25ef7cfa4..da723a694 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java
@@ -2,6 +2,10 @@ package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@@ -25,6 +29,8 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
+import java.util.Objects;
+
public class ConversationTitleView extends RelativeLayout {
private AvatarImageView avatar;
@@ -78,20 +84,21 @@ public class ConversationTitleView extends RelativeLayout {
if (recipient == null) setComposeTitle();
else setRecipientTitle(recipient);
- int startDrawable = 0;
- int endDrawable = 0;
+ Drawable startDrawable = null;
+ Drawable endDrawable = null;
if (recipient != null && recipient.isBlocked()) {
- startDrawable = R.drawable.ic_block_white_18dp;
+ startDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_block_white_18dp);
} else if (recipient != null && recipient.isMuted()) {
- startDrawable = R.drawable.ic_volume_off_white_18dp;
+ startDrawable = Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.ic_bell_disabled_16));
+ startDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18));
}
if (recipient != null && recipient.isSystemContact() && !recipient.isSelf()) {
- endDrawable = R.drawable.ic_profile_circle_outline_16;
+ endDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_profile_circle_outline_16);
}
- title.setCompoundDrawablesRelativeWithIntrinsicBounds(startDrawable, 0, endDrawable, 0);
+ title.setCompoundDrawablesRelativeWithIntrinsicBounds(startDrawable, null, endDrawable, null);
TextViewCompat.setCompoundDrawableTintList(title, ColorStateList.valueOf(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_80)));
if (recipient != null) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/LeaveGroupDialog.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/LeaveGroupDialog.java
index d27fe0f97..fdfb082e0 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/LeaveGroupDialog.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/LeaveGroupDialog.java
@@ -8,6 +8,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import com.annimon.stream.Stream;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
@@ -76,7 +77,7 @@ public final class LeaveGroupDialog {
}
private void showSelectNewAdminDialog() {
- new AlertDialog.Builder(activity)
+ new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.LeaveGroupDialog_choose_new_admin)
.setMessage(R.string.LeaveGroupDialog_before_you_leave_you_must_choose_at_least_one_new_admin_for_this_group)
.setNegativeButton(android.R.string.cancel, null)
@@ -85,7 +86,7 @@ public final class LeaveGroupDialog {
}
private void showLeaveDialog() {
- new AlertDialog.Builder(activity)
+ new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.LeaveGroupDialog_leave_group)
.setCancelable(true)
.setMessage(R.string.LeaveGroupDialog_you_will_no_longer_be_able_to_send_or_receive_messages_in_this_group)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java
index 82326993d..a2cd4df2c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/addmembers/AddMembersActivity.java
@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.groups.ui.addmembers;
+import android.content.Context;
+import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
@@ -9,14 +11,20 @@ import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.ContactSelectionActivity;
+import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.PushContactSelectionActivity;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.groups.GroupId;
+import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
+import java.util.ArrayList;
+import java.util.List;
+
public class AddMembersActivity extends PushContactSelectionActivity {
public static final String GROUP_ID = "group_id";
@@ -24,6 +32,22 @@ public class AddMembersActivity extends PushContactSelectionActivity {
private View done;
private AddMembersViewModel viewModel;
+ public static @NonNull Intent createIntent(@NonNull Context context,
+ @NonNull GroupId groupId,
+ int displayModeFlags,
+ int selectionWarning,
+ int selectionLimit,
+ @NonNull List membersWithoutSelf) {
+ Intent intent = new Intent(context, AddMembersActivity.class);
+
+ intent.putExtra(AddMembersActivity.GROUP_ID, groupId.toString());
+ intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayModeFlags);
+ intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, new SelectionLimits(selectionWarning, selectionLimit));
+ intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(membersWithoutSelf));
+
+ return intent;
+ }
+
@Override
protected void onCreate(Bundle icicle, boolean ready) {
getIntent().putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.add_members_activity);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupActivity.java
deleted file mode 100644
index 52609d71b..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupActivity.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.thoughtcrime.securesms.groups.ui.managegroup;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.ActivityOptionsCompat;
-
-import org.thoughtcrime.securesms.PassphraseRequiredActivity;
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.groups.GroupId;
-import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
-import org.thoughtcrime.securesms.util.DynamicTheme;
-
-public class ManageGroupActivity extends PassphraseRequiredActivity {
-
- private static final String GROUP_ID = "GROUP_ID";
-
- private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
-
- public static Intent newIntent(@NonNull Context context, @NonNull GroupId groupId) {
- Intent intent = new Intent(context, ManageGroupActivity.class);
- intent.putExtra(GROUP_ID, groupId.toString());
- return intent;
- }
-
- public static @Nullable Bundle createTransitionBundle(@NonNull Context activityContext, @NonNull View from) {
- if (activityContext instanceof Activity) {
- return ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) activityContext, from, "avatar").toBundle();
- } else {
- return null;
- }
- }
-
- @Override
- protected void onPreCreate() {
- dynamicTheme.onCreate(this);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState, boolean ready) {
- super.onCreate(savedInstanceState, ready);
- getWindow().getDecorView().setSystemUiVisibility(getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
- setContentView(R.layout.group_manage_activity);
- if (savedInstanceState == null) {
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.container, ManageGroupFragment.newInstance(getIntent().getStringExtra(GROUP_ID)))
- .commitNow();
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
- dynamicTheme.onResume(this);
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
deleted file mode 100644
index 8805884a3..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupFragment.java
+++ /dev/null
@@ -1,521 +0,0 @@
-package org.thoughtcrime.securesms.groups.ui.managegroup;
-
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CompoundButton;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.widget.SwitchCompat;
-import androidx.appcompat.widget.Toolbar;
-import androidx.core.widget.TextViewCompat;
-import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.ViewModelProviders;
-
-import com.google.android.material.snackbar.Snackbar;
-
-import org.signal.core.util.logging.Log;
-import org.thoughtcrime.securesms.AvatarPreviewActivity;
-import org.thoughtcrime.securesms.InviteActivity;
-import org.thoughtcrime.securesms.LoggingFragment;
-import org.thoughtcrime.securesms.MainActivity;
-import org.thoughtcrime.securesms.MediaPreviewActivity;
-import org.thoughtcrime.securesms.MuteDialog;
-import org.thoughtcrime.securesms.PushContactSelectionActivity;
-import org.thoughtcrime.securesms.R;
-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.conversation.colors.AvatarColor;
-import org.thoughtcrime.securesms.groups.GroupId;
-import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
-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;
-import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
-import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
-import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
-import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkDialogFragment;
-import org.thoughtcrime.securesms.util.AsynchronousCallback;
-import org.thoughtcrime.securesms.util.DateUtils;
-import org.thoughtcrime.securesms.util.FeatureFlags;
-import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
-import org.thoughtcrime.securesms.util.LongClickMovementMethod;
-import org.thoughtcrime.securesms.util.ViewUtil;
-import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
-import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
-import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
-
-import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
-
-public class ManageGroupFragment extends LoggingFragment {
- private static final String GROUP_ID = "GROUP_ID";
-
- private static final String TAG = Log.tag(ManageGroupFragment.class);
-
- private static final int RETURN_FROM_MEDIA = 33114;
- private static final int PICK_CONTACT = 61341;
- public static final String DIALOG_TAG = "DIALOG";
-
- private ManageGroupViewModel viewModel;
- private GroupMemberListView groupMemberList;
- private View pendingAndRequestingRow;
- private TextView pendingAndRequestingCount;
- private Toolbar toolbar;
- private TextView groupName;
- private EmojiTextView groupDescription;
- private LearnMoreTextView groupInfoText;
- private TextView memberCountUnderAvatar;
- private TextView memberCountAboveList;
- private AvatarImageView avatar;
- private ThreadPhotoRailView threadPhotoRailView;
- private View groupMediaCard;
- private View accessControlCard;
- private View groupLinkCard;
- private ManageGroupViewModel.CursorFactory cursorFactory;
- private View sharedMediaRow;
- private View editGroupAccessRow;
- private TextView editGroupAccessValue;
- private View editGroupMembershipRow;
- private TextView editGroupMembershipValue;
- private View disappearingMessagesCard;
- private View disappearingMessagesRow;
- private TextView disappearingMessages;
- private View blockAndLeaveCard;
- private TextView blockGroup;
- private TextView unblockGroup;
- private TextView leaveGroup;
- private TextView addMembers;
- private SwitchCompat muteNotificationsSwitch;
- private View muteNotificationsRow;
- private TextView muteNotificationsUntilLabel;
- private TextView customNotificationsButton;
- private View customNotificationsRow;
- private View mentionsRow;
- private TextView mentionsValue;
- private View toggleAllMembers;
- private View groupLinkRow;
- private TextView groupLinkButton;
- private TextView wallpaperButton;
-
- static ManageGroupFragment newInstance(@NonNull String groupId) {
- ManageGroupFragment fragment = new ManageGroupFragment();
- Bundle args = new Bundle();
-
- args.putString(GROUP_ID, groupId);
- fragment.setArguments(args);
-
- return fragment;
- }
-
- @Override
- public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
- @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState)
- {
- View view = inflater.inflate(R.layout.group_manage_fragment, container, false);
-
- 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);
- groupMemberList = view.findViewById(R.id.group_members);
- pendingAndRequestingRow = view.findViewById(R.id.pending_and_requesting_members_row);
- pendingAndRequestingCount = view.findViewById(R.id.pending_and_requesting_members_count);
- threadPhotoRailView = view.findViewById(R.id.recent_photos);
- groupMediaCard = view.findViewById(R.id.group_media_card);
- accessControlCard = view.findViewById(R.id.group_access_control_card);
- groupLinkCard = view.findViewById(R.id.group_link_card);
- sharedMediaRow = view.findViewById(R.id.shared_media_row);
- editGroupAccessRow = view.findViewById(R.id.edit_group_access_row);
- editGroupAccessValue = view.findViewById(R.id.edit_group_access_value);
- editGroupMembershipRow = view.findViewById(R.id.edit_group_membership_row);
- editGroupMembershipValue = view.findViewById(R.id.edit_group_membership_value);
- disappearingMessagesCard = view.findViewById(R.id.group_disappearing_messages_card);
- disappearingMessagesRow = view.findViewById(R.id.disappearing_messages_row);
- disappearingMessages = view.findViewById(R.id.disappearing_messages);
- blockAndLeaveCard = view.findViewById(R.id.group_block_and_leave_card);
- blockGroup = view.findViewById(R.id.blockGroup);
- unblockGroup = view.findViewById(R.id.unblockGroup);
- leaveGroup = view.findViewById(R.id.leaveGroup);
- addMembers = view.findViewById(R.id.add_members);
- muteNotificationsUntilLabel = view.findViewById(R.id.group_mute_notifications_until);
- muteNotificationsSwitch = view.findViewById(R.id.group_mute_notifications_switch);
- muteNotificationsRow = view.findViewById(R.id.group_mute_notifications_row);
- customNotificationsButton = view.findViewById(R.id.group_custom_notifications_button);
- customNotificationsRow = view.findViewById(R.id.group_custom_notifications_row);
- mentionsRow = view.findViewById(R.id.group_mentions_row);
- mentionsValue = view.findViewById(R.id.group_mentions_value);
- toggleAllMembers = view.findViewById(R.id.toggle_all_members);
- groupLinkRow = view.findViewById(R.id.group_link_row);
- groupLinkButton = view.findViewById(R.id.group_link_button);
- wallpaperButton = view.findViewById(R.id.chat_wallpaper);
-
- return view;
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- Context context = requireContext();
- GroupId groupId = getGroupId();
- ManageGroupViewModel.Factory factory = new ManageGroupViewModel.Factory(context, groupId);
-
- disappearingMessagesCard.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
- blockAndLeaveCard.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
-
- viewModel = ViewModelProviders.of(requireActivity(), factory).get(ManageGroupViewModel.class);
-
- viewModel.getMembers().observe(getViewLifecycleOwner(), members -> groupMemberList.setMembers(members));
-
- viewModel.getCanCollapseMemberList().observe(getViewLifecycleOwner(), canCollapseMemberList -> {
- if (canCollapseMemberList) {
- toggleAllMembers.setVisibility(View.VISIBLE);
- toggleAllMembers.setOnClickListener(v -> viewModel.revealCollapsedMembers());
- } else {
- toggleAllMembers.setVisibility(View.GONE);
- }
- });
-
- viewModel.getPendingAndRequestingCount().observe(getViewLifecycleOwner(), pendingAndRequestingCount -> {
- pendingAndRequestingRow.setOnClickListener(v -> {
- FragmentActivity activity = requireActivity();
- activity.startActivity(ManagePendingAndRequestingMembersActivity.newIntent(activity, groupId.requireV2()));
- });
- if (pendingAndRequestingCount == 0) {
- this.pendingAndRequestingCount.setVisibility(View.GONE);
- } else {
- this.pendingAndRequestingCount.setText(String.format(Locale.getDefault(), "%d", pendingAndRequestingCount));
- this.pendingAndRequestingCount.setVisibility(View.VISIBLE);
- }
- });
-
- toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
- toolbar.setOnMenuItemClickListener(this::onMenuItemSelected);
- toolbar.inflateMenu(R.menu.manage_group_fragment);
-
- viewModel.getCanEditGroupAttributes().observe(getViewLifecycleOwner(), canEdit -> {
- toolbar.getMenu().findItem(R.id.action_edit).setVisible(canEdit);
- disappearingMessages.setEnabled(canEdit);
- disappearingMessagesRow.setEnabled(canEdit);
- });
-
- 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 -> {
- avatar.setFallbackPhotoProvider(new FallbackPhotoProvider(groupRecipient.getAvatarColor()));
- avatar.setRecipient(groupRecipient);
- avatar.setOnClickListener(v -> {
- FragmentActivity activity = requireActivity();
- activity.startActivity(AvatarPreviewActivity.intentFromRecipientId(activity, groupRecipient.getId()),
- AvatarPreviewActivity.createTransitionBundle(activity, avatar));
- });
- customNotificationsRow.setOnClickListener(v -> CustomNotificationsDialogFragment.create(groupRecipient.getId())
- .show(requireFragmentManager(), DIALOG_TAG));
- wallpaperButton.setOnClickListener(v -> startActivity(ChatWallpaperActivity.createIntent(requireContext(), groupRecipient.getId())));
-
- Drawable colorCircle = groupRecipient.getChatColors().asCircle();
- colorCircle.setBounds(0, 0, ViewUtil.dpToPx(16), ViewUtil.dpToPx(16));
- TextViewCompat.setCompoundDrawablesRelative(wallpaperButton, null, null, colorCircle, null);
- });
-
- if (groupId.isV2()) {
- groupLinkRow.setOnClickListener(v -> ShareableGroupLinkDialogFragment.create(groupId.requireV2())
- .show(requireFragmentManager(), DIALOG_TAG));
- viewModel.getGroupLinkOn().observe(getViewLifecycleOwner(), linkEnabled -> groupLinkButton.setText(booleanToOnOff(linkEnabled)));
- }
-
- viewModel.getGroupViewState().observe(getViewLifecycleOwner(), vs -> {
- if (vs == null) return;
- sharedMediaRow.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(context, vs.getThreadId())));
-
- setMediaCursorFactory(vs.getMediaCursorFactory());
-
- threadPhotoRailView.setListener(mediaRecord ->
- startActivityForResult(MediaPreviewActivity.intentFromMediaRecord(context,
- mediaRecord,
- ViewUtil.isLtr(threadPhotoRailView)),
- RETURN_FROM_MEDIA));
-
- groupLinkCard.setVisibility(vs.getGroupRecipient().requireGroupId().isV2() ? View.VISIBLE : View.GONE);
- });
-
- leaveGroup.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
- leaveGroup.setOnClickListener(v -> LeaveGroupDialog.handleLeavePushGroup(requireActivity(), groupId.requirePush(), () -> startActivity(MainActivity.clearTop(context))));
-
- viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
-
- disappearingMessagesRow.setOnClickListener(v -> {
- Recipient recipient = viewModel.getGroupRecipient().getValue();
- if (recipient != null) {
- startActivity(RecipientDisappearingMessagesActivity.forRecipient(requireContext(), recipient.getId()));
- }
- });
- blockGroup.setOnClickListener(v -> viewModel.blockAndLeave(requireActivity()));
- unblockGroup.setOnClickListener(v -> viewModel.unblock(requireActivity()));
-
- addMembers.setOnClickListener(v -> viewModel.onAddMembersClick(this, PICK_CONTACT));
-
- viewModel.getMembershipRights().observe(getViewLifecycleOwner(), r -> {
- if (r != null) {
- editGroupMembershipValue.setText(r.getString());
- editGroupMembershipRow.setOnClickListener(v -> new GroupRightsDialog(context, GroupRightsDialog.Type.MEMBERSHIP, r, (from, to) -> viewModel.applyMembershipRightsChange(to)).show());
- }
- }
- );
-
- viewModel.getEditGroupAttributesRights().observe(getViewLifecycleOwner(), r -> {
- if (r != null) {
- editGroupAccessValue.setText(r.getString());
- editGroupAccessRow.setOnClickListener(v -> new GroupRightsDialog(context, GroupRightsDialog.Type.ATTRIBUTES, r, (from, to) -> viewModel.applyAttributesRightsChange(to)).show());
- }
- }
- );
-
- viewModel.getIsAdmin().observe(getViewLifecycleOwner(), admin -> {
- accessControlCard.setVisibility(admin ? View.VISIBLE : View.GONE);
- editGroupMembershipRow.setEnabled(admin);
- editGroupMembershipValue.setEnabled(admin);
- editGroupAccessRow.setEnabled(admin);
- editGroupAccessValue.setEnabled(admin);
- });
-
- viewModel.getCanAddMembers().observe(getViewLifecycleOwner(), canEdit -> addMembers.setVisibility(canEdit ? View.VISIBLE : View.GONE));
-
- groupMemberList.setRecipientClickListener(recipient -> RecipientBottomSheetDialogFragment.create(recipient.getId(), groupId).show(requireFragmentManager(), "BOTTOM"));
- groupMemberList.setOverScrollMode(View.OVER_SCROLL_NEVER);
-
- final CompoundButton.OnCheckedChangeListener muteSwitchListener = (buttonView, isChecked) -> {
- if (isChecked) {
- MuteDialog.show(context, viewModel::setMuteUntil, () -> muteNotificationsSwitch.setChecked(false));
- } else {
- viewModel.clearMuteUntil();
- }
- };
-
- muteNotificationsRow.setOnClickListener(v -> {
- if (muteNotificationsSwitch.isEnabled()) {
- muteNotificationsSwitch.toggle();
- }
- });
-
- viewModel.getMuteState().observe(getViewLifecycleOwner(), muteState -> {
- if (muteNotificationsSwitch.isChecked() != muteState.isMuted()) {
- muteNotificationsSwitch.setOnCheckedChangeListener(null);
- muteNotificationsSwitch.setChecked(muteState.isMuted());
- }
-
- muteNotificationsSwitch.setEnabled(true);
- muteNotificationsSwitch.setOnCheckedChangeListener(muteSwitchListener);
- muteNotificationsUntilLabel.setVisibility(muteState.isMuted() ? View.VISIBLE : View.GONE);
-
- if (muteState.isMuted()) {
- if (muteState.getMutedUntil() == Long.MAX_VALUE) {
- muteNotificationsUntilLabel.setText(R.string.ManageGroupActivity_always);
- } else {
- muteNotificationsUntilLabel.setText(getString(R.string.ManageGroupActivity_until_s,
- DateUtils.getTimeString(requireContext(),
- Locale.getDefault(),
- muteState.getMutedUntil())));
- }
- }
- });
-
- customNotificationsRow.setVisibility(View.VISIBLE);
-
- if (NotificationChannels.supported()) {
- viewModel.hasCustomNotifications().observe(getViewLifecycleOwner(), hasCustomNotifications -> {
- customNotificationsButton.setText(booleanToOnOff(hasCustomNotifications));
- });
- }
-
- mentionsRow.setVisibility(groupId.isV2() ? View.VISIBLE : View.GONE);
- mentionsRow.setOnClickListener(v -> viewModel.handleMentionNotificationSelection());
- viewModel.getMentionSetting().observe(getViewLifecycleOwner(), value -> mentionsValue.setText(value));
-
- viewModel.getCanLeaveGroup().observe(getViewLifecycleOwner(), canLeave -> leaveGroup.setVisibility(canLeave ? View.VISIBLE : View.GONE));
- viewModel.getCanBlockGroup().observe(getViewLifecycleOwner(), canBlock -> blockGroup.setVisibility(canBlock ? View.VISIBLE : View.GONE));
- viewModel.getCanUnblockGroup().observe(getViewLifecycleOwner(), canUnblock -> unblockGroup.setVisibility(canUnblock ? View.VISIBLE : View.GONE));
-
- viewModel.getGroupInfoMessage().observe(getViewLifecycleOwner(), message -> {
- switch (message) {
- case LEGACY_GROUP_LEARN_MORE:
- groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_learn_more);
- groupInfoText.setOnLinkClickListener(v -> GroupsLearnMoreBottomSheetDialogFragment.show(requireFragmentManager()));
- groupInfoText.setLearnMoreVisible(true);
- groupInfoText.setVisibility(View.VISIBLE);
- break;
- case LEGACY_GROUP_UPGRADE:
- groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_upgrade);
- groupInfoText.setOnLinkClickListener(v -> GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(requireFragmentManager(), Recipient.externalPossiblyMigratedGroup(requireContext(), groupId).getId()));
- groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_upgrade_this_group);
- groupInfoText.setVisibility(View.VISIBLE);
- break;
- case LEGACY_GROUP_TOO_LARGE:
- groupInfoText.setText(context.getString(R.string.ManageGroupActivity_legacy_group_too_large, FeatureFlags.groupLimits().getHardLimit() - 1));
- groupInfoText.setLearnMoreVisible(false);
- groupInfoText.setVisibility(View.VISIBLE);
- break;
- case MMS_WARNING:
- groupInfoText.setText(R.string.ManageGroupActivity_this_is_an_insecure_mms_group);
- groupInfoText.setOnLinkClickListener(v -> startActivity(new Intent(requireContext(), InviteActivity.class)));
- groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_invite_now);
- groupInfoText.setVisibility(View.VISIBLE);
- break;
- default:
- groupInfoText.setVisibility(View.GONE);
- break;
- }
- });
- }
-
- private static int booleanToOnOff(boolean isOn) {
- return isOn ? R.string.ManageGroupActivity_on
- : R.string.ManageGroupActivity_off;
- }
-
- public boolean onMenuItemSelected(@NonNull MenuItem item) {
- if (item.getItemId() == R.id.action_edit) {
- startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getGroupId()));
- return true;
- }
-
- return false;
- }
-
- private GroupId getGroupId() {
- return GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(GROUP_ID)));
- }
-
- private void setMediaCursorFactory(@Nullable ManageGroupViewModel.CursorFactory cursorFactory) {
- if (this.cursorFactory != cursorFactory) {
- this.cursorFactory = cursorFactory;
- applyMediaCursorFactory();
- }
- }
-
- private void applyMediaCursorFactory() {
- Context context = getContext();
- if (context == null) return;
- if (this.cursorFactory != null) {
- Cursor cursor = this.cursorFactory.create();
- getViewLifecycleOwner().getLifecycle().addObserver(new LifecycleCursorWrapper(cursor));
-
- threadPhotoRailView.setCursor(GlideApp.with(context), cursor);
- groupMediaCard.setVisibility(cursor.getCount() > 0 ? View.VISIBLE : View.GONE);
- } else {
- threadPhotoRailView.setCursor(GlideApp.with(context), null);
- groupMediaCard.setVisibility(View.GONE);
- }
- }
-
- private void updateGroupDescription(@NonNull ManageGroupViewModel.Description description) {
- if (!TextUtils.isEmpty(description.getDescription()) || description.canEditDescription()) {
- groupDescription.setVisibility(View.VISIBLE);
- groupDescription.setMovementMethod(LongClickMovementMethod.getInstance(requireContext()));
- memberCountUnderAvatar.setVisibility(View.GONE);
- } else {
- groupDescription.setVisibility(View.GONE);
- groupDescription.setMovementMethod(null);
- memberCountUnderAvatar.setVisibility(View.VISIBLE);
- }
-
- if (TextUtils.isEmpty(description.getDescription())) {
- if (description.canEditDescription()) {
- groupDescription.setOverflowText(null);
- groupDescription.setText(R.string.ManageGroupActivity_add_group_description);
- groupDescription.setOnClickListener(v -> startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getGroupId())));
- }
- } else {
- groupDescription.setOnClickListener(null);
- GroupDescriptionUtil.setText(requireContext(),
- groupDescription,
- 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);
-
- if (requestCode == RETURN_FROM_MEDIA) {
- applyMediaCursorFactory();
- } else if (requestCode == PICK_CONTACT && data != null) {
- List selected = data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS);
- SimpleProgressDialog.DismissibleDialog progress = SimpleProgressDialog.showDelayed(requireContext());
-
- viewModel.onAddMembers(selected, new AsynchronousCallback.MainThread() {
- @Override
- public void onComplete(ManageGroupViewModel.AddMembersResult result) {
- progress.dismiss();
- if (!result.getNewInvitedMembers().isEmpty()) {
- GroupInviteSentDialog.showInvitesSent(requireContext(), result.getNewInvitedMembers());
- }
-
- if (result.getNumberOfMembersAdded() > 0) {
- String string = getResources().getQuantityString(R.plurals.ManageGroupActivity_added,
- result.getNumberOfMembersAdded(),
- result.getNumberOfMembersAdded());
- Snackbar.make(requireView(), string, Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show();
- }
- }
-
- @Override
- public void onError(@Nullable GroupChangeFailureReason error) {
- progress.dismiss();
- Toast.makeText(requireContext(), GroupErrors.getUserDisplayMessage(error), Toast.LENGTH_LONG).show();
- }
- });
- }
- }
-
- private final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
-
- private final AvatarColor groupColors;
-
- private FallbackPhotoProvider(@NonNull AvatarColor groupColors) {
- this.groupColors = groupColors;
- }
-
- @Override
- public @NonNull FallbackContactPhoto getPhotoForGroup() {
- return new FallbackPhoto80dp(R.drawable.ic_group_80, groupColors.colorInt());
- }
- };
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java
deleted file mode 100644
index 22c988239..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupRepository.java
+++ /dev/null
@@ -1,233 +0,0 @@
-package org.thoughtcrime.securesms.groups.ui.managegroup;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-import androidx.core.util.Consumer;
-
-import com.annimon.stream.Stream;
-
-import org.signal.core.util.concurrent.SignalExecutors;
-import org.signal.core.util.logging.Log;
-import org.signal.storageservice.protos.groups.local.DecryptedGroup;
-import org.thoughtcrime.securesms.ContactSelectionListFragment;
-import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.database.GroupDatabase;
-import org.thoughtcrime.securesms.database.RecipientDatabase;
-import org.thoughtcrime.securesms.database.ThreadDatabase;
-import org.thoughtcrime.securesms.groups.GroupAccessControl;
-import org.thoughtcrime.securesms.groups.GroupChangeException;
-import org.thoughtcrime.securesms.groups.GroupId;
-import org.thoughtcrime.securesms.groups.GroupManager;
-import org.thoughtcrime.securesms.groups.GroupProtoUtil;
-import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception;
-import org.thoughtcrime.securesms.groups.SelectionLimits;
-import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback;
-import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
-import org.thoughtcrime.securesms.notifications.NotificationChannels;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.recipients.RecipientUtil;
-import org.thoughtcrime.securesms.util.AsynchronousCallback;
-import org.thoughtcrime.securesms.util.FeatureFlags;
-import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-
-final class ManageGroupRepository {
-
- private static final String TAG = Log.tag(ManageGroupRepository.class);
-
- private final Context context;
-
- ManageGroupRepository(@NonNull Context context) {
- this.context = context;
- }
-
- void getGroupState(@NonNull GroupId groupId, @NonNull Consumer onGroupStateLoaded) {
- SignalExecutors.BOUNDED.execute(() -> onGroupStateLoaded.accept(getGroupState(groupId)));
- }
-
- void getGroupCapacity(@NonNull GroupId groupId, @NonNull Consumer onGroupCapacityLoaded) {
- SimpleTask.run(SignalExecutors.BOUNDED, () -> {
- GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get();
- if (groupRecord.isV2Group()) {
- DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
- List pendingMembers = Stream.of(decryptedGroup.getPendingMembersList())
- .map(member -> GroupProtoUtil.uuidByteStringToRecipientId(member.getUuid()))
- .toList();
- List members = new LinkedList<>(groupRecord.getMembers());
-
- members.addAll(pendingMembers);
-
- return new GroupCapacityResult(members, FeatureFlags.groupLimits());
- } else {
- return new GroupCapacityResult(groupRecord.getMembers(), FeatureFlags.groupLimits());
- }
- }, onGroupCapacityLoaded::accept);
- }
-
- @WorkerThread
- private GroupStateResult getGroupState(@NonNull GroupId groupId) {
- ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
- Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
- long threadId = threadDatabase.getThreadIdFor(groupRecipient);
-
- return new GroupStateResult(threadId, groupRecipient);
- }
-
- void applyMembershipRightsChange(@NonNull GroupId groupId, @NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
- SignalExecutors.UNBOUNDED.execute(() -> {
- try {
- GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights);
- } catch (GroupChangeException | IOException e) {
- Log.w(TAG, e);
- error.onError(GroupChangeFailureReason.fromException(e));
- }
- });
- }
-
- void applyAttributesRightsChange(@NonNull GroupId groupId, @NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
- SignalExecutors.UNBOUNDED.execute(() -> {
- try {
- GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights);
- } catch (GroupChangeException | IOException e) {
- Log.w(TAG, e);
- error.onError(GroupChangeFailureReason.fromException(e));
- }
- });
- }
-
- public void getRecipient(@NonNull GroupId groupId, @NonNull Consumer recipientCallback) {
- SimpleTask.run(SignalExecutors.BOUNDED,
- () -> Recipient.externalGroupExact(context, groupId),
- recipientCallback::accept);
- }
-
- void setMuteUntil(@NonNull GroupId groupId, long until) {
- SignalExecutors.BOUNDED.execute(() -> {
- RecipientId recipientId = Recipient.externalGroupExact(context, groupId).getId();
- DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until);
- });
- }
-
- void addMembers(@NonNull GroupId groupId,
- @NonNull List selected,
- @NonNull AsynchronousCallback.WorkerThread callback)
- {
- SignalExecutors.UNBOUNDED.execute(() -> {
- try {
- GroupManager.GroupActionResult groupActionResult = GroupManager.addMembers(context, groupId.requirePush(), selected);
- callback.onComplete(new ManageGroupViewModel.AddMembersResult(groupActionResult.getAddedMemberCount(), Recipient.resolvedList(groupActionResult.getInvitedMembers())));
- } catch (GroupChangeException | MembershipNotSuitableForV2Exception | IOException e) {
- Log.w(TAG, e);
- callback.onError(GroupChangeFailureReason.fromException(e));
- }
- });
- }
-
- void blockAndLeaveGroup(@NonNull GroupId groupId, @NonNull GroupChangeErrorCallback error, @NonNull Runnable onSuccess) {
- SignalExecutors.UNBOUNDED.execute(() -> {
- try {
- RecipientUtil.block(context, Recipient.externalGroupExact(context, groupId));
- onSuccess.run();
- } catch (GroupChangeException | IOException e) {
- Log.w(TAG, e);
- error.onError(GroupChangeFailureReason.fromException(e));
- }
- });
- }
-
- void setMentionSetting(@NonNull GroupId groupId, RecipientDatabase.MentionSetting mentionSetting) {
- SignalExecutors.BOUNDED.execute(() -> {
- RecipientId recipientId = Recipient.externalGroupExact(context, groupId).getId();
- DatabaseFactory.getRecipientDatabase(context).setMentionSetting(recipientId, mentionSetting);
- });
- }
-
- @WorkerThread
- boolean hasCustomNotifications(Recipient recipient) {
- if (recipient.getNotificationChannel() != null || !NotificationChannels.supported()) {
- return true;
- }
-
- return NotificationChannels.updateWithShortcutBasedChannel(context, recipient);
- }
-
- static final class GroupStateResult {
-
- private final long threadId;
- private final Recipient recipient;
-
- private GroupStateResult(long threadId,
- Recipient recipient)
- {
- this.threadId = threadId;
- this.recipient = recipient;
- }
-
- long getThreadId() {
- return threadId;
- }
-
- Recipient getRecipient() {
- return recipient;
- }
- }
-
- static final class GroupCapacityResult {
- private final List members;
- private final SelectionLimits selectionLimits;
-
- GroupCapacityResult(@NonNull List members, @NonNull SelectionLimits selectionLimits) {
- this.members = members;
- this.selectionLimits = selectionLimits;
- }
-
- public @NonNull List getMembers() {
- return members;
- }
-
- public int getSelectionLimit() {
- if (!selectionLimits.hasHardLimit()) {
- return ContactSelectionListFragment.NO_LIMIT;
- }
-
- boolean containsSelf = members.indexOf(Recipient.self().getId()) != -1;
-
- return selectionLimits.getHardLimit() - (containsSelf ? 1 : 0);
- }
-
- public int getSelectionWarning() {
- if (!selectionLimits.hasRecommendedLimit()) {
- return ContactSelectionListFragment.NO_LIMIT;
- }
-
- boolean containsSelf = members.indexOf(Recipient.self().getId()) != -1;
-
- return selectionLimits.getRecommendedLimit() - (containsSelf ? 1 : 0);
- }
-
- public int getRemainingCapacity() {
- return selectionLimits.getHardLimit() - members.size();
- }
-
- public @NonNull List getMembersWithoutSelf() {
- ArrayList recipientIds = new ArrayList<>(members.size());
- RecipientId selfId = Recipient.self().getId();
-
- for (RecipientId recipientId : members) {
- if (!recipientId.equals(selfId)) {
- recipientIds.add(recipientId);
- }
- }
-
- return recipientIds;
- }
- }
-
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
deleted file mode 100644
index ebd737e24..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/ManageGroupViewModel.java
+++ /dev/null
@@ -1,468 +0,0 @@
-package org.thoughtcrime.securesms.groups.ui.managegroup;
-
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.text.TextUtils;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.Transformations;
-import androidx.lifecycle.ViewModel;
-import androidx.lifecycle.ViewModelProvider;
-
-import org.signal.core.util.ThreadUtil;
-import org.thoughtcrime.securesms.BlockUnblockDialog;
-import org.thoughtcrime.securesms.ContactSelectionListFragment;
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
-import org.thoughtcrime.securesms.database.MediaDatabase;
-import org.thoughtcrime.securesms.database.MentionUtil;
-import org.thoughtcrime.securesms.database.loaders.MediaLoader;
-import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
-import org.thoughtcrime.securesms.groups.GroupAccessControl;
-import org.thoughtcrime.securesms.groups.GroupId;
-import org.thoughtcrime.securesms.groups.LiveGroup;
-import org.thoughtcrime.securesms.groups.SelectionLimits;
-import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
-import org.thoughtcrime.securesms.groups.ui.GroupErrors;
-import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
-import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
-import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity;
-import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupMentionSettingDialog;
-import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.recipients.RecipientUtil;
-import org.thoughtcrime.securesms.util.AsynchronousCallback;
-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;
-import java.util.List;
-
-public class ManageGroupViewModel extends ViewModel {
-
- private static final int MAX_UNCOLLAPSED_MEMBERS = 6;
- private static final int SHOW_COLLAPSED_MEMBERS = 5;
-
- private final Context context;
- private final ManageGroupRepository manageGroupRepository;
- private final LiveData title;
- private final Store descriptionStore;
- private final LiveData description;
- private final LiveData isAdmin;
- private final LiveData canEditGroupAttributes;
- private final LiveData canAddMembers;
- private final LiveData> members;
- private final LiveData pendingMemberCount;
- private final LiveData pendingAndRequestingCount;
- private final LiveData disappearingMessageTimer;
- private final LiveData memberCountSummary;
- private final LiveData fullMemberCountSummary;
- private final LiveData editMembershipRights;
- private final LiveData editGroupAttributesRights;
- private final LiveData groupRecipient;
- private final MutableLiveData groupViewState = new MutableLiveData<>(null);
- private final LiveData muteState;
- private final LiveData hasCustomNotifications;
- private final LiveData canCollapseMemberList;
- private final DefaultValueLiveData memberListCollapseState = new DefaultValueLiveData<>(CollapseState.COLLAPSED);
- private final LiveData canLeaveGroup;
- private final LiveData canBlockGroup;
- private final LiveData canUnblockGroup;
- private final LiveData showLegacyIndicator;
- private final LiveData mentionSetting;
- private final LiveData groupLinkOn;
- private final LiveData groupInfoMessage;
-
- private ManageGroupViewModel(@NonNull Context context, @NonNull GroupId groupId, @NonNull ManageGroupRepository manageGroupRepository) {
- this.context = context;
- this.manageGroupRepository = manageGroupRepository;
-
- manageGroupRepository.getGroupState(groupId, this::groupStateLoaded);
-
- LiveGroup liveGroup = new LiveGroup(groupId);
-
- this.title = Transformations.map(liveGroup.getTitle(),
- title -> TextUtils.isEmpty(title) ? context.getString(R.string.Recipient_unknown)
- : title);
- this.groupRecipient = liveGroup.getGroupRecipient();
- this.isAdmin = liveGroup.isSelfAdmin();
- this.canCollapseMemberList = LiveDataUtil.combineLatest(memberListCollapseState,
- Transformations.map(liveGroup.getFullMembers(), m -> m.size() > MAX_UNCOLLAPSED_MEMBERS),
- (state, hasEnoughMembers) -> state != CollapseState.OPEN && hasEnoughMembers);
- this.members = LiveDataUtil.combineLatest(liveGroup.getFullMembers(),
- memberListCollapseState,
- ManageGroupViewModel::filterMemberList);
- this.pendingMemberCount = liveGroup.getPendingMemberCount();
- this.pendingAndRequestingCount = liveGroup.getPendingAndRequestingMemberCount();
- this.showLegacyIndicator = Transformations.map(groupRecipient, recipient -> recipient.requireGroupId().isV1());
- this.memberCountSummary = LiveDataUtil.combineLatest(liveGroup.getMembershipCountDescription(context.getResources()),
- this.showLegacyIndicator,
- (description, legacy) -> legacy ? String.format("%s ยท %s", description, context.getString(R.string.ManageGroupActivity_legacy_group))
- : description);
- this.fullMemberCountSummary = liveGroup.getFullMembershipCountDescription(context.getResources());
- this.editMembershipRights = liveGroup.getMembershipAdditionAccessControl();
- this.editGroupAttributesRights = liveGroup.getAttributesAccessControl();
- this.disappearingMessageTimer = Transformations.map(liveGroup.getExpireMessages(), expiration -> ExpirationUtil.getExpirationDisplayValue(context, expiration));
- this.canEditGroupAttributes = liveGroup.selfCanEditGroupAttributes();
- this.canAddMembers = liveGroup.selfCanAddMembers();
- this.muteState = Transformations.map(this.groupRecipient,
- recipient -> new MuteState(recipient.getMuteUntil(), recipient.isMuted()));
- this.hasCustomNotifications = LiveDataUtil.mapAsync(this.groupRecipient, manageGroupRepository::hasCustomNotifications);
- this.canLeaveGroup = liveGroup.isActive();
- this.canBlockGroup = Transformations.map(this.groupRecipient, recipient -> RecipientUtil.isBlockable(recipient) && !recipient.isBlocked());
- this.canUnblockGroup = Transformations.map(this.groupRecipient, Recipient::isBlocked);
- this.mentionSetting = Transformations.distinctUntilChanged(Transformations.map(this.groupRecipient,
- recipient -> MentionUtil.getMentionSettingDisplayValue(context, recipient.getMentionSetting())));
- this.groupLinkOn = Transformations.map(liveGroup.getGroupLink(), GroupLinkUrlAndStatus::isEnabled);
- this.groupInfoMessage = Transformations.map(this.groupRecipient,
- recipient -> {
- boolean showLegacyInfo = recipient.requireGroupId().isV1();
-
- if (showLegacyInfo && recipient.getParticipants().size() > FeatureFlags.groupLimits().getHardLimit()) {
- return GroupInfoMessage.LEGACY_GROUP_TOO_LARGE;
- } else if (showLegacyInfo) {
- return GroupInfoMessage.LEGACY_GROUP_UPGRADE;
- } else if (groupId.isMms()) {
- return GroupInfoMessage.MMS_WARNING;
- } else {
- 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
- private void groupStateLoaded(@NonNull ManageGroupRepository.GroupStateResult groupStateResult) {
- groupViewState.postValue(new GroupViewState(groupStateResult.getThreadId(),
- groupStateResult.getRecipient(),
- () -> new ThreadMediaLoader(context, groupStateResult.getThreadId(), MediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest).getCursor()));
- }
-
- LiveData> getMembers() {
- return members;
- }
-
- LiveData getPendingMemberCount() {
- return pendingMemberCount;
- }
-
- LiveData getPendingAndRequestingCount() {
- return pendingAndRequestingCount;
- }
-
- LiveData getMemberCountSummary() {
- return memberCountSummary;
- }
-
- LiveData getFullMemberCountSummary() {
- return fullMemberCountSummary;
- }
-
- LiveData getGroupRecipient() {
- return groupRecipient;
- }
-
- LiveData getGroupViewState() {
- return groupViewState;
- }
-
- LiveData getTitle() {
- return title;
- }
-
- LiveData getDescription() {
- return description;
- }
-
- LiveData getMuteState() {
- return muteState;
- }
-
- LiveData getMembershipRights() {
- return editMembershipRights;
- }
-
- LiveData getEditGroupAttributesRights() {
- return editGroupAttributesRights;
- }
-
- LiveData getIsAdmin() {
- return isAdmin;
- }
-
- LiveData getCanEditGroupAttributes() {
- return canEditGroupAttributes;
- }
-
- LiveData getCanAddMembers() {
- return canAddMembers;
- }
-
- LiveData getDisappearingMessageTimer() {
- return disappearingMessageTimer;
- }
-
- LiveData hasCustomNotifications() {
- return hasCustomNotifications;
- }
-
- LiveData getCanCollapseMemberList() {
- return canCollapseMemberList;
- }
-
- LiveData getCanBlockGroup() {
- return canBlockGroup;
- }
-
- LiveData getCanUnblockGroup() {
- return canUnblockGroup;
- }
-
- LiveData getCanLeaveGroup() {
- return canLeaveGroup;
- }
-
- LiveData getMentionSetting() {
- return mentionSetting;
- }
-
- LiveData getGroupLinkOn() {
- return groupLinkOn;
- }
-
- LiveData getGroupInfoMessage() {
- return groupInfoMessage;
- }
-
- void applyMembershipRightsChange(@NonNull GroupAccessControl newRights) {
- manageGroupRepository.applyMembershipRightsChange(getGroupId(), newRights, this::showErrorToast);
- }
-
- void applyAttributesRightsChange(@NonNull GroupAccessControl newRights) {
- manageGroupRepository.applyAttributesRightsChange(getGroupId(), newRights, this::showErrorToast);
- }
-
- void blockAndLeave(@NonNull FragmentActivity activity) {
- manageGroupRepository.getRecipient(getGroupId(),
- recipient -> BlockUnblockDialog.showBlockFor(activity,
- activity.getLifecycle(),
- recipient,
- this::onBlockAndLeaveConfirmed));
- }
-
- void unblock(@NonNull FragmentActivity activity) {
- manageGroupRepository.getRecipient(getGroupId(),
- recipient -> BlockUnblockDialog.showUnblockFor(activity, activity.getLifecycle(), recipient,
- () -> RecipientUtil.unblock(context, recipient)));
- }
-
- void onAddMembers(@NonNull List selected,
- @NonNull AsynchronousCallback.MainThread callback)
- {
- manageGroupRepository.addMembers(getGroupId(), selected, callback.toWorkerCallback());
- }
-
- void setMuteUntil(long muteUntil) {
- manageGroupRepository.setMuteUntil(getGroupId(), muteUntil);
- }
-
- void clearMuteUntil() {
- manageGroupRepository.setMuteUntil(getGroupId(), 0);
- }
-
- void revealCollapsedMembers() {
- memberListCollapseState.setValue(CollapseState.OPEN);
- }
-
- void handleMentionNotificationSelection() {
- manageGroupRepository.getRecipient(getGroupId(), r -> GroupMentionSettingDialog.show(context, r.getMentionSetting(), setting -> manageGroupRepository.setMentionSetting(getGroupId(), setting)));
- }
-
- private void onBlockAndLeaveConfirmed() {
- SimpleProgressDialog.DismissibleDialog dismissibleDialog = SimpleProgressDialog.showDelayed(context);
-
- manageGroupRepository.blockAndLeaveGroup(getGroupId(),
- e -> {
- dismissibleDialog.dismiss();
- showErrorToast(e);
- },
- dismissibleDialog::dismiss);
- }
-
- private @NonNull GroupId getGroupId() {
- return groupRecipient.getValue().requireGroupId();
- }
-
- private static @NonNull List filterMemberList(@NonNull List members,
- @NonNull CollapseState collapseState)
- {
- if (collapseState == CollapseState.COLLAPSED && members.size() > MAX_UNCOLLAPSED_MEMBERS) {
- return members.subList(0, SHOW_COLLAPSED_MEMBERS);
- } else {
- return members;
- }
- }
-
- @WorkerThread
- private void showErrorToast(@NonNull GroupChangeFailureReason e) {
- ThreadUtil.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show());
- }
-
- public void onAddMembersClick(@NonNull Fragment fragment, int resultCode) {
- manageGroupRepository.getGroupCapacity(getGroupId(), capacity -> {
- int remainingCapacity = capacity.getRemainingCapacity();
- if (remainingCapacity <= 0) {
- GroupLimitDialog.showHardLimitMessage(fragment.requireContext());
- } else {
- Intent intent = new Intent(fragment.requireActivity(), AddMembersActivity.class);
- intent.putExtra(AddMembersActivity.GROUP_ID, getGroupId().toString());
- intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
- intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, new SelectionLimits(capacity.getSelectionWarning(), capacity.getSelectionLimit()));
- intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(capacity.getMembersWithoutSelf()));
- fragment.startActivityForResult(intent, resultCode);
- }
- });
- }
-
- static final class AddMembersResult {
- private final int numberOfMembersAdded;
- private final List newInvitedMembers;
-
- AddMembersResult(int numberOfMembersAdded, @NonNull List newInvitedMembers) {
- this.numberOfMembersAdded = numberOfMembersAdded;
- this.newInvitedMembers = newInvitedMembers;
- }
-
- int getNumberOfMembersAdded() {
- return numberOfMembersAdded;
- }
-
- List getNewInvitedMembers() {
- return newInvitedMembers;
- }
- }
-
- static final class GroupViewState {
- private final long threadId;
- @NonNull private final Recipient groupRecipient;
- @NonNull private final CursorFactory mediaCursorFactory;
-
- private GroupViewState(long threadId,
- @NonNull Recipient groupRecipient,
- @NonNull CursorFactory mediaCursorFactory)
- {
- this.threadId = threadId;
- this.groupRecipient = groupRecipient;
- this.mediaCursorFactory = mediaCursorFactory;
- }
-
- long getThreadId() {
- return threadId;
- }
-
- @NonNull Recipient getGroupRecipient() {
- return groupRecipient;
- }
-
- @NonNull CursorFactory getMediaCursorFactory() {
- return mediaCursorFactory;
- }
- }
-
- static final class MuteState {
- private final long mutedUntil;
- private final boolean isMuted;
-
- MuteState(long mutedUntil, boolean isMuted) {
- this.mutedUntil = mutedUntil;
- this.isMuted = isMuted;
- }
-
- public long getMutedUntil() {
- return mutedUntil;
- }
-
- public boolean isMuted() {
- return isMuted;
- }
- }
-
- enum GroupInfoMessage {
- NONE,
- LEGACY_GROUP_LEARN_MORE,
- LEGACY_GROUP_UPGRADE,
- LEGACY_GROUP_TOO_LARGE,
- MMS_WARNING
- }
-
- private enum CollapseState {
- OPEN,
- COLLAPSED
- }
-
- interface CursorFactory {
- 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;
-
- public Factory(@NonNull Context context, @NonNull GroupId groupId) {
- this.context = context;
- this.groupId = groupId;
- }
-
- @Override
- public @NonNull T create(@NonNull Class modelClass) {
- //noinspection unchecked
- return (T) new ManageGroupViewModel(context, groupId, new ManageGroupRepository(context.getApplicationContext()));
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientSettingsCoordinatorLayoutBehavior.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientSettingsCoordinatorLayoutBehavior.java
deleted file mode 100644
index cf3c6d1e9..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/RecipientSettingsCoordinatorLayoutBehavior.java
+++ /dev/null
@@ -1,130 +0,0 @@
-package org.thoughtcrime.securesms.recipients.ui;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-import android.widget.TextView;
-
-import androidx.annotation.IdRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.coordinatorlayout.widget.CoordinatorLayout;
-
-import com.google.android.material.appbar.AppBarLayout;
-
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.util.ViewUtil;
-
-import java.lang.ref.WeakReference;
-
-public final class RecipientSettingsCoordinatorLayoutBehavior extends CoordinatorLayout.Behavior {
-
- private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
-
- private final ViewReference avatarTargetRef = new ViewReference(R.id.avatar_target);
- private final ViewReference nameRef = new ViewReference(R.id.name);
- private final ViewReference nameTargetRef = new ViewReference(R.id.name_target);
- private final Rect targetRect = new Rect();
- private final Rect childRect = new Rect();
-
- public RecipientSettingsCoordinatorLayoutBehavior(@NonNull Context context, @Nullable AttributeSet attrs) {
- }
-
- @Override
- public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
- return dependency instanceof AppBarLayout;
- }
-
- @Override
- public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
- AppBarLayout appBarLayout = (AppBarLayout) dependency;
- int range = appBarLayout.getTotalScrollRange();
- float factor = INTERPOLATOR.getInterpolation(-appBarLayout.getY() / range);
-
- updateAvatarPositionAndScale(parent, child, factor);
- updateNamePosition(parent, factor);
-
- return true;
- }
-
- private void updateAvatarPositionAndScale(@NonNull CoordinatorLayout parent, @NonNull View child, float factor) {
- View target = avatarTargetRef.require(parent);
-
- targetRect.set(target.getLeft(), target.getTop(), target.getRight(), target.getBottom());
- childRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
-
- float widthScale = 1f - (1f - (targetRect.width() / (float) childRect.width())) * factor;
- float heightScale = 1f - (1f - (targetRect.height() / (float) childRect.height())) * factor;
-
- float superimposedLeft = childRect.left + (childRect.width() - targetRect.width()) / 2f;
- float superimposedTop = childRect.top + (childRect.height() - targetRect.height()) / 2f;
-
- float xTranslation = (targetRect.left - superimposedLeft) * factor;
- float yTranslation = (targetRect.top - superimposedTop) * factor;
-
- child.setScaleX(widthScale);
- child.setScaleY(heightScale);
- child.setTranslationX(xTranslation);
- child.setTranslationY(yTranslation);
- }
-
- private void updateNamePosition(@NonNull CoordinatorLayout parent, float factor) {
- TextView child = (TextView) nameRef.require(parent);
- View target = nameTargetRef.require(parent);
-
- targetRect.set(target.getLeft(), target.getTop(), target.getRight(), target.getBottom());
- childRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
-
- if (child.getMaxWidth() != targetRect.width()) {
- child.setMaxWidth(targetRect.width());
- }
-
- float deltaTop = targetRect.top - childRect.top;
- float deltaStart = getStart(parent, targetRect) - getStart(parent, childRect);
-
- float yTranslation = deltaTop * factor;
- float xTranslation = deltaStart * factor;
-
- child.setTranslationY(yTranslation);
- child.setTranslationX(xTranslation);
- }
-
- private static int getStart(@NonNull CoordinatorLayout parent, @NonNull Rect rect) {
- return ViewUtil.isLtr(parent) ? rect.left : rect.right;
- }
-
- private static final class ViewReference {
-
- private WeakReference ref = new WeakReference<>(null);
-
- private final @IdRes int idRes;
-
- private ViewReference(@IdRes int idRes) {
- this.idRes = idRes;
- }
-
- private @NonNull View require(@NonNull View parent) {
- View view = ref.get();
-
- if (view == null) {
- view = getChildOrThrow(parent, idRes);
- ref = new WeakReference<>(view);
- }
-
- return view;
- }
-
- private static @NonNull View getChildOrThrow(@NonNull View parent, @IdRes int id) {
- View child = parent.findViewById(id);
-
- if (child == null) {
- throw new AssertionError("Can't find view with ID " + R.id.avatar_target);
- } else {
- return child;
- }
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java
index 78c52ff3b..816730ff1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java
@@ -25,6 +25,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
+import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon;
+import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
import org.thoughtcrime.securesms.groups.GroupId;
@@ -34,6 +36,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
+import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -41,6 +44,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
+import kotlin.Unit;
+
/**
* A bottom sheet that shows some simple recipient details, as well as some actions (like calling,
* adding to contacts, etc).
@@ -57,10 +62,6 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
private TextView fullName;
private TextView about;
private TextView usernameNumber;
- private Button messageButton;
- private Button secureCallButton;
- private Button insecureCallButton;
- private Button secureVideoCallButton;
private Button blockButton;
private Button unblockButton;
private Button addContactButton;
@@ -71,6 +72,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
private Button removeFromGroupButton;
private ProgressBar adminActionBusy;
private View noteToSelfDescription;
+ private View buttonStrip;
public static BottomSheetDialogFragment create(@NonNull RecipientId recipientId,
@Nullable GroupId groupId)
@@ -105,10 +107,6 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
fullName = view.findViewById(R.id.rbs_full_name);
about = view.findViewById(R.id.rbs_about);
usernameNumber = view.findViewById(R.id.rbs_username_number);
- messageButton = view.findViewById(R.id.rbs_message_button);
- secureCallButton = view.findViewById(R.id.rbs_secure_call_button);
- insecureCallButton = view.findViewById(R.id.rbs_insecure_call_button);
- secureVideoCallButton = view.findViewById(R.id.rbs_video_call_button);
blockButton = view.findViewById(R.id.rbs_block_button);
unblockButton = view.findViewById(R.id.rbs_unblock_button);
addContactButton = view.findViewById(R.id.rbs_add_contact_button);
@@ -119,6 +117,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
removeFromGroupButton = view.findViewById(R.id.rbs_remove_from_group_button);
adminActionBusy = view.findViewById(R.id.rbs_admin_action_busy);
noteToSelfDescription = view.findViewById(R.id.rbs_note_to_self_description);
+ buttonStrip = view.findViewById(R.id.button_strip);
return view;
}
@@ -192,10 +191,41 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
unblockButton.setVisibility(View.GONE);
}
- messageButton.setVisibility(!recipient.isSelf() ? View.VISIBLE : View.GONE);
- secureCallButton.setVisibility(recipient.isRegistered() && !recipient.isSelf() ? View.VISIBLE : View.GONE);
- insecureCallButton.setVisibility(!recipient.isRegistered() && !recipient.isSelf() ? View.VISIBLE : View.GONE);
- secureVideoCallButton.setVisibility(recipient.isRegistered() && !recipient.isSelf() ? View.VISIBLE : View.GONE);
+ ButtonStripPreference.State buttonStripState = new ButtonStripPreference.State(
+ /* isMessageAvailable = */ !recipient.isSelf(),
+ /* isVideoAvailable = */ recipient.isRegistered() && !recipient.isSelf(),
+ /* isAudioAvailable = */ !recipient.isSelf(),
+ /* isMuteAvailable = */ false,
+ /* isSearchAvailable = */ false,
+ /* isAudioSecure = */ recipient.isRegistered(),
+ /* isMuted = */ false
+ );
+
+ ButtonStripPreference.Model buttonStripModel = new ButtonStripPreference.Model(
+ buttonStripState,
+ DSLSettingsIcon.from(ContextUtil.requireDrawable(requireContext(), R.drawable.selectable_recipient_bottom_sheet_icon_button)),
+ () -> {
+ dismiss();
+ viewModel.onMessageClicked(requireActivity());
+ return Unit.INSTANCE;
+ },
+ () -> {
+ viewModel.onSecureVideoCallClicked(requireActivity());
+ return Unit.INSTANCE;
+ },
+ () -> {
+ if (buttonStripState.isAudioSecure()) {
+ viewModel.onSecureCallClicked(requireActivity());
+ } else {
+ viewModel.onInsecureCallClicked(requireActivity());
+ }
+ return Unit.INSTANCE;
+ },
+ () -> Unit.INSTANCE,
+ () -> Unit.INSTANCE
+ );
+
+ new ButtonStripPreference.ViewHolder(buttonStrip).bind(buttonStripModel);
if (recipient.isSystemContact() || recipient.isGroup() || recipient.isSelf()) {
addContactButton.setVisibility(View.GONE);
@@ -234,15 +264,6 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
viewModel.onAvatarClicked(requireActivity());
});
- messageButton.setOnClickListener(view -> {
- dismiss();
- viewModel.onMessageClicked(requireActivity());
- });
-
- secureCallButton.setOnClickListener(view -> viewModel.onSecureCallClicked(requireActivity()));
- insecureCallButton.setOnClickListener(view -> viewModel.onInsecureCallClicked(requireActivity()));
- secureVideoCallButton.setOnClickListener(view -> viewModel.onSecureVideoCallClicked(requireActivity()));
-
blockButton.setOnClickListener(view -> viewModel.onBlockClicked(requireActivity()));
unblockButton.setOnClickListener(view -> viewModel.onUnblockClicked(requireActivity()));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java
index ccc30154b..e1c38734c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java
@@ -19,6 +19,7 @@ import org.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.BlockUnblockDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
+import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
@@ -29,7 +30,6 @@ import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
-import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
@@ -138,13 +138,13 @@ final class RecipientDialogViewModel extends ViewModel {
}
void onAvatarClicked(@NonNull Activity activity) {
- activity.startActivity(ManageRecipientActivity.newIntent(activity, recipientDialogRepository.getRecipientId()));
+ activity.startActivity(ConversationSettingsActivity.forRecipient(activity, recipientDialogRepository.getRecipientId()));
}
void onMakeGroupAdminClicked(@NonNull Activity activity) {
new AlertDialog.Builder(activity)
.setMessage(context.getString(R.string.RecipientBottomSheet_s_will_be_able_to_edit_group, Objects.requireNonNull(recipient.getValue()).getDisplayName(context)))
- .setPositiveButton(R.string.RecipientBottomSheet_make_group_admin,
+ .setPositiveButton(R.string.RecipientBottomSheet_make_admin,
(dialog, which) -> {
adminActionBusy.setValue(true);
recipientDialogRepository.setMemberAdmin(true, result -> {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientActivity.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientActivity.java
deleted file mode 100644
index 34e7a0187..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientActivity.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package org.thoughtcrime.securesms.recipients.ui.managerecipient;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.app.ActivityOptionsCompat;
-
-import org.thoughtcrime.securesms.PassphraseRequiredActivity;
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
-import org.thoughtcrime.securesms.util.DynamicTheme;
-
-public class ManageRecipientActivity extends PassphraseRequiredActivity {
-
- private static final String RECIPIENT_ID = "RECIPIENT_ID";
- private static final String FROM_CONVERSATION = "FROM_CONVERSATION";
-
- private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
-
- public static Intent newIntent(@NonNull Context context, @NonNull RecipientId recipientId) {
- Intent intent = new Intent(context, ManageRecipientActivity.class);
- intent.putExtra(RECIPIENT_ID, recipientId);
- return intent;
- }
-
- /**
- * Makes the message button behave like back.
- */
- public static Intent newIntentFromConversation(@NonNull Context context, @NonNull RecipientId recipientId) {
- Intent intent = new Intent(context, ManageRecipientActivity.class);
- intent.putExtra(RECIPIENT_ID, recipientId);
- intent.putExtra(FROM_CONVERSATION, true);
- return intent;
- }
-
- public static @Nullable Bundle createTransitionBundle(@NonNull Context activityContext, @NonNull View from) {
- if (activityContext instanceof Activity) {
- return ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) activityContext, from, "avatar").toBundle();
- } else {
- return null;
- }
- }
-
- @Override
- protected void onPreCreate() {
- dynamicTheme.onCreate(this);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState, boolean ready) {
- super.onCreate(savedInstanceState, ready);
- getWindow().getDecorView().setSystemUiVisibility(getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
- setContentView(R.layout.recipient_manage_activity);
- if (savedInstanceState == null) {
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.container, ManageRecipientFragment.newInstance(getIntent().getParcelableExtra(RECIPIENT_ID), getIntent().getBooleanExtra(FROM_CONVERSATION, false)))
- .commitNow();
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
- dynamicTheme.onResume(this);
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java
deleted file mode 100644
index 23941ff0b..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientFragment.java
+++ /dev/null
@@ -1,416 +0,0 @@
-package org.thoughtcrime.securesms.recipients.ui.managerecipient;
-
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.widget.SwitchCompat;
-import androidx.appcompat.widget.Toolbar;
-import androidx.core.widget.TextViewCompat;
-import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.ViewModelProviders;
-
-import org.signal.core.util.concurrent.SignalExecutors;
-import org.thoughtcrime.securesms.AvatarPreviewActivity;
-import org.thoughtcrime.securesms.LoggingFragment;
-import org.thoughtcrime.securesms.MediaPreviewActivity;
-import org.thoughtcrime.securesms.MuteDialog;
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.components.AvatarImageView;
-import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
-import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
-import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
-import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
-import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
-import org.thoughtcrime.securesms.keyvalue.SignalStore;
-import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
-import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.notifications.NotificationChannels;
-import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientExporter;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
-import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
-import org.thoughtcrime.securesms.util.DateUtils;
-import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
-import org.thoughtcrime.securesms.util.ServiceUtil;
-import org.thoughtcrime.securesms.util.Util;
-import org.thoughtcrime.securesms.util.ViewUtil;
-import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
-
-import java.util.Locale;
-import java.util.Objects;
-
-public class ManageRecipientFragment extends LoggingFragment {
- private static final String RECIPIENT_ID = "RECIPIENT_ID";
- private static final String FROM_CONVERSATION = "FROM_CONVERSATION";
-
- private static final int REQUEST_CODE_RETURN_FROM_MEDIA = 405;
- private static final int REQUEST_CODE_ADD_CONTACT = 588;
- private static final int REQUEST_CODE_VIEW_CONTACT = 610;
-
- private ManageRecipientViewModel viewModel;
- private GroupMemberListView sharedGroupList;
- private Toolbar toolbar;
- private TextView title;
- private TextView about;
- private TextView subtitle;
- private ViewGroup internalDetails;
- private TextView internalDetailsText;
- private View disableProfileSharingButton;
- private View contactRow;
- private TextView contactText;
- private ImageView contactIcon;
- private AvatarImageView avatar;
- private ThreadPhotoRailView threadPhotoRailView;
- private View mediaCard;
- private ManageRecipientViewModel.CursorFactory cursorFactory;
- private View sharedMediaRow;
- private View disappearingMessagesCard;
- private View disappearingMessagesRow;
- private TextView disappearingMessages;
- private View blockUnblockCard;
- private TextView block;
- private TextView unblock;
- private View groupMembershipCard;
- private TextView addToAGroup;
- private SwitchCompat muteNotificationsSwitch;
- private View muteNotificationsRow;
- private TextView muteNotificationsUntilLabel;
- private View notificationsCard;
- private TextView customNotificationsButton;
- private View customNotificationsRow;
- private View toggleAllGroups;
- private View viewSafetyNumber;
- private TextView groupsInCommonCount;
- private View messageButton;
- private View secureCallButton;
- private View insecureCallButton;
- private View secureVideoCallButton;
- private TextView chatWallpaperButton;
-
- static ManageRecipientFragment newInstance(@NonNull RecipientId recipientId, boolean fromConversation) {
- ManageRecipientFragment fragment = new ManageRecipientFragment();
- Bundle args = new Bundle();
-
- args.putParcelable(RECIPIENT_ID, recipientId);
- args.putBoolean(FROM_CONVERSATION, fromConversation);
- fragment.setArguments(args);
-
- return fragment;
- }
-
- @Override
- public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
- @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState)
- {
- View view = inflater.inflate(R.layout.recipient_manage_fragment, container, false);
-
- avatar = view.findViewById(R.id.recipient_avatar);
- toolbar = view.findViewById(R.id.toolbar);
- contactRow = view.findViewById(R.id.recipient_contact_row);
- contactText = view.findViewById(R.id.recipient_contact_text);
- contactIcon = view.findViewById(R.id.recipient_contact_icon);
- title = view.findViewById(R.id.name);
- about = view.findViewById(R.id.about);
- subtitle = view.findViewById(R.id.username_number);
- internalDetails = view.findViewById(R.id.recipient_internal_details);
- internalDetailsText = view.findViewById(R.id.recipient_internal_details_text);
- disableProfileSharingButton = view.findViewById(R.id.recipient_internal_details_disable_profile_sharing_button);
- sharedGroupList = view.findViewById(R.id.shared_group_list);
- groupsInCommonCount = view.findViewById(R.id.groups_in_common_count);
- threadPhotoRailView = view.findViewById(R.id.recent_photos);
- mediaCard = view.findViewById(R.id.recipient_media_card);
- sharedMediaRow = view.findViewById(R.id.shared_media_row);
- disappearingMessagesCard = view.findViewById(R.id.recipient_disappearing_messages_card);
- disappearingMessagesRow = view.findViewById(R.id.disappearing_messages_row);
- disappearingMessages = view.findViewById(R.id.disappearing_messages);
- blockUnblockCard = view.findViewById(R.id.recipient_block_and_leave_card);
- block = view.findViewById(R.id.block);
- unblock = view.findViewById(R.id.unblock);
- viewSafetyNumber = view.findViewById(R.id.view_safety_number);
- groupMembershipCard = view.findViewById(R.id.recipient_membership_card);
- addToAGroup = view.findViewById(R.id.add_to_a_group);
- muteNotificationsUntilLabel = view.findViewById(R.id.recipient_mute_notifications_until);
- muteNotificationsSwitch = view.findViewById(R.id.recipient_mute_notifications_switch);
- muteNotificationsRow = view.findViewById(R.id.recipient_mute_notifications_row);
- notificationsCard = view.findViewById(R.id.recipient_notifications_card);
- customNotificationsButton = view.findViewById(R.id.recipient_custom_notifications_button);
- customNotificationsRow = view.findViewById(R.id.recipient_custom_notifications_row);
- toggleAllGroups = view.findViewById(R.id.toggle_all_groups);
- messageButton = view.findViewById(R.id.recipient_message);
- secureCallButton = view.findViewById(R.id.recipient_voice_call);
- insecureCallButton = view.findViewById(R.id.recipient_insecure_voice_call);
- secureVideoCallButton = view.findViewById(R.id.recipient_video_call);
- chatWallpaperButton = view.findViewById(R.id.chat_wallpaper);
-
- return view;
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- RecipientId recipientId = Objects.requireNonNull(requireArguments().getParcelable(RECIPIENT_ID));
- boolean fromConversation = requireArguments().getBoolean(FROM_CONVERSATION, false);
- ManageRecipientViewModel.Factory factory = new ManageRecipientViewModel.Factory(recipientId);
-
- viewModel = ViewModelProviders.of(requireActivity(), factory).get(ManageRecipientViewModel.class);
-
- viewModel.getCanCollapseMemberList().observe(getViewLifecycleOwner(), canCollapseMemberList -> {
- if (canCollapseMemberList) {
- toggleAllGroups.setVisibility(View.VISIBLE);
- toggleAllGroups.setOnClickListener(v -> viewModel.revealCollapsedMembers());
- } else {
- toggleAllGroups.setVisibility(View.GONE);
- }
- });
-
- viewModel.getIdentity().observe(getViewLifecycleOwner(), identityRecord -> {
- viewSafetyNumber.setVisibility(identityRecord != null ? View.VISIBLE : View.GONE);
-
- if (identityRecord != null) {
- viewSafetyNumber.setOnClickListener(view -> viewModel.onViewSafetyNumberClicked(requireActivity(), identityRecord));
- }
- });
-
- toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
- toolbar.setOnMenuItemClickListener(this::onMenuItemSelected);
- toolbar.inflateMenu(R.menu.manage_recipient_fragment);
-
- if (recipientId.equals(Recipient.self().getId())) {
- notificationsCard.setVisibility(View.GONE);
- groupMembershipCard.setVisibility(View.GONE);
- blockUnblockCard.setVisibility(View.GONE);
- contactRow.setVisibility(View.GONE);
- } else {
- viewModel.getVisibleSharedGroups().observe(getViewLifecycleOwner(), members -> sharedGroupList.setMembers(members));
- viewModel.getSharedGroupsCountSummary().observe(getViewLifecycleOwner(), members -> groupsInCommonCount.setText(members));
- addToAGroup.setOnClickListener(v -> viewModel.onAddToGroupButton(requireActivity()));
- sharedGroupList.setRecipientClickListener(recipient -> viewModel.onGroupClicked(requireActivity(), recipient));
- sharedGroupList.setOverScrollMode(View.OVER_SCROLL_NEVER);
- }
-
- viewModel.getTitle().observe(getViewLifecycleOwner(), title::setText);
- viewModel.getSubtitle().observe(getViewLifecycleOwner(), text -> {
- subtitle.setText(text);
- subtitle.setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE);
- subtitle.setOnLongClickListener(null);
- title.setOnLongClickListener(null);
- setCopyToClipboardOnLongPress(TextUtils.isEmpty(text) ? title : subtitle);
- });
- viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
- viewModel.getRecipient().observe(getViewLifecycleOwner(), this::presentRecipient);
- viewModel.getMediaCursor().observe(getViewLifecycleOwner(), this::presentMediaCursor);
- viewModel.getMuteState().observe(getViewLifecycleOwner(), this::presentMuteState);
- viewModel.getCanAddToAGroup().observe(getViewLifecycleOwner(), canAdd -> addToAGroup.setVisibility(canAdd ? View.VISIBLE : View.GONE));
-
- if (SignalStore.internalValues().recipientDetails()) {
- viewModel.getInternalDetails().observe(getViewLifecycleOwner(), internalDetailsText::setText);
- disableProfileSharingButton.setOnClickListener(v -> {
- SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getRecipientDatabase(requireContext()).setProfileSharing(recipientId, false));
- });
- internalDetails.setVisibility(View.VISIBLE);
- } else {
- internalDetails.setVisibility(View.GONE);
- }
-
- disappearingMessagesRow.setOnClickListener(v -> startActivity(RecipientDisappearingMessagesActivity.forRecipient(requireContext(), recipientId)));
- block.setOnClickListener(v -> viewModel.onBlockClicked(requireActivity()));
- unblock.setOnClickListener(v -> viewModel.onUnblockClicked(requireActivity()));
-
- muteNotificationsRow.setOnClickListener(v -> {
- if (muteNotificationsSwitch.isEnabled()) {
- muteNotificationsSwitch.toggle();
- }
- });
-
- customNotificationsRow.setVisibility(View.VISIBLE);
- customNotificationsRow.setOnClickListener(v -> CustomNotificationsDialogFragment.create(recipientId)
- .show(requireFragmentManager(), "CUSTOM_NOTIFICATIONS"));
-
- //noinspection CodeBlock2Expr
- if (NotificationChannels.supported()) {
- viewModel.hasCustomNotifications().observe(getViewLifecycleOwner(), hasCustomNotifications -> {
- customNotificationsButton.setText(hasCustomNotifications ? R.string.ManageRecipientActivity_on
- : R.string.ManageRecipientActivity_off);
- });
- }
-
- viewModel.getCanBlock().observe(getViewLifecycleOwner(),
- canBlock -> block.setVisibility(canBlock ? View.VISIBLE : View.GONE));
-
- viewModel.getCanUnblock().observe(getViewLifecycleOwner(),
- canUnblock -> unblock.setVisibility(canUnblock ? View.VISIBLE : View.GONE));
-
- messageButton.setOnClickListener(v -> {
- if (fromConversation) {
- requireActivity().onBackPressed();
- } else {
- viewModel.onMessage(requireActivity());
- }
- });
- secureCallButton.setOnClickListener(v -> viewModel.onSecureCall(requireActivity()));
- insecureCallButton.setOnClickListener(v -> viewModel.onInsecureCall(requireActivity()));
- secureVideoCallButton.setOnClickListener(v -> viewModel.onSecureVideoCall(requireActivity()));
- chatWallpaperButton.setOnClickListener(v -> startActivity(ChatWallpaperActivity.createIntent(requireContext(), recipientId)));
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == REQUEST_CODE_RETURN_FROM_MEDIA) {
- applyMediaCursorFactory();
- } else if (requestCode == REQUEST_CODE_ADD_CONTACT) {
- viewModel.onAddedToContacts();
- } else if (requestCode == REQUEST_CODE_VIEW_CONTACT) {
- viewModel.onFinishedViewingContact();
- }
- }
-
- private void presentRecipient(@NonNull Recipient recipient) {
- Drawable colorCircle = recipient.getChatColors().asCircle();
- colorCircle.setBounds(0, 0, ViewUtil.dpToPx(16), ViewUtil.dpToPx(16));
- TextViewCompat.setCompoundDrawablesRelative(chatWallpaperButton, null, null, colorCircle, null);
-
- if (recipient.isSystemContact()) {
- contactText.setText(R.string.ManageRecipientActivity_this_person_is_in_your_contacts);
- contactIcon.setVisibility(View.VISIBLE);
- contactRow.setOnClickListener(v -> {
- startActivityForResult(new Intent(Intent.ACTION_VIEW, recipient.getContactUri()), REQUEST_CODE_VIEW_CONTACT);
- });
- } else {
- contactText.setText(R.string.ManageRecipientActivity_add_to_system_contacts);
- contactIcon.setVisibility(View.GONE);
- contactRow.setOnClickListener(v -> {
- startActivityForResult(RecipientExporter.export(recipient).asAddContactIntent(), REQUEST_CODE_ADD_CONTACT);
- });
- }
-
- String aboutText = recipient.getCombinedAboutAndEmoji();
- about.setText(aboutText);
- about.setVisibility(Util.isEmpty(aboutText) ? View.GONE : View.VISIBLE);
-
- disappearingMessagesCard.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE);
- addToAGroup.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE);
-
- AvatarColor recipientColor = recipient.getAvatarColor();
- avatar.setFallbackPhotoProvider(new Recipient.FallbackPhotoProvider() {
- @Override
- public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
- return new FallbackPhoto80dp(R.drawable.ic_profile_80, recipientColor.colorInt());
- }
-
- @Override
- public @NonNull FallbackContactPhoto getPhotoForLocalNumber() {
- return new FallbackPhoto80dp(R.drawable.ic_note_80, recipientColor.colorInt());
- }
- });
- avatar.setAvatar(recipient);
- avatar.setOnClickListener(v -> {
- FragmentActivity activity = requireActivity();
- activity.startActivity(AvatarPreviewActivity.intentFromRecipientId(activity, recipient.getId()),
- AvatarPreviewActivity.createTransitionBundle(activity, avatar));
- });
-
- secureCallButton.setVisibility(recipient.isRegistered() && !recipient.isSelf() ? View.VISIBLE : View.GONE);
- insecureCallButton.setVisibility(!recipient.isRegistered() && !recipient.isSelf() ? View.VISIBLE : View.GONE);
- secureVideoCallButton.setVisibility(recipient.isRegistered() && !recipient.isSelf() ? View.VISIBLE : View.GONE);
- }
-
- private void presentMediaCursor(ManageRecipientViewModel.MediaCursor mediaCursor) {
- if (mediaCursor == null) return;
- sharedMediaRow.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(requireContext(), mediaCursor.getThreadId())));
-
- setMediaCursorFactory(mediaCursor.getMediaCursorFactory());
-
- threadPhotoRailView.setListener(mediaRecord ->
- startActivityForResult(MediaPreviewActivity.intentFromMediaRecord(requireContext(),
- mediaRecord,
- ViewUtil.isLtr(threadPhotoRailView)),
- REQUEST_CODE_RETURN_FROM_MEDIA));
- }
-
- private void presentMuteState(@NonNull ManageRecipientViewModel.MuteState muteState) {
- if (muteNotificationsSwitch.isChecked() != muteState.isMuted()) {
- muteNotificationsSwitch.setOnCheckedChangeListener(null);
- muteNotificationsSwitch.setChecked(muteState.isMuted());
- }
-
- muteNotificationsSwitch.setEnabled(true);
- muteNotificationsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
- if (isChecked) {
- MuteDialog.show(requireContext(), viewModel::setMuteUntil, () -> muteNotificationsSwitch.setChecked(false));
- } else {
- viewModel.clearMuteUntil();
- }
- });
- muteNotificationsUntilLabel.setVisibility(muteState.isMuted() ? View.VISIBLE : View.GONE);
-
- if (muteState.isMuted()) {
- if (muteState.getMutedUntil() == Long.MAX_VALUE) {
- muteNotificationsUntilLabel.setText(R.string.ManageRecipientActivity_always);
- } else {
- muteNotificationsUntilLabel.setText(getString(R.string.ManageRecipientActivity_until_s,
- DateUtils.getTimeString(requireContext(),
- Locale.getDefault(),
- muteState.getMutedUntil())));
- }
- }
- }
-
- public boolean onMenuItemSelected(@NonNull MenuItem item) {
- if (item.getItemId() == R.id.action_edit) {
- startActivity(EditProfileActivity.getIntentForUserProfileEdit(requireActivity()));
- return true;
- }
-
- return false;
- }
-
- private void setMediaCursorFactory(@Nullable ManageRecipientViewModel.CursorFactory cursorFactory) {
- if (this.cursorFactory != cursorFactory) {
- this.cursorFactory = cursorFactory;
- applyMediaCursorFactory();
- }
- }
-
- private void applyMediaCursorFactory() {
- Context context = getContext();
- if (context == null) return;
- if (cursorFactory != null) {
- Cursor cursor = cursorFactory.create();
- getViewLifecycleOwner().getLifecycle().addObserver(new LifecycleCursorWrapper(cursor));
-
- threadPhotoRailView.setCursor(GlideApp.with(context), cursor);
- mediaCard.setVisibility(cursor.getCount() > 0 ? View.VISIBLE : View.GONE);
- } else {
- threadPhotoRailView.setCursor(GlideApp.with(context), null);
- mediaCard.setVisibility(View.GONE);
- }
- }
-
- private static void setCopyToClipboardOnLongPress(@NonNull TextView textView) {
- textView.setOnLongClickListener(v -> {
- Util.copyToClipboard(v.getContext(), textView.getText().toString());
- ServiceUtil.getVibrator(v.getContext()).vibrate(250);
- Toast.makeText(v.getContext(), R.string.RecipientBottomSheet_copied_to_clipboard, Toast.LENGTH_SHORT).show();
- return true;
- });
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java
deleted file mode 100644
index 6d69d4bc8..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientRepository.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package org.thoughtcrime.securesms.recipients.ui.managerecipient;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-import androidx.core.util.Consumer;
-
-import com.annimon.stream.Stream;
-
-import org.signal.core.util.concurrent.SignalExecutors;
-import org.signal.core.util.logging.Log;
-import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
-import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.database.GroupDatabase;
-import org.thoughtcrime.securesms.database.IdentityDatabase;
-import org.thoughtcrime.securesms.database.ThreadDatabase;
-import org.thoughtcrime.securesms.notifications.NotificationChannels;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-final class ManageRecipientRepository {
-
- private static final String TAG = Log.tag(ManageRecipientRepository.class);
-
- private final Context context;
- private final RecipientId recipientId;
-
- ManageRecipientRepository(@NonNull Context context, @NonNull RecipientId recipientId) {
- this.context = context;
- this.recipientId = recipientId;
- }
-
- public RecipientId getRecipientId() {
- return recipientId;
- }
-
- void getThreadId(@NonNull Consumer onGetThreadId) {
- SignalExecutors.BOUNDED.execute(() -> onGetThreadId.accept(getThreadId()));
- }
-
- @WorkerThread
- private long getThreadId() {
- ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
- Recipient groupRecipient = Recipient.resolved(recipientId);
-
- return threadDatabase.getThreadIdFor(groupRecipient);
- }
-
- void getIdentity(@NonNull Consumer callback) {
- SignalExecutors.BOUNDED.execute(() -> callback.accept(DatabaseFactory.getIdentityDatabase(context)
- .getIdentity(recipientId)
- .orNull()));
- }
-
- void getGroupMembership(@NonNull Consumer> onComplete) {
- SignalExecutors.BOUNDED.execute(() -> {
- GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
- List groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId);
- ArrayList groupRecipients = new ArrayList<>(groupRecords.size());
-
- for (GroupDatabase.GroupRecord groupRecord : groupRecords) {
- groupRecipients.add(groupRecord.getRecipientId());
- }
-
- onComplete.accept(groupRecipients);
- });
- }
-
- public void getRecipient(@NonNull Consumer recipientCallback) {
- SignalExecutors.BOUNDED.execute(() -> recipientCallback.accept(Recipient.resolved(recipientId)));
- }
-
- void setMuteUntil(long until) {
- SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until));
- }
-
- void refreshRecipient() {
- SignalExecutors.UNBOUNDED.execute(() -> {
- try {
- DirectoryHelper.refreshDirectoryFor(context, Recipient.resolved(recipientId), false);
- } catch (IOException e) {
- Log.w(TAG, "Failed to refresh user after adding to contacts.");
- }
- });
- }
-
- @WorkerThread
- @NonNull List getSharedGroups(@NonNull RecipientId recipientId) {
- return Stream.of(DatabaseFactory.getGroupDatabase(context)
- .getPushGroupsContainingMember(recipientId))
- .filter(g -> g.getMembers().contains(Recipient.self().getId()))
- .map(GroupDatabase.GroupRecord::getRecipientId)
- .map(Recipient::resolved)
- .sortBy(gr -> gr.getDisplayName(context))
- .toList();
- }
-
- void getActiveGroupCount(@NonNull Consumer onComplete) {
- SignalExecutors.BOUNDED.execute(() -> onComplete.accept(DatabaseFactory.getGroupDatabase(context).getActiveGroupCount()));
- }
-
- @WorkerThread
- boolean hasCustomNotifications(Recipient recipient) {
- if (recipient.getNotificationChannel() != null || !NotificationChannels.supported()) {
- return true;
- }
-
- return NotificationChannels.updateWithShortcutBasedChannel(context, recipient);
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java
deleted file mode 100644
index 97a83ab35..000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/managerecipient/ManageRecipientViewModel.java
+++ /dev/null
@@ -1,361 +0,0 @@
-package org.thoughtcrime.securesms.recipients.ui.managerecipient;
-
-import android.app.Activity;
-import android.app.NotificationChannel;
-import android.content.Context;
-import android.database.Cursor;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.WorkerThread;
-import androidx.core.util.Consumer;
-import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.Transformations;
-import androidx.lifecycle.ViewModel;
-import androidx.lifecycle.ViewModelProvider;
-
-import com.annimon.stream.Stream;
-
-import org.signal.core.util.ThreadUtil;
-import org.thoughtcrime.securesms.BlockUnblockDialog;
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.VerifyIdentityActivity;
-import org.thoughtcrime.securesms.database.IdentityDatabase;
-import org.thoughtcrime.securesms.database.MediaDatabase;
-import org.thoughtcrime.securesms.database.loaders.MediaLoader;
-import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
-import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
-import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
-import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity;
-import org.thoughtcrime.securesms.keyvalue.SignalStore;
-import org.thoughtcrime.securesms.notifications.NotificationChannels;
-import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.recipients.RecipientUtil;
-import org.thoughtcrime.securesms.util.Base64;
-import org.thoughtcrime.securesms.util.CommunicationActions;
-import org.thoughtcrime.securesms.util.DefaultValueLiveData;
-import org.thoughtcrime.securesms.util.ExpirationUtil;
-import org.thoughtcrime.securesms.util.Hex;
-import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
-
-import java.util.List;
-import java.util.UUID;
-
-public final class ManageRecipientViewModel extends ViewModel {
-
- private static final int MAX_UNCOLLAPSED_GROUPS = 6;
- private static final int SHOW_COLLAPSED_GROUPS = 5;
-
- private final Context context;
- private final ManageRecipientRepository manageRecipientRepository;
- private final LiveData title;
- private final LiveData subtitle;
- private final LiveData internalDetails;
- private final LiveData disappearingMessageTimer;
- private final MutableLiveData identity;
- private final LiveData recipient;
- private final MutableLiveData mediaCursor;
- private final LiveData muteState;
- private final LiveData hasCustomNotifications;
- private final LiveData canCollapseMemberList;
- private final DefaultValueLiveData groupListCollapseState;
- private final LiveData canBlock;
- private final LiveData canUnblock;
- private final LiveData> visibleSharedGroups;
- private final LiveData sharedGroupsCountSummary;
- private final LiveData canAddToAGroup;
-
- private ManageRecipientViewModel(@NonNull Context context, @NonNull ManageRecipientRepository manageRecipientRepository) {
- this.context = context;
- this.manageRecipientRepository = manageRecipientRepository;
- this.recipient = Recipient.live(manageRecipientRepository.getRecipientId()).getLiveData();
- this.title = Transformations.map(recipient, r -> getDisplayTitle(r, context) );
- this.subtitle = Transformations.map(recipient, r -> getDisplaySubtitle(r, context));
- this.identity = new MutableLiveData<>();
- this.mediaCursor = new MutableLiveData<>(null);
- this.groupListCollapseState = new DefaultValueLiveData<>(CollapseState.COLLAPSED);
- this.disappearingMessageTimer = Transformations.map(this.recipient, r -> ExpirationUtil.getExpirationDisplayValue(context, r.getExpireMessages()));
- this.muteState = Transformations.map(this.recipient, r -> new MuteState(r.getMuteUntil(), r.isMuted()));
- this.hasCustomNotifications = LiveDataUtil.mapAsync(this.recipient, manageRecipientRepository::hasCustomNotifications);
- this.canBlock = Transformations.map(this.recipient, r -> RecipientUtil.isBlockable(r) && !r.isBlocked());
- this.canUnblock = Transformations.map(this.recipient, Recipient::isBlocked);
- this.internalDetails = Transformations.map(this.recipient, this::populateInternalDetails);
-
- manageRecipientRepository.getThreadId(this::onThreadIdLoaded);
-
- LiveData> allSharedGroups = LiveDataUtil.mapAsync(this.recipient, r -> manageRecipientRepository.getSharedGroups(r.getId()));
-
- this.sharedGroupsCountSummary = Transformations.map(allSharedGroups, list -> {
- int size = list.size();
- return size == 0 ? context.getString(R.string.ManageRecipientActivity_no_groups_in_common)
- : context.getResources().getQuantityString(R.plurals.ManageRecipientActivity_d_groups_in_common, size, size);
- });
-
- this.canCollapseMemberList = LiveDataUtil.combineLatest(this.groupListCollapseState,
- Transformations.map(allSharedGroups, m -> m.size() > MAX_UNCOLLAPSED_GROUPS),
- (state, hasEnoughMembers) -> state != CollapseState.OPEN && hasEnoughMembers);
- this.visibleSharedGroups = Transformations.map(LiveDataUtil.combineLatest(allSharedGroups,
- this.groupListCollapseState,
- ManageRecipientViewModel::filterSharedGroupList),
- recipients -> Stream.of(recipients).map(r -> new GroupMemberEntry.FullMember(r, false)).toList());
-
-
- boolean isSelf = manageRecipientRepository.getRecipientId().equals(Recipient.self().getId());
- if (!isSelf) {
- manageRecipientRepository.getIdentity(identity::postValue);
- }
-
- MutableLiveData localGroupCount = new MutableLiveData<>(0);
-
- this.canAddToAGroup = LiveDataUtil.combineLatest(recipient,
- localGroupCount,
- (r, count) -> count > 0 && r.isRegistered() && !r.isGroup() && !r.isSelf());
-
- manageRecipientRepository.getActiveGroupCount(localGroupCount::postValue);
- }
-
- private static @NonNull String getDisplayTitle(@NonNull Recipient recipient, @NonNull Context context) {
- if (recipient.isSelf()) {
- return context.getString(R.string.note_to_self);
- } else {
- return recipient.getDisplayName(context);
- }
- }
-
- private static @NonNull String getDisplaySubtitle(@NonNull Recipient recipient, @NonNull Context context) {
- if (!recipient.isSelf() && recipient.hasAUserSetDisplayName(context)) {
- return recipient.getSmsAddress().transform(PhoneNumberFormatter::prettyPrint).or("").trim();
- } else {
- return "";
- }
- }
-
- @WorkerThread
- private void onThreadIdLoaded(long threadId) {
- mediaCursor.postValue(new MediaCursor(threadId,
- () -> new ThreadMediaLoader(context, threadId, MediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest).getCursor()));
- }
-
- LiveData getTitle() {
- return title;
- }
-
- LiveData getSubtitle() {
- return subtitle;
- }
-
- LiveData getInternalDetails() {
- return internalDetails;
- }
-
- LiveData getRecipient() {
- return recipient;
- }
-
- LiveData getCanAddToAGroup() {
- return canAddToAGroup;
- }
-
- LiveData getMediaCursor() {
- return mediaCursor;
- }
-
- LiveData getMuteState() {
- return muteState;
- }
-
- LiveData getDisappearingMessageTimer() {
- return disappearingMessageTimer;
- }
-
- LiveData hasCustomNotifications() {
- return hasCustomNotifications;
- }
-
- LiveData getCanCollapseMemberList() {
- return canCollapseMemberList;
- }
-
- LiveData getCanBlock() {
- return canBlock;
- }
-
- LiveData getCanUnblock() {
- return canUnblock;
- }
-
- void setMuteUntil(long muteUntil) {
- manageRecipientRepository.setMuteUntil(muteUntil);
- }
-
- void clearMuteUntil() {
- manageRecipientRepository.setMuteUntil(0);
- }
-
- void revealCollapsedMembers() {
- groupListCollapseState.setValue(CollapseState.OPEN);
- }
-
- void onAddToGroupButton(@NonNull Activity activity) {
- manageRecipientRepository.getGroupMembership(existingGroups -> ThreadUtil.runOnMain(() -> activity.startActivity(AddToGroupsActivity.newIntent(activity, manageRecipientRepository.getRecipientId(), existingGroups))));
- }
-
- private void withRecipient(@NonNull Consumer mainThreadRecipientCallback) {
- manageRecipientRepository.getRecipient(recipient -> ThreadUtil.runOnMain(() -> mainThreadRecipientCallback.accept(recipient)));
- }
-
- private static @NonNull List filterSharedGroupList(@NonNull List groups,
- @NonNull CollapseState collapseState)
- {
- if (collapseState == CollapseState.COLLAPSED && groups.size() > MAX_UNCOLLAPSED_GROUPS) {
- return groups.subList(0, SHOW_COLLAPSED_GROUPS);
- } else {
- return groups;
- }
- }
-
- LiveData getIdentity() {
- return identity;
- }
-
- void onBlockClicked(@NonNull FragmentActivity activity) {
- withRecipient(recipient -> BlockUnblockDialog.showBlockFor(activity, activity.getLifecycle(), recipient, () -> RecipientUtil.blockNonGroup(context, recipient)));
- }
-
- void onUnblockClicked(@NonNull FragmentActivity activity) {
- withRecipient(recipient -> BlockUnblockDialog.showUnblockFor(activity, activity.getLifecycle(), recipient, () -> RecipientUtil.unblock(context, recipient)));
- }
-
- void onViewSafetyNumberClicked(@NonNull Activity activity, @NonNull IdentityDatabase.IdentityRecord identityRecord) {
- activity.startActivity(VerifyIdentityActivity.newIntent(activity, identityRecord));
- }
-
- LiveData> getVisibleSharedGroups() {
- return visibleSharedGroups;
- }
-
- LiveData getSharedGroupsCountSummary() {
- return sharedGroupsCountSummary;
- }
-
- void onGroupClicked(@NonNull Activity activity, @NonNull Recipient recipient) {
- CommunicationActions.startConversation(activity, recipient, null);
- activity.finish();
- }
-
- void onMessage(@NonNull FragmentActivity activity) {
- withRecipient(r -> {
- CommunicationActions.startConversation(activity, r, null);
- activity.finish();
- });
- }
-
- void onSecureCall(@NonNull FragmentActivity activity) {
- withRecipient(r -> CommunicationActions.startVoiceCall(activity, r));
- }
-
- void onInsecureCall(@NonNull FragmentActivity activity) {
- withRecipient(r -> CommunicationActions.startInsecureCall(activity, r));
- }
-
- void onSecureVideoCall(@NonNull FragmentActivity activity) {
- withRecipient(r -> CommunicationActions.startVideoCall(activity, r));
- }
-
- void onAddedToContacts() {
- manageRecipientRepository.refreshRecipient();
- }
-
- void onFinishedViewingContact() {
- manageRecipientRepository.refreshRecipient();
- }
-
- private @NonNull String populateInternalDetails(@NonNull Recipient recipient) {
- if (!SignalStore.internalValues().recipientDetails()) {
- return "";
- }
-
- String profileKeyBase64 = recipient.getProfileKey() != null ? Base64.encodeBytes(recipient.getProfileKey()) : "None";
- String profileKeyHex = recipient.getProfileKey() != null ? Hex.toStringCondensed(recipient.getProfileKey()) : "None";
- return String.format("-- Profile Name --\n[%s] [%s]\n\n" +
- "-- Profile Sharing --\n%s\n\n" +
- "-- Profile Key (Base64) --\n%s\n\n" +
- "-- Profile Key (Hex) --\n%s\n\n" +
- "-- Sealed Sender Mode --\n%s\n\n" +
- "-- UUID --\n%s\n\n" +
- "-- RecipientId --\n%s",
- recipient.getProfileName().getGivenName(), recipient.getProfileName().getFamilyName(),
- recipient.isProfileSharing(),
- profileKeyBase64,
- profileKeyHex,
- recipient.getUnidentifiedAccessMode(),
- recipient.getUuid().transform(UUID::toString).or("None"),
- recipient.getId().serialize());
- }
-
- static final class MediaCursor {
- private final long threadId;
- @NonNull private final CursorFactory mediaCursorFactory;
-
- private MediaCursor(long threadId,
- @NonNull CursorFactory mediaCursorFactory)
- {
- this.threadId = threadId;
- this.mediaCursorFactory = mediaCursorFactory;
- }
-
- long getThreadId() {
- return threadId;
- }
-
- @NonNull CursorFactory getMediaCursorFactory() {
- return mediaCursorFactory;
- }
- }
-
- static final class MuteState {
- private final long mutedUntil;
- private final boolean isMuted;
-
- MuteState(long mutedUntil, boolean isMuted) {
- this.mutedUntil = mutedUntil;
- this.isMuted = isMuted;
- }
-
- long getMutedUntil() {
- return mutedUntil;
- }
-
- public boolean isMuted() {
- return isMuted;
- }
- }
-
- private enum CollapseState {
- OPEN,
- COLLAPSED
- }
-
- interface CursorFactory {
- Cursor create();
- }
-
- public static class Factory implements ViewModelProvider.Factory {
- private final Context context;
- private final RecipientId recipientId;
-
- public Factory(@NonNull RecipientId recipientId) {
- this.context = ApplicationDependencies.getApplication();
- this.recipientId = recipientId;
- }
-
- @Override
- public @NonNull T create(@NonNull Class modelClass) {
- //noinspection unchecked
- return (T) new ManageRecipientViewModel(context, new ManageRecipientRepository(context, recipientId));
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DynamicConversationSettingsTheme.java b/app/src/main/java/org/thoughtcrime/securesms/util/DynamicConversationSettingsTheme.java
new file mode 100644
index 000000000..75411f8df
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/DynamicConversationSettingsTheme.java
@@ -0,0 +1,12 @@
+package org.thoughtcrime.securesms.util;
+
+import androidx.annotation.StyleRes;
+
+import org.thoughtcrime.securesms.R;
+
+public class DynamicConversationSettingsTheme extends DynamicTheme {
+
+ protected @StyleRes int getTheme() {
+ return R.style.Signal_DayNight_ConversationSettings;
+ }
+}
diff --git a/app/src/main/res/drawable-hdpi/ic_volume_off_grey600_18dp.webp b/app/src/main/res/drawable-hdpi/ic_volume_off_grey600_18dp.webp
deleted file mode 100644
index 1df83242a..000000000
Binary files a/app/src/main/res/drawable-hdpi/ic_volume_off_grey600_18dp.webp and /dev/null differ
diff --git a/app/src/main/res/drawable-mdpi/ic_volume_off_grey600_18dp.webp b/app/src/main/res/drawable-mdpi/ic_volume_off_grey600_18dp.webp
deleted file mode 100644
index 2bc08c0dc..000000000
Binary files a/app/src/main/res/drawable-mdpi/ic_volume_off_grey600_18dp.webp and /dev/null differ
diff --git a/app/src/main/res/drawable-night/ic_bell_disabled_16.xml b/app/src/main/res/drawable-night/ic_bell_disabled_16.xml
new file mode 100644
index 000000000..c1a2b36d9
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_bell_disabled_16.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_bell_disabled_24.xml b/app/src/main/res/drawable-night/ic_bell_disabled_24.xml
new file mode 100644
index 000000000..d01a0353a
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_bell_disabled_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_safety_number_24.xml b/app/src/main/res/drawable-night/ic_safety_number_24.xml
new file mode 100644
index 000000000..16cf56ac9
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_safety_number_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_speaker_24.xml b/app/src/main/res/drawable-night/ic_speaker_24.xml
new file mode 100644
index 000000000..fe4c351f0
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_speaker_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_wallpaper_24.xml b/app/src/main/res/drawable-night/ic_wallpaper_24.xml
new file mode 100644
index 000000000..e8974b6c8
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_wallpaper_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable-v21/selectable_icon_button.xml b/app/src/main/res/drawable-v21/selectable_icon_button.xml
new file mode 100644
index 000000000..178226daf
--- /dev/null
+++ b/app/src/main/res/drawable-v21/selectable_icon_button.xml
@@ -0,0 +1,11 @@
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/selectable_recipient_bottom_sheet_icon_button.xml b/app/src/main/res/drawable-v21/selectable_recipient_bottom_sheet_icon_button.xml
new file mode 100644
index 000000000..ec3cc562c
--- /dev/null
+++ b/app/src/main/res/drawable-v21/selectable_recipient_bottom_sheet_icon_button.xml
@@ -0,0 +1,11 @@
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-xhdpi/ic_volume_off_grey600_18dp.webp b/app/src/main/res/drawable-xhdpi/ic_volume_off_grey600_18dp.webp
deleted file mode 100644
index abf5aa27f..000000000
Binary files a/app/src/main/res/drawable-xhdpi/ic_volume_off_grey600_18dp.webp and /dev/null differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_volume_off_grey600_18dp.webp b/app/src/main/res/drawable-xxhdpi/ic_volume_off_grey600_18dp.webp
deleted file mode 100644
index 9e6616785..000000000
Binary files a/app/src/main/res/drawable-xxhdpi/ic_volume_off_grey600_18dp.webp and /dev/null differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_volume_off_grey600_18dp.webp b/app/src/main/res/drawable-xxxhdpi/ic_volume_off_grey600_18dp.webp
deleted file mode 100644
index fe2cfeddf..000000000
Binary files a/app/src/main/res/drawable-xxxhdpi/ic_volume_off_grey600_18dp.webp and /dev/null differ
diff --git a/app/src/main/res/drawable/add_to_a_group.xml b/app/src/main/res/drawable/add_to_a_group.xml
new file mode 100644
index 000000000..f40030d47
--- /dev/null
+++ b/app/src/main/res/drawable/add_to_a_group.xml
@@ -0,0 +1,10 @@
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bell_disabled_16.xml b/app/src/main/res/drawable/ic_bell_disabled_16.xml
new file mode 100644
index 000000000..cbea253f7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bell_disabled_16.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_bell_disabled_24.xml b/app/src/main/res/drawable/ic_bell_disabled_24.xml
new file mode 100644
index 000000000..9a1de4f6d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bell_disabled_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_link_16.xml b/app/src/main/res/drawable/ic_link_16.xml
new file mode 100644
index 000000000..07d166879
--- /dev/null
+++ b/app/src/main/res/drawable/ic_link_16.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_safety_number_24.xml b/app/src/main/res/drawable/ic_safety_number_24.xml
new file mode 100644
index 000000000..6b26bf4d5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_safety_number_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_speaker_24.xml b/app/src/main/res/drawable/ic_speaker_24.xml
new file mode 100644
index 000000000..7fa3ed8e0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_speaker_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_wallpaper_24.xml b/app/src/main/res/drawable/ic_wallpaper_24.xml
new file mode 100644
index 000000000..ad23ef800
--- /dev/null
+++ b/app/src/main/res/drawable/ic_wallpaper_24.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/icon_button_squircle.xml b/app/src/main/res/drawable/icon_button_squircle.xml
new file mode 100644
index 000000000..b06cecbd1
--- /dev/null
+++ b/app/src/main/res/drawable/icon_button_squircle.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/recipient_bottom_sheet_icon_button_squircle.xml b/app/src/main/res/drawable/recipient_bottom_sheet_icon_button_squircle.xml
new file mode 100644
index 000000000..d5454e5fe
--- /dev/null
+++ b/app/src/main/res/drawable/recipient_bottom_sheet_icon_button_squircle.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/selectable_icon_button.xml b/app/src/main/res/drawable/selectable_icon_button.xml
new file mode 100644
index 000000000..12de62fc7
--- /dev/null
+++ b/app/src/main/res/drawable/selectable_icon_button.xml
@@ -0,0 +1,15 @@
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/selectable_recipient_bottom_sheet_icon_button.xml b/app/src/main/res/drawable/selectable_recipient_bottom_sheet_icon_button.xml
new file mode 100644
index 000000000..1d4cfed7b
--- /dev/null
+++ b/app/src/main/res/drawable/selectable_recipient_bottom_sheet_icon_button.xml
@@ -0,0 +1,15 @@
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/show_more.xml b/app/src/main/res/drawable/show_more.xml
new file mode 100644
index 000000000..1c5bb8c11
--- /dev/null
+++ b/app/src/main/res/drawable/show_more.xml
@@ -0,0 +1,10 @@
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/conversation_settings_avatar_preference_item.xml b/app/src/main/res/layout/conversation_settings_avatar_preference_item.xml
new file mode 100644
index 000000000..7b53fee6e
--- /dev/null
+++ b/app/src/main/res/layout/conversation_settings_avatar_preference_item.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/conversation_settings_bio_preference_item.xml b/app/src/main/res/layout/conversation_settings_bio_preference_item.xml
new file mode 100644
index 000000000..fa0de6a67
--- /dev/null
+++ b/app/src/main/res/layout/conversation_settings_bio_preference_item.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/conversation_settings_button_strip.xml b/app/src/main/res/layout/conversation_settings_button_strip.xml
new file mode 100644
index 000000000..4c0863976
--- /dev/null
+++ b/app/src/main/res/layout/conversation_settings_button_strip.xml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/conversation_settings_fragment.xml b/app/src/main/res/layout/conversation_settings_fragment.xml
new file mode 100644
index 000000000..2c5e62179
--- /dev/null
+++ b/app/src/main/res/layout/conversation_settings_fragment.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/conversation_settings_group_description_preference.xml b/app/src/main/res/layout/conversation_settings_group_description_preference.xml
new file mode 100644
index 000000000..d953fce83
--- /dev/null
+++ b/app/src/main/res/layout/conversation_settings_group_description_preference.xml
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/conversation_settings_internal_preference.xml b/app/src/main/res/layout/conversation_settings_internal_preference.xml
new file mode 100644
index 000000000..10fe8c61a
--- /dev/null
+++ b/app/src/main/res/layout/conversation_settings_internal_preference.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/conversation_settings_legacy_group_preference.xml b/app/src/main/res/layout/conversation_settings_legacy_group_preference.xml
new file mode 100644
index 000000000..e088cb274
--- /dev/null
+++ b/app/src/main/res/layout/conversation_settings_legacy_group_preference.xml
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/conversation_settings_shared_media.xml b/app/src/main/res/layout/conversation_settings_shared_media.xml
new file mode 100644
index 000000000..714340679
--- /dev/null
+++ b/app/src/main/res/layout/conversation_settings_shared_media.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/conversation_settings_toolbar.xml b/app/src/main/res/layout/conversation_settings_toolbar.xml
new file mode 100644
index 000000000..2570964ba
--- /dev/null
+++ b/app/src/main/res/layout/conversation_settings_toolbar.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dsl_settings_activity.xml b/app/src/main/res/layout/dsl_settings_activity.xml
index c2fe2cfdd..a2bf31576 100644
--- a/app/src/main/res/layout/dsl_settings_activity.xml
+++ b/app/src/main/res/layout/dsl_settings_activity.xml
@@ -2,4 +2,6 @@
\ No newline at end of file
+ android:layout_height="match_parent"
+ android:background="@color/signal_background_primary"
+ android:transitionName="window_content" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/group_manage_activity.xml b/app/src/main/res/layout/group_manage_activity.xml
deleted file mode 100644
index e98b23ce2..000000000
--- a/app/src/main/res/layout/group_manage_activity.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/group_manage_fragment.xml b/app/src/main/res/layout/group_manage_fragment.xml
deleted file mode 100644
index 5c43aafe5..000000000
--- a/app/src/main/res/layout/group_manage_fragment.xml
+++ /dev/null
@@ -1,733 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/group_recipient_list_item.xml b/app/src/main/res/layout/group_recipient_list_item.xml
index 4edef244a..4c3780cfa 100644
--- a/app/src/main/res/layout/group_recipient_list_item.xml
+++ b/app/src/main/res/layout/group_recipient_list_item.xml
@@ -3,16 +3,17 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="64dp"
+ android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
- android:focusable="true">
+ android:focusable="true"
+ android:minHeight="64dp"
+ android:paddingHorizontal="@dimen/dsl_settings_gutter">
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/recipient_bottom_sheet.xml b/app/src/main/res/layout/recipient_bottom_sheet.xml
index 94e63d325..f3d6e7d29 100644
--- a/app/src/main/res/layout/recipient_bottom_sheet.xml
+++ b/app/src/main/res/layout/recipient_bottom_sheet.xml
@@ -8,10 +8,10 @@
+ app:layout_constraintTop_toBottomOf="@id/rbs_full_name"
+ tools:text="๐ท๐ท๐ทHangin' on the web๐ท๐ท" />
-
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
@@ -225,16 +165,42 @@
tools:visibility="visible" />
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/recipient_manage_activity.xml b/app/src/main/res/layout/recipient_manage_activity.xml
deleted file mode 100644
index f72065ce7..000000000
--- a/app/src/main/res/layout/recipient_manage_activity.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/recipient_manage_fragment.xml b/app/src/main/res/layout/recipient_manage_fragment.xml
deleted file mode 100644
index 2e41876b5..000000000
--- a/app/src/main/res/layout/recipient_manage_fragment.xml
+++ /dev/null
@@ -1,639 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/recipient_preference_photo_rail.xml b/app/src/main/res/layout/recipient_preference_photo_rail.xml
index afca69033..2dbb71d6a 100644
--- a/app/src/main/res/layout/recipient_preference_photo_rail.xml
+++ b/app/src/main/res/layout/recipient_preference_photo_rail.xml
@@ -1,12 +1,17 @@
-
+
+ android:id="@+id/photo_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipToPadding="false"
+ android:nestedScrollingEnabled="false"
+ android:paddingStart="@dimen/dsl_settings_gutter"
+ android:paddingEnd="@dimen/dsl_settings_gutter"
+ android:scrollbars="none"
+ tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ tools:listitem="@layout/recipient_preference_photo_rail_item"
+ tools:orientation="horizontal" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/recipient_preference_photo_rail_item.xml b/app/src/main/res/layout/recipient_preference_photo_rail_item.xml
index 8ca166244..bfe4ce225 100644
--- a/app/src/main/res/layout/recipient_preference_photo_rail_item.xml
+++ b/app/src/main/res/layout/recipient_preference_photo_rail_item.xml
@@ -1,17 +1,17 @@
-
+
+ android:id="@+id/thumbnail"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"
+ app:thumbnail_radius="12dp"
+ app:transparent_overlay_color="@color/transparent_black_08" />
-
diff --git a/app/src/main/res/menu/manage_group_fragment.xml b/app/src/main/res/menu/conversation_settings.xml
similarity index 100%
rename from app/src/main/res/menu/manage_group_fragment.xml
rename to app/src/main/res/menu/conversation_settings.xml
diff --git a/app/src/main/res/menu/manage_recipient_fragment.xml b/app/src/main/res/menu/manage_recipient_fragment.xml
deleted file mode 100644
index 49f392335..000000000
--- a/app/src/main/res/menu/manage_recipient_fragment.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/navigation/conversation_settings.xml b/app/src/main/res/navigation/conversation_settings.xml
new file mode 100644
index 000000000..2c9c75ea3
--- /dev/null
+++ b/app/src/main/res/navigation/conversation_settings.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/transition/conversation_settings_exit_transition.xml b/app/src/main/res/transition/conversation_settings_exit_transition.xml
new file mode 100644
index 000000000..d5eac1bbd
--- /dev/null
+++ b/app/src/main/res/transition/conversation_settings_exit_transition.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/transition/transparent_window_wipe_transition.xml b/app/src/main/res/transition/transparent_window_wipe_transition.xml
new file mode 100644
index 000000000..5e3f17b6f
--- /dev/null
+++ b/app/src/main/res/transition/transparent_window_wipe_transition.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/dark_colors.xml b/app/src/main/res/values-night/dark_colors.xml
index a1503354a..6a1014a73 100644
--- a/app/src/main/res/values-night/dark_colors.xml
+++ b/app/src/main/res/values-night/dark_colors.xml
@@ -114,6 +114,8 @@
@color/core_grey_80
+ @color/core_grey_65
+
@color/grey_900
@color/core_grey_85
diff --git a/app/src/main/res/values-night/dark_themes.xml b/app/src/main/res/values-night/dark_themes.xml
index baee3a787..09ca4376a 100644
--- a/app/src/main/res/values-night/dark_themes.xml
+++ b/app/src/main/res/values-night/dark_themes.xml
@@ -19,4 +19,6 @@
+
+
diff --git a/app/src/main/res/values-sw360dp/dimens.xml b/app/src/main/res/values-sw360dp/dimens.xml
index 307f5a0ef..6f574be57 100644
--- a/app/src/main/res/values-sw360dp/dimens.xml
+++ b/app/src/main/res/values-sw360dp/dimens.xml
@@ -19,4 +19,8 @@
24dp
260dp
+
+ 32dp
+ 56dp
+ 16dp
\ No newline at end of file
diff --git a/app/src/main/res/values-v21/themes.xml b/app/src/main/res/values-v21/themes.xml
index 304ab5103..33fc9b4f6 100644
--- a/app/src/main/res/values-v21/themes.xml
+++ b/app/src/main/res/values-v21/themes.xml
@@ -46,4 +46,24 @@
- @color/transparent
+
+
+
+
+
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 6153352be..13fa00020 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -392,4 +392,14 @@
- @string/CustomExpireTimerSelectorView__weeks
+
+ - @string/PermissionsSettingsFragment__only_admins
+ - @string/PermissionsSettingsFragment__all_members
+
+
+
+ - @string/SoundsAndNotificationsSettingsFragment__always_notify
+ - @string/SoundsAndNotificationsSettingsFragment__do_not_notify
+
+
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index eeb9d62b5..ba3bb8496 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -53,6 +53,8 @@
+
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 89208ebde..21385c20e 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -183,4 +183,8 @@
16dp
240dp
+
+ 16dp
+ 48dp
+ 12dp
diff --git a/app/src/main/res/values/light_colors.xml b/app/src/main/res/values/light_colors.xml
index ef07aa4a1..0d2a75c51 100644
--- a/app/src/main/res/values/light_colors.xml
+++ b/app/src/main/res/values/light_colors.xml
@@ -114,6 +114,8 @@
@color/core_grey_02
+ @color/signal_background_secondary
+
@color/grey_400
@color/core_grey_02
diff --git a/app/src/main/res/values/light_themes.xml b/app/src/main/res/values/light_themes.xml
index 7edab902f..6eec2eb74 100644
--- a/app/src/main/res/values/light_themes.xml
+++ b/app/src/main/res/values/light_themes.xml
@@ -18,4 +18,6 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1d7868b88..1051a57c7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -769,32 +769,11 @@
Failed to set avatar
- Add to system contacts
- This person is in your contacts
- Disappearing messages
- Chat color
- Chat wallpaper
- Block
- Unblock
- View safety number
- Mute notifications
- Custom notifications
- Until %1$s
- Always
- Off
- On
- Add to a group
- View all groups
- See all
No groups in common
- %d group in common
- %d groups in common
- Message
- Voice call
- Insecure voice call
- Video call
- %1$s invited 1 person
@@ -3137,7 +3116,7 @@
Add to a group
Add to another group
View safety number
- Make group admin
+ Make admin
Remove as admin
Remove from group
Message
@@ -3561,6 +3540,53 @@
No results found
Unknown ringtone
+
+ Send message
+ Start video call
+ Start audio call
+ Message
+ Video
+ Audio
+ Call
+ Mute
+ Muted
+ Search
+ Disappearing messages
+ Sounds & notifications
+ Contact details
+ View safety number
+ Block
+ Block group
+ Unblock
+ Unblock group
+ Add to a group
+ See all
+ Add members
+ Permissions
+ Requests & invites
+ Group link
+ Add as a contact
+ Unmute
+ Conversation muted until %1$s
+ Conversation muted forever
+
+
+ Add members
+ Edit group info
+ All members
+ Only admins
+ Who can add new members?
+ Who can edit this group\'s info?
+
+
+ Mute notifications
+ Not muted
+ Muted until %1$s
+ Mentions
+ Always notify
+ Do not notify
+ Custom notifications
+
diff --git a/app/src/main/res/values/text_styles.xml b/app/src/main/res/values/text_styles.xml
index 8401f40e7..29542ca56 100644
--- a/app/src/main/res/values/text_styles.xml
+++ b/app/src/main/res/values/text_styles.xml
@@ -8,6 +8,11 @@
- 0
+
+
+
+
+
+
diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/GroupCapacityResultTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/GroupCapacityResultTest.kt
new file mode 100644
index 000000000..d8893884c
--- /dev/null
+++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/GroupCapacityResultTest.kt
@@ -0,0 +1,103 @@
+package org.thoughtcrime.securesms.components.settings.conversation
+
+import org.junit.Assert
+import org.junit.Test
+import org.thoughtcrime.securesms.groups.SelectionLimits
+import org.thoughtcrime.securesms.recipients.RecipientId
+
+private const val SELECTION_WARNING = 151
+private const val SELECTION_LIMIT = 1001
+
+private val SELF_ID = RecipientId.from(1L)
+
+class GroupCapacityResultTest {
+
+ private val selectionLimits = SelectionLimits(SELECTION_WARNING, SELECTION_LIMIT)
+
+ @Test
+ fun `Given an empty group, when I getRemainingCapacity, then I expect maximum capacity`() {
+ // GIVEN
+ val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(), selectionLimits)
+
+ // WHEN
+ val result = emptyGroupCapacityResult.getRemainingCapacity()
+
+ // THEN
+ Assert.assertEquals(result, SELECTION_LIMIT)
+ }
+
+ @Test
+ fun `Given an empty group, when I getSelectionLimit, then I expect SELECTION_LIMIT`() {
+ // GIVEN
+ val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(), selectionLimits)
+
+ // WHEN
+ val result = emptyGroupCapacityResult.getSelectionLimit()
+
+ // THEN
+ Assert.assertEquals(result, SELECTION_LIMIT)
+ }
+
+ @Test
+ fun `Given an empty group, when I getSelectionWarning, then I expect SELECTION_WARNING`() {
+ // GIVEN
+ val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(), selectionLimits)
+
+ // WHEN
+ val result = emptyGroupCapacityResult.getSelectionWarning()
+
+ // THEN
+ Assert.assertEquals(result, SELECTION_WARNING)
+ }
+
+ @Test
+ fun `Given a group only containing self, when I getSelectionLimit, then I expect SELECTION_LIMIT minus 1`() {
+ // GIVEN
+ val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(SELF_ID), selectionLimits)
+
+ // WHEN
+ val result = emptyGroupCapacityResult.getSelectionLimit()
+
+ // THEN
+ Assert.assertEquals(result, SELECTION_LIMIT - 1)
+ }
+
+ @Test
+ fun `Given a group only containing self, when I getSelectionWarning, then I expect SELECTION_WARNING minus 1`() {
+ // GIVEN
+ val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, listOf(SELF_ID), selectionLimits)
+
+ // WHEN
+ val result = emptyGroupCapacityResult.getSelectionWarning()
+
+ // THEN
+ Assert.assertEquals(result, SELECTION_WARNING - 1)
+ }
+
+ @Test
+ fun `Given a group containing self and others, when I getMembers, then I expect all members including self`() {
+ // GIVEN
+ val allMembers: List = (1L..10L).map { RecipientId.from(it) }
+ val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, allMembers, selectionLimits)
+
+ // WHEN
+ val result = emptyGroupCapacityResult.getMembers()
+
+ // THEN
+ Assert.assertEquals(result, allMembers)
+ }
+
+ @Test
+ fun `Given a group containing self and others, when I getMembersWithoutSelf, then I expect all members without self`() {
+ // GIVEN
+ val allMembers: List = (1L..10L).map { RecipientId.from(it) }
+ val emptyGroupCapacityResult = GroupCapacityResult(SELF_ID, allMembers, selectionLimits)
+ val expectedMembers = allMembers - SELF_ID
+
+ // WHEN
+ val result = emptyGroupCapacityResult.getMembersWithoutSelf()
+
+ // THEN
+ Assert.assertEquals(result, expectedMembers)
+ }
+}
diff --git a/build.gradle b/build.gradle
index 5ea67ceab..2eb3759a5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,6 +22,7 @@ buildscript {
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.0.0"
+ classpath 'app.cash.exhaustive:exhaustive-gradle:0.1.1'
}
}