kopia lustrzana https://github.com/ryukoposting/Signal-Android
Refactor conversation settings screens into a single fragment with new UI.
rodzic
f19033a7a2
commit
da2ee33dff
|
@ -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"]
|
||||
}
|
||||
|
||||
|
|
|
@ -308,14 +308,6 @@
|
|||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
|
@ -381,6 +373,13 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.ConversationSettings"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
|
||||
|
||||
<activity android:name=".wallpaper.ChatWallpaperActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
|
|
|
@ -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<Float> {
|
||||
|
||||
override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
|
||||
val interpolatedFraction = interpolator.getInterpolation(fraction)
|
||||
val delta = endValue - startValue
|
||||
|
||||
return delta * interpolatedFraction + startValue
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<Transformation<Bitmap>> 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<Drawable> 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<Drawable> {
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<T : PreferenceModel<T>>(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<Radio
|
|||
summaryView.text = model.listItems[model.selected]
|
||||
|
||||
itemView.setOnClickListener {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(model.title.resolve(context))
|
||||
var selection = -1
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(model.dialogTitle.resolve(context))
|
||||
.setSingleChoiceItems(model.listItems, model.selected) { dialog, which ->
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -289,7 +289,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
|||
val radioListPreference: RadioListPreference
|
||||
) : PreferenceModel<LedColorPreference>(
|
||||
title = radioListPreference.title,
|
||||
iconId = radioListPreference.iconId,
|
||||
icon = radioListPreference.icon,
|
||||
summary = radioListPreference.summary
|
||||
) {
|
||||
override fun areContentsTheSame(newItem: LedColorPreference): Boolean {
|
||||
|
|
|
@ -430,7 +430,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
|||
) : PreferenceModel<ValueClickPreference>(
|
||||
title = clickPreference.title,
|
||||
summary = clickPreference.summary,
|
||||
iconId = clickPreference.iconId,
|
||||
icon = clickPreference.icon,
|
||||
isEnabled = clickPreference.isEnabled
|
||||
) {
|
||||
override fun areContentsTheSame(newItem: ValueClickPreference): Boolean {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<RecipientId>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class AddMembersToGroup(
|
||||
val groupId: GroupId,
|
||||
val selectionWarning: Int,
|
||||
val selectionLimit: Int,
|
||||
val groupMembersWithoutSelf: List<RecipientId>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
object ShowGroupHardLimitDialog : ConversationSettingsEvent()
|
||||
|
||||
class ShowAddMembersToGroupError(
|
||||
val failureReason: GroupChangeFailureReason
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class ShowGroupInvitesSentDialog(
|
||||
val invitesSentTo: List<Recipient>
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class ShowMembersAdded(
|
||||
val membersAddedCount: Int
|
||||
) : ConversationSettingsEvent()
|
||||
|
||||
class InitiateGroupMigration(
|
||||
val recipientId: RecipientId
|
||||
) : ConversationSettingsEvent()
|
||||
}
|
|
@ -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<ConversationSettingsViewModel>(
|
||||
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<RecipientId> = 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()
|
||||
}
|
||||
}
|
|
@ -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<Recipient>) -> 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<RecipientId>) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val groupDatabase = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId)
|
||||
val groupRecipients = ArrayList<RecipientId>(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<RecipientId> = decryptedGroup.pendingMembersList
|
||||
.map(DecryptedPendingMember::getUuid)
|
||||
.map(GroupProtoUtil::uuidByteStringToRecipientId)
|
||||
|
||||
val members = mutableListOf<RecipientId>()
|
||||
|
||||
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<RecipientId>, 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<String> {
|
||||
return liveGroup.getMembershipCountDescription(context.resources)
|
||||
}
|
||||
|
||||
fun getExternalPossiblyMigratedGroupRecipientId(groupId: GroupId, consumer: (RecipientId) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(Recipient.externalPossiblyMigratedGroup(context, groupId).id)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Recipient> = listOf(),
|
||||
val groupsInCommon: List<Recipient> = 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<GroupMemberEntry.FullMember> = listOf(),
|
||||
val members: List<GroupMemberEntry.FullMember> = 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
|
||||
}
|
|
@ -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<Cursor>()
|
||||
|
||||
@Volatile
|
||||
private var cleared = false
|
||||
|
||||
protected val store = Store(
|
||||
ConversationSettingsState(
|
||||
specificSettingsState = specificSettingsState
|
||||
)
|
||||
)
|
||||
protected val internalEvents = SingleLiveEvent<ConversationSettingsEvent>()
|
||||
|
||||
private val sharedMediaUpdateTrigger = MutableLiveData(Unit)
|
||||
|
||||
val state: LiveData<ConversationSettingsState> = store.stateLiveData
|
||||
val events: LiveData<ConversationSettingsEvent> = internalEvents
|
||||
|
||||
init {
|
||||
val threadId: LiveData<Long> = Transformations.distinctUntilChanged(Transformations.map(state) { it.threadId })
|
||||
val updater: LiveData<Long> = LiveDataUtil.combineLatest(threadId, sharedMediaUpdateTrigger) { tId, _ -> tId }
|
||||
|
||||
val sharedMedia: LiveData<Cursor> = 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<RecipientId>, 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<RecipientId>, 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<Boolean> = LiveDataUtil.mapAsync(liveGroup.groupRecipient) { r -> repository.isMessageRequestAccepted(r) }
|
||||
val descriptionState: LiveData<DescriptionState> = 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<RecipientId>, 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 <T : ViewModel?> create(modelClass: Class<T>): 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
|
||||
)
|
||||
}
|
|
@ -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<Recipient>
|
||||
) : GroupAddMembersResult()
|
||||
|
||||
class Failure(
|
||||
val reason: GroupChangeFailureReason
|
||||
) : GroupAddMembersResult()
|
||||
}
|
|
@ -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<RecipientId>,
|
||||
private val selectionLimits: SelectionLimits
|
||||
) {
|
||||
fun getMembers(): List<RecipientId?> {
|
||||
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<RecipientId> {
|
||||
val recipientIds = ArrayList<RecipientId>(members.size)
|
||||
for (recipientId in members) {
|
||||
if (recipientId != selfId) {
|
||||
recipientIds.add(recipientId)
|
||||
}
|
||||
}
|
||||
return recipientIds
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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<String> 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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<PermissionsSettingsEvents>()
|
||||
|
||||
val state: LiveData<PermissionsSettingsState> = store.stateLiveData
|
||||
val events: LiveData<PermissionsSettingsEvents> = 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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(PermissionsSettingsViewModel(groupId, repository)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Model>() {
|
||||
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<Model>(itemView) {
|
||||
private val avatar: AvatarImageView = itemView.findViewById<AvatarImageView>(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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<T : BioTextPreferenceModel<T>> : PreferenceModel<T>() {
|
||||
abstract fun getHeadlineText(context: Context): String
|
||||
abstract fun getSubhead1Text(): String?
|
||||
abstract fun getSubhead2Text(): String?
|
||||
}
|
||||
|
||||
class RecipientModel(
|
||||
private val recipient: Recipient,
|
||||
) : BioTextPreferenceModel<RecipientModel>() {
|
||||
|
||||
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<GroupModel>() {
|
||||
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<T : BioTextPreferenceModel<T>>(itemView: View) : MappingViewHolder<T>(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<RecipientModel>(itemView)
|
||||
private class GroupViewHolder(itemView: View) : BioTextViewHolder<GroupModel>(itemView)
|
||||
}
|
|
@ -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<Model>() {
|
||||
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<Model>(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,
|
||||
)
|
||||
}
|
|
@ -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<Model>() {
|
||||
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<Model>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Model>() {
|
||||
|
||||
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<Model>(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() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Model>()
|
||||
|
||||
private class ViewHolder(itemView: View) : PreferenceViewHolder<Model>(itemView) {
|
||||
override fun bind(model: Model) {
|
||||
super.bind(model)
|
||||
itemView.setOnClickListener { model.onClick() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return state == newItem.state
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(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
|
||||
}
|
||||
}
|
|
@ -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<Model>() {
|
||||
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<Model>(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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.mediaCursor == mediaCursor
|
||||
}
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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<SoundsAndNotificationsSettingsState> = 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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(SoundsAndNotificationsSettingsViewModel(recipientId, repository)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String>,
|
||||
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<T : PreferenceModel<T>>(
|
||||
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<T> {
|
||||
override fun areItemsTheSame(newItem: T): Boolean {
|
||||
return when {
|
||||
|
@ -131,7 +139,7 @@ abstract class PreferenceModel<T : PreferenceModel<T>>(
|
|||
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<DividerPreference>() {
|
|||
|
||||
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<String>,
|
||||
val selected: Int,
|
||||
val onSelected: (Int) -> Unit
|
||||
) : PreferenceModel<RadioListPreference>(title = title, iconId = iconId, isEnabled = isEnabled) {
|
||||
val onSelected: (Int) -> Unit,
|
||||
val confirmAction: Boolean = false
|
||||
) : PreferenceModel<RadioListPreference>() {
|
||||
|
||||
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<SwitchPreference>(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled) {
|
||||
) : PreferenceModel<SwitchPreference>() {
|
||||
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<ClickPreference>(title = title, summary = summary, iconId = iconId, isEnabled = isEnabled)
|
||||
) : PreferenceModel<ClickPreference>()
|
||||
|
||||
class ExternalLinkPreference(
|
||||
override val title: DSLSettingsText,
|
||||
@DrawableRes override val iconId: Int,
|
||||
override val icon: DSLSettingsIcon?,
|
||||
@StringRes val linkId: Int
|
||||
) : PreferenceModel<ExternalLinkPreference>(title = title, iconId = iconId)
|
||||
) : PreferenceModel<ExternalLinkPreference>()
|
||||
|
||||
class SectionHeaderPreference(override val title: DSLSettingsText) : PreferenceModel<SectionHeaderPreference>(title = title)
|
||||
class SectionHeaderPreference(override val title: DSLSettingsText) : PreferenceModel<SectionHeaderPreference>()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<RecipientId> 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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<RecipientId> selected = data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS);
|
||||
SimpleProgressDialog.DismissibleDialog progress = SimpleProgressDialog.showDelayed(requireContext());
|
||||
|
||||
viewModel.onAddMembers(selected, new AsynchronousCallback.MainThread<ManageGroupViewModel.AddMembersResult, GroupChangeFailureReason>() {
|
||||
@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());
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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<GroupStateResult> onGroupStateLoaded) {
|
||||
SignalExecutors.BOUNDED.execute(() -> onGroupStateLoaded.accept(getGroupState(groupId)));
|
||||
}
|
||||
|
||||
void getGroupCapacity(@NonNull GroupId groupId, @NonNull Consumer<GroupCapacityResult> onGroupCapacityLoaded) {
|
||||
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
|
||||
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get();
|
||||
if (groupRecord.isV2Group()) {
|
||||
DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
|
||||
List<RecipientId> pendingMembers = Stream.of(decryptedGroup.getPendingMembersList())
|
||||
.map(member -> GroupProtoUtil.uuidByteStringToRecipientId(member.getUuid()))
|
||||
.toList();
|
||||
List<RecipientId> 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<Recipient> 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<RecipientId> selected,
|
||||
@NonNull AsynchronousCallback.WorkerThread<ManageGroupViewModel.AddMembersResult, GroupChangeFailureReason> 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<RecipientId> members;
|
||||
private final SelectionLimits selectionLimits;
|
||||
|
||||
GroupCapacityResult(@NonNull List<RecipientId> members, @NonNull SelectionLimits selectionLimits) {
|
||||
this.members = members;
|
||||
this.selectionLimits = selectionLimits;
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientId> 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<RecipientId> getMembersWithoutSelf() {
|
||||
ArrayList<RecipientId> recipientIds = new ArrayList<>(members.size());
|
||||
RecipientId selfId = Recipient.self().getId();
|
||||
|
||||
for (RecipientId recipientId : members) {
|
||||
if (!recipientId.equals(selfId)) {
|
||||
recipientIds.add(recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
return recipientIds;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String> title;
|
||||
private final Store<Description> descriptionStore;
|
||||
private final LiveData<Description> description;
|
||||
private final LiveData<Boolean> isAdmin;
|
||||
private final LiveData<Boolean> canEditGroupAttributes;
|
||||
private final LiveData<Boolean> canAddMembers;
|
||||
private final LiveData<List<GroupMemberEntry.FullMember>> members;
|
||||
private final LiveData<Integer> pendingMemberCount;
|
||||
private final LiveData<Integer> pendingAndRequestingCount;
|
||||
private final LiveData<String> disappearingMessageTimer;
|
||||
private final LiveData<String> memberCountSummary;
|
||||
private final LiveData<String> fullMemberCountSummary;
|
||||
private final LiveData<GroupAccessControl> editMembershipRights;
|
||||
private final LiveData<GroupAccessControl> editGroupAttributesRights;
|
||||
private final LiveData<Recipient> groupRecipient;
|
||||
private final MutableLiveData<GroupViewState> groupViewState = new MutableLiveData<>(null);
|
||||
private final LiveData<MuteState> muteState;
|
||||
private final LiveData<Boolean> hasCustomNotifications;
|
||||
private final LiveData<Boolean> canCollapseMemberList;
|
||||
private final DefaultValueLiveData<CollapseState> memberListCollapseState = new DefaultValueLiveData<>(CollapseState.COLLAPSED);
|
||||
private final LiveData<Boolean> canLeaveGroup;
|
||||
private final LiveData<Boolean> canBlockGroup;
|
||||
private final LiveData<Boolean> canUnblockGroup;
|
||||
private final LiveData<Boolean> showLegacyIndicator;
|
||||
private final LiveData<String> mentionSetting;
|
||||
private final LiveData<Boolean> groupLinkOn;
|
||||
private final LiveData<GroupInfoMessage> 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<List<GroupMemberEntry.FullMember>> getMembers() {
|
||||
return members;
|
||||
}
|
||||
|
||||
LiveData<Integer> getPendingMemberCount() {
|
||||
return pendingMemberCount;
|
||||
}
|
||||
|
||||
LiveData<Integer> getPendingAndRequestingCount() {
|
||||
return pendingAndRequestingCount;
|
||||
}
|
||||
|
||||
LiveData<String> getMemberCountSummary() {
|
||||
return memberCountSummary;
|
||||
}
|
||||
|
||||
LiveData<String> getFullMemberCountSummary() {
|
||||
return fullMemberCountSummary;
|
||||
}
|
||||
|
||||
LiveData<Recipient> getGroupRecipient() {
|
||||
return groupRecipient;
|
||||
}
|
||||
|
||||
LiveData<GroupViewState> getGroupViewState() {
|
||||
return groupViewState;
|
||||
}
|
||||
|
||||
LiveData<String> getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
LiveData<Description> getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
LiveData<MuteState> getMuteState() {
|
||||
return muteState;
|
||||
}
|
||||
|
||||
LiveData<GroupAccessControl> getMembershipRights() {
|
||||
return editMembershipRights;
|
||||
}
|
||||
|
||||
LiveData<GroupAccessControl> getEditGroupAttributesRights() {
|
||||
return editGroupAttributesRights;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getIsAdmin() {
|
||||
return isAdmin;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanEditGroupAttributes() {
|
||||
return canEditGroupAttributes;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanAddMembers() {
|
||||
return canAddMembers;
|
||||
}
|
||||
|
||||
LiveData<String> getDisappearingMessageTimer() {
|
||||
return disappearingMessageTimer;
|
||||
}
|
||||
|
||||
LiveData<Boolean> hasCustomNotifications() {
|
||||
return hasCustomNotifications;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanCollapseMemberList() {
|
||||
return canCollapseMemberList;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanBlockGroup() {
|
||||
return canBlockGroup;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanUnblockGroup() {
|
||||
return canUnblockGroup;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanLeaveGroup() {
|
||||
return canLeaveGroup;
|
||||
}
|
||||
|
||||
LiveData<String> getMentionSetting() {
|
||||
return mentionSetting;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getGroupLinkOn() {
|
||||
return groupLinkOn;
|
||||
}
|
||||
|
||||
LiveData<GroupInfoMessage> 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<RecipientId> selected,
|
||||
@NonNull AsynchronousCallback.MainThread<AddMembersResult, GroupChangeFailureReason> 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<GroupMemberEntry.FullMember> filterMemberList(@NonNull List<GroupMemberEntry.FullMember> 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<Recipient> newInvitedMembers;
|
||||
|
||||
AddMembersResult(int numberOfMembersAdded, @NonNull List<Recipient> newInvitedMembers) {
|
||||
this.numberOfMembersAdded = numberOfMembersAdded;
|
||||
this.newInvitedMembers = newInvitedMembers;
|
||||
}
|
||||
|
||||
int getNumberOfMembersAdded() {
|
||||
return numberOfMembersAdded;
|
||||
}
|
||||
|
||||
List<Recipient> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new ManageGroupViewModel(context, groupId, new ManageGroupRepository(context.getApplicationContext()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<View> {
|
||||
|
||||
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<View> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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<Long> 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<IdentityDatabase.IdentityRecord> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> callback.accept(DatabaseFactory.getIdentityDatabase(context)
|
||||
.getIdentity(recipientId)
|
||||
.orNull()));
|
||||
}
|
||||
|
||||
void getGroupMembership(@NonNull Consumer<List<RecipientId>> onComplete) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
||||
List<GroupDatabase.GroupRecord> groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId);
|
||||
ArrayList<RecipientId> groupRecipients = new ArrayList<>(groupRecords.size());
|
||||
|
||||
for (GroupDatabase.GroupRecord groupRecord : groupRecords) {
|
||||
groupRecipients.add(groupRecord.getRecipientId());
|
||||
}
|
||||
|
||||
onComplete.accept(groupRecipients);
|
||||
});
|
||||
}
|
||||
|
||||
public void getRecipient(@NonNull Consumer<Recipient> 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<Recipient> 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<Integer> 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);
|
||||
}
|
||||
}
|
|
@ -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<String> title;
|
||||
private final LiveData<String> subtitle;
|
||||
private final LiveData<String> internalDetails;
|
||||
private final LiveData<String> disappearingMessageTimer;
|
||||
private final MutableLiveData<IdentityDatabase.IdentityRecord> identity;
|
||||
private final LiveData<Recipient> recipient;
|
||||
private final MutableLiveData<MediaCursor> mediaCursor;
|
||||
private final LiveData<MuteState> muteState;
|
||||
private final LiveData<Boolean> hasCustomNotifications;
|
||||
private final LiveData<Boolean> canCollapseMemberList;
|
||||
private final DefaultValueLiveData<CollapseState> groupListCollapseState;
|
||||
private final LiveData<Boolean> canBlock;
|
||||
private final LiveData<Boolean> canUnblock;
|
||||
private final LiveData<List<GroupMemberEntry.FullMember>> visibleSharedGroups;
|
||||
private final LiveData<String> sharedGroupsCountSummary;
|
||||
private final LiveData<Boolean> 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<List<Recipient>> 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<Integer> 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<String> getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
LiveData<String> getSubtitle() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
LiveData<String> getInternalDetails() {
|
||||
return internalDetails;
|
||||
}
|
||||
|
||||
LiveData<Recipient> getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanAddToAGroup() {
|
||||
return canAddToAGroup;
|
||||
}
|
||||
|
||||
LiveData<MediaCursor> getMediaCursor() {
|
||||
return mediaCursor;
|
||||
}
|
||||
|
||||
LiveData<MuteState> getMuteState() {
|
||||
return muteState;
|
||||
}
|
||||
|
||||
LiveData<String> getDisappearingMessageTimer() {
|
||||
return disappearingMessageTimer;
|
||||
}
|
||||
|
||||
LiveData<Boolean> hasCustomNotifications() {
|
||||
return hasCustomNotifications;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanCollapseMemberList() {
|
||||
return canCollapseMemberList;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getCanBlock() {
|
||||
return canBlock;
|
||||
}
|
||||
|
||||
LiveData<Boolean> 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<Recipient> mainThreadRecipientCallback) {
|
||||
manageRecipientRepository.getRecipient(recipient -> ThreadUtil.runOnMain(() -> mainThreadRecipientCallback.accept(recipient)));
|
||||
}
|
||||
|
||||
private static @NonNull List<Recipient> filterSharedGroupList(@NonNull List<Recipient> groups,
|
||||
@NonNull CollapseState collapseState)
|
||||
{
|
||||
if (collapseState == CollapseState.COLLAPSED && groups.size() > MAX_UNCOLLAPSED_GROUPS) {
|
||||
return groups.subList(0, SHOW_COLLAPSED_GROUPS);
|
||||
} else {
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
LiveData<IdentityDatabase.IdentityRecord> 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<List<GroupMemberEntry.FullMember>> getVisibleSharedGroups() {
|
||||
return visibleSharedGroups;
|
||||
}
|
||||
|
||||
LiveData<String> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new ManageRecipientViewModel(context, new ManageRecipientRepository(context, recipientId));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 314 B |
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 224 B |
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.163,14L9.837,14a1.875,1.875 0,0 1,-3.674 0ZM14.191,2.107 L11.985,3.841a4.225,4.225 0,0 0,-8.123 0.5L2.855,9.167a5.83,5.83 0,0 1,-1.785 3.25l-0.879,0.69 0.618,0.786 14,-11ZM14.54,11.035a2.846,2.846 0,0 1,-1.395 -1.868l-0.662,-3.176L3.562,13L14,13a1.056,1.056 0,0 0,0.54 -1.965Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M1.9,20.056 L0.943,18.9l2.122,-1.769A6.1,6.1 0,0 0,4.711 13.57L6.138,6.727A5.981,5.981 0,0 1,17.314 5.262L20.1,2.944 21.057,4.1ZM12,22.5a2.5,2.5 0,0 0,2.45 -2L9.55,20.5A2.5,2.5 0,0 0,12 22.5ZM21.264,16.216a4.033,4.033 0,0 1,-1.975 -2.646L18.215,8.419 5.517,19L20.5,19a1.5,1.5 0,0 0,0.764 -2.784Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,15.631 L7.435,11.066l1.13,-1.132L12,13.369l8.9,-8.9C14.689,4.127 12,1 12,1S9,4.5 2,4.5C2,15.5 7.6,20.4 12,23c4.142,-2.5 9.568,-7.16 9.962,-17.33Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/core_grey_75"
|
||||
android:pathData="M19.78,4.22 L18.72,5.28a9.52,9.52 0,0 1,0 13.44l1.06,1.06A11,11 0,0 0,19.78 4.22ZM19,12A7,7 0,0 0,17 7.05L15.89,8.11a5.5,5.5 0,0 1,0 7.78L17,17A7,7 0,0 0,19 12ZM14,3.14V20.86a0.5,0.5 0,0 1,-0.84 0.37L8,16.5H4a2,2 0,0 1,-2 -2v-5a2,2 0,0 1,2 -2H8l5.16,-4.73A0.5,0.5 0,0 1,14 3.14Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17,1L7,1A3,3 0,0 0,4 4L4,20a3,3 0,0 0,3 3L17,23a3,3 0,0 0,3 -3L20,4A3,3 0,0 0,17 1ZM15.6,8.94l-2.5,2.5L9.6,7.94 5.5,12L5.5,6h13v5.84ZM7,2.5L17,2.5A1.5,1.5 0,0 1,18.5 4v0.5L5.5,4.5L5.5,4A1.5,1.5 0,0 1,7 2.5ZM17,21.5L7,21.5A1.5,1.5 0,0 1,5.5 20v-0.5h13L18.5,20A1.5,1.5 0,0 1,17 21.5Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?colorControlHighlight">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#000000" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="@drawable/icon_button_squircle" />
|
||||
</ripple>
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?colorControlHighlight">
|
||||
<item android:id="@android:id/mask">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#000000" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="@drawable/recipient_bottom_sheet_icon_button_squircle" />
|
||||
</ripple>
|
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 364 B |
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 512 B |
Plik binarny nie jest wyświetlany.
Przed Szerokość: | Wysokość: | Rozmiar: 644 B |
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/signal_background_secondary" />
|
||||
<padding android:right="8dp" android:bottom="8dp" android:left="8dp" android:top="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="@drawable/ic_plus_24" />
|
||||
</layer-list>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.163,14L9.837,14a1.875,1.875 0,0 1,-3.674 0ZM8,2a3.233,3.233 0,0 1,3.041 2.171l0.113,0.322L3.5,10.507a8.079,8.079 0,0 0,0.335 -1.136L4.84,4.548A3.25,3.25 0,0 1,8 2M8,1A4.236,4.236 0,0 0,3.862 4.337L2.855,9.167a5.83,5.83 0,0 1,-1.785 3.25l-0.879,0.69 0.618,0.786 14,-11 -0.618,-0.786L11.985,3.841A4.225,4.225 0,0 0,8 1ZM14.54,11.035a2.846,2.846 0,0 1,-1.395 -1.868l-0.662,-3.176h0l-0.878,0.689 0.564,2.7a3.954,3.954 0,0 0,1.89 2.558A0.059,0.059 0,0 1,14 12L4.834,12L3.562,13L14,13a1.056,1.056 0,0 0,0.54 -1.965Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21.057,4.1 L20.1,2.944 17.314,5.262A5.981,5.981 0,0 0,6.138 6.727L4.711,13.57a7.284,7.284 0,0 1,-1.749 3.651L0.943,18.9 1.9,20.056ZM6.18,13.876 L7.6,7.044a4.5,4.5 0,0 1,8.533 -0.8L5.81,14.849A3.552,3.552 0,0 0,6.18 13.876ZM22,17.5A1.5,1.5 0,0 1,20.5 19L5.517,19l1.8,-1.5 13.175,0a5.511,5.511 0,0 1,-2.664 -3.606l-0.915,-4.387 1.306,-1.088 1.074,5.151a4.033,4.033 0,0 0,1.975 2.646A1.486,1.486 0,0 1,22 17.5ZM9.55,20.5h4.9a2.5,2.5 0,0 1,-4.9 0Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15,7.5A2.5,2.5 0,0 1,12.5 10h-4A2.5,2.5 0,0 1,6 7.5,2.577 2.577,0 0,1 6.05,7H7.092A1.483,1.483 0,0 0,7 7.5,1.5 1.5,0 0,0 8.5,9h4a1.5,1.5 0,0 0,0 -3H11L10,5h2.5A2.5,2.5 0,0 1,15 7.5ZM3.5,10H6L5,9H3.5a1.5,1.5 0,0 1,0 -3h4A1.5,1.5 0,0 1,9 7.5a1.483,1.483 0,0 1,-0.092 0.5H9.95A2.577,2.577 0,0 0,10 7.5,2.5 2.5,0 0,0 7.5,5h-4a2.5,2.5 0,0 0,0 5Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21.793,7.888A19.35,19.35 0,0 1,12 23C7.6,20.4 2,15.5 2,4.5 9,4.5 12,1 12,1s2.156,2.5 7.05,3.268L17.766,5.553A14.7,14.7 0,0 1,12 3,15.653 15.653,0 0,1 3.534,5.946c0.431,8.846 4.8,12.96 8.458,15.29A17.39,17.39 0,0 0,19.983 9.7ZM22.53,5.03 L21.47,3.97 12,13.439 8.53,9.97 7.47,11.03 12,15.561Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.78,19.78l-1.06,-1.06a9.52,9.52 0,0 0,0 -13.44l1.06,-1.06A11,11 0,0 1,19.78 19.78ZM19,12A7,7 0,0 0,17 7.05L15.89,8.11a5.5,5.5 0,0 1,0 7.78L17,17A7,7 0,0 0,19 12ZM14,3.14V20.86a0.5,0.5 0,0 1,-0.84 0.37L8,16.5H4a2,2 0,0 1,-2 -2v-5a2,2 0,0 1,2 -2H8l5.16,-4.73a0.5,0.5 0,0 1,0.84 0.37ZM12.5,16.5v-9l0.25,-2.75L11.34,6.47 8.58,9H4a0.5,0.5 0,0 0,-0.5 0.5v5A0.5,0.5 0,0 0,4 15H8.58l2.76,2.53 1.41,1.72Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,29 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17,2.5A1.5,1.5 0,0 1,18.5 4V20A1.5,1.5 0,0 1,17 21.5H7A1.5,1.5 0,0 1,5.5 20V4A1.5,1.5 0,0 1,7 2.5H17M17,1H7A3,3 0,0 0,4 4V20a3,3 0,0 0,3 3H17a3,3 0,0 0,3 -3V4a3,3 0,0 0,-3 -3Z"/>
|
||||
<path
|
||||
android:pathData="M4.8,13.8l4.8,-4.8l5.5,5.5"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M13.1,12.5l2.5,-2.5l3.7,3.7"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M5,5.25L19,5.25"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M5,18.75L19,18.75"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/signal_background_secondary" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/recipient_bottom_sheet_button_strip_background_color" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<layer-list>
|
||||
<item android:drawable="@drawable/icon_button_squircle" />
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/signal_inverse_transparent_10" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
<item android:drawable="@drawable/icon_button_squircle" />
|
||||
</selector>
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true">
|
||||
<layer-list>
|
||||
<item android:drawable="@drawable/recipient_bottom_sheet_icon_button_squircle" />
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/signal_inverse_transparent_10" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
<item android:drawable="@drawable/recipient_bottom_sheet_icon_button_squircle" />
|
||||
</selector>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/signal_background_secondary" />
|
||||
<padding android:right="8dp" android:bottom="8dp" android:left="8dp" android:top="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="@drawable/ic_chevron_down_20" />
|
||||
</layer-list>
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:id="@+id/bio_preference_avatar"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="40dp" />
|
||||
</FrameLayout>
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/bio_preference_headline"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/Signal.Text.Headline.Medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Miles Morales" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/bio_preference_subhead_1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="4sp"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bio_preference_headline"
|
||||
tools:text=":-) Just hanging around." />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/bio_preference_subhead_2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingExtra="4sp"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bio_preference_subhead_1"
|
||||
tools:text="+1 555-876-5309" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,137 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/button_strip"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/message"
|
||||
android:layout_width="@dimen/conversation_settings_button_strip_button_size"
|
||||
android:layout_height="@dimen/conversation_settings_button_strip_button_size"
|
||||
android:layout_marginEnd="@dimen/conversation_settings_button_strip_spacing"
|
||||
android:background="@drawable/selectable_icon_button"
|
||||
android:contentDescription="@string/ConversationSettingsFragment__message"
|
||||
android:padding="@dimen/conversation_settings_button_strip_button_padding"
|
||||
app:layout_constraintEnd_toStartOf="@id/start_video"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_message_24"
|
||||
app:tint="@color/signal_icon_tint_primary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/ConversationSettingsFragment__message"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
app:layout_constraintEnd_toEndOf="@id/message"
|
||||
app:layout_constraintStart_toStartOf="@id/message"
|
||||
app:layout_constraintTop_toBottomOf="@id/message" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/start_video"
|
||||
android:layout_width="@dimen/conversation_settings_button_strip_button_size"
|
||||
android:layout_height="@dimen/conversation_settings_button_strip_button_size"
|
||||
android:layout_marginEnd="@dimen/conversation_settings_button_strip_spacing"
|
||||
android:background="@drawable/selectable_icon_button"
|
||||
android:contentDescription="@string/ConversationSettingsFragment__start_video_call"
|
||||
android:padding="@dimen/conversation_settings_button_strip_button_padding"
|
||||
app:layout_constraintEnd_toStartOf="@id/start_audio"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toEndOf="@id/message"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_video_call_24"
|
||||
app:tint="@color/signal_icon_tint_primary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/start_video_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/ConversationSettingsFragment__video"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
app:layout_constraintEnd_toEndOf="@id/start_video"
|
||||
app:layout_constraintStart_toStartOf="@id/start_video"
|
||||
app:layout_constraintTop_toBottomOf="@id/start_video" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/start_audio"
|
||||
android:layout_width="@dimen/conversation_settings_button_strip_button_size"
|
||||
android:layout_height="@dimen/conversation_settings_button_strip_button_size"
|
||||
android:layout_marginEnd="@dimen/conversation_settings_button_strip_spacing"
|
||||
android:background="@drawable/selectable_icon_button"
|
||||
android:contentDescription="@string/ConversationSettingsFragment__start_audio_call"
|
||||
android:padding="@dimen/conversation_settings_button_strip_button_padding"
|
||||
app:layout_constraintEnd_toStartOf="@id/mute"
|
||||
app:layout_constraintStart_toEndOf="@id/start_video"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginEnd="0dp"
|
||||
app:srcCompat="@drawable/ic_phone_right_24"
|
||||
app:tint="@color/signal_icon_tint_primary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/start_audio_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/ConversationSettingsFragment__audio"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
app:layout_constraintEnd_toEndOf="@id/start_audio"
|
||||
app:layout_constraintStart_toStartOf="@id/start_audio"
|
||||
app:layout_constraintTop_toBottomOf="@id/start_audio" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/mute"
|
||||
android:layout_width="@dimen/conversation_settings_button_strip_button_size"
|
||||
android:layout_height="@dimen/conversation_settings_button_strip_button_size"
|
||||
android:layout_marginEnd="@dimen/conversation_settings_button_strip_spacing"
|
||||
android:background="@drawable/selectable_icon_button"
|
||||
android:contentDescription="@string/ConversationSettingsFragment__mute"
|
||||
android:padding="@dimen/conversation_settings_button_strip_button_padding"
|
||||
app:layout_constraintEnd_toStartOf="@id/search"
|
||||
app:layout_constraintStart_toEndOf="@id/start_audio"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_bell_24"
|
||||
app:tint="@color/signal_icon_tint_primary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mute_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/ConversationSettingsFragment__mute"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
app:layout_constraintEnd_toEndOf="@id/mute"
|
||||
app:layout_constraintStart_toStartOf="@id/mute"
|
||||
app:layout_constraintTop_toBottomOf="@id/mute" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/search"
|
||||
android:layout_width="@dimen/conversation_settings_button_strip_button_size"
|
||||
android:layout_height="@dimen/conversation_settings_button_strip_button_size"
|
||||
android:background="@drawable/selectable_icon_button"
|
||||
android:contentDescription="@string/ConversationSettingsFragment__search"
|
||||
android:padding="@dimen/conversation_settings_button_strip_button_padding"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/mute"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_search_24"
|
||||
app:tint="@color/signal_icon_tint_primary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/search_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/ConversationSettingsFragment__search"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
app:layout_constraintEnd_toEndOf="@id/search"
|
||||
app:layout_constraintStart_toStartOf="@id/search"
|
||||
app:layout_constraintTop_toBottomOf="@id/search" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/toolbar" />
|
||||
|
||||
<include layout="@layout/conversation_settings_toolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/manage_group_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_horizontal"
|
||||
android:maxLines="2"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
tools:text="Group to plot the capture of Doc Oct" />
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/internal_preference_body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/internal_disable_profile_sharing"
|
||||
style="@style/Signal.Widget.Button.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:text="@string/preferences__internal_disable_profile_sharing" />
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.util.views.LearnMoreTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/manage_group_info_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:background="@drawable/round_background"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Subtitle"
|
||||
android:textColor="@color/signal_text_secondary" />
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.ThreadPhotoRailView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/rail_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp" />
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<View
|
||||
android:id="@+id/toolbar_background"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:alpha="0"
|
||||
android:background="@color/signal_background_primary"
|
||||
app:layout_constraintBottom_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/toolbar" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:theme="?attr/settingsToolbarStyle"
|
||||
app:contentInsetStartWithNavigation="60dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navigationIcon="@drawable/ic_arrow_left_24">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/toolbar_avatar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:alpha="0"
|
||||
android:translationY="56dp"
|
||||
app:fallbackImageSize="small" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/toolbar_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/Signal.Text.Title"
|
||||
android:translationY="56dp"
|
||||
tools:text="Miles Morales" />
|
||||
|
||||
</org.thoughtcrime.securesms.util.views.DarkOverflowToolbar>
|
||||
|
||||
<View
|
||||
android:id="@+id/toolbar_shadow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="5dp"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/toolbar_shadow"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
||||
</merge>
|
|
@ -2,4 +2,6 @@
|
|||
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/signal_background_primary"
|
||||
android:transitionName="window_content" />
|
|
@ -1,15 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".groups.ui.managegroup.ManageGroupActivity">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,733 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".groups.ui.managegroup.ManageGroupFragment">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
app:contentScrim="?android:attr/windowBackground"
|
||||
app:expandedTitleGravity="center_horizontal"
|
||||
app:expandedTitleMarginTop="156dp"
|
||||
app:expandedTitleTextAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed"
|
||||
app:scrimAnimationDuration="200"
|
||||
app:scrimVisibleHeightTrigger="156dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="128dp"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="14dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/member_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
tools:text="12 members (4 invited)" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/manage_group_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:visibility="gone"
|
||||
tools:text="Group to plot the capture of Doc Oct"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.LearnMoreTextView
|
||||
android:id="@+id/manage_group_info_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/round_background"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Subtitle"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:visibility="gone"
|
||||
tools:text="@string/ManageGroupActivity_legacy_group_learn_more"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:contentInsetStartWithNavigation="0dp"
|
||||
app:layout_collapseMode="pin"
|
||||
app:navigationIcon="@drawable/ic_arrow_left_24">
|
||||
|
||||
<Space
|
||||
android:id="@+id/avatar_target"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:layout_marginEnd="10dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/name_target"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold" />
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/group_avatar"
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="128dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:elevation="8dp"
|
||||
android:transitionName="avatar"
|
||||
app:layout_behavior="org.thoughtcrime.securesms.recipients.ui.RecipientSettingsCoordinatorLayoutBehavior" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="158dp"
|
||||
android:elevation="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||
tools:text="Parkdale Run Crew" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/preference_divider"
|
||||
android:fillViewport="true"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_disappearing_messages_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/disappearing_messages_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="@dimen/group_manage_fragment_row_height"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageRecipientActivity_disappearing_messages"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/disappearing_messages"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:minWidth="48dp"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
tools:text="Off" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_notifications_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_disappearing_messages_card"
|
||||
app:layout_goneMarginTop="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/group_mute_notifications_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:padding="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_mute_notifications"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ManageGroupActivity_mute_notifications"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@id/group_mute_notifications_until"
|
||||
app:layout_constraintEnd_toStartOf="@id/group_mute_notifications_switch"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_mute_notifications_until"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextSecure.SubtitleTextStyle"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_mute_notifications"
|
||||
tools:text="Until 12:42 PM"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/group_mute_notifications_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"
|
||||
android:enabled="false"
|
||||
android:minWidth="48dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/group_mute_notifications"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/group_custom_notifications_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_custom_notifications"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_custom_notifications"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/group_mute_notifications_switch"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_custom_notifications_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical|end"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/group_custom_notifications"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_mute_notifications_switch"
|
||||
tools:text="Off" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/group_mentions_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_mentions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_mentions"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/group_mute_notifications_switch"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_mentions_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical|end"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/group_custom_notifications"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_mute_notifications_switch"
|
||||
tools:text="Default (Notify me)" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chat_wallpaper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_chat_color_and_wallpaper"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_media_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_notifications_card"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shared_media_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/recipient_preference_activity__shared_media"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:minWidth="48dp"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_see_all"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThreadPhotoRailView
|
||||
android:id="@+id/recent_photos"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="90dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/shared_media_row" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_access_control_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_media_card"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/edit_group_membership_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edit_group_membership_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_add_members"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edit_group_membership_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical|end"
|
||||
android:minWidth="136dp"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
tools:text="Only admin" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/edit_group_access_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edit_group_access_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_edit_group_info"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/edit_group_access_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical|end"
|
||||
android:minWidth="136dp"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
tools:text="All members" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_link_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_access_control_card">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pending_and_requesting_members_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:baselineAligned="false"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/ManageGroupActivity_member_requests_and_invites"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pending_and_requesting_members_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="@drawable/circle_ultramarine"
|
||||
android:gravity="center"
|
||||
android:minWidth="22dp"
|
||||
android:minHeight="22dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
android:textColor="@color/core_white"
|
||||
tools:text="3" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/group_link_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_group_link"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_link_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical|end"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/group_custom_notifications"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_mute_notifications_switch"
|
||||
tools:text="Off" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_membership_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_link_card">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/member_count_2"
|
||||
style="@style/TextAppearance.Signal.Subtitle2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
tools:text="12 members" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/add_members"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawableStart="@drawable/ic_add_members_circle"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_add_members"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
|
||||
android:id="@+id/group_members"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:listitem="@layout/group_recipient_list_item" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/toggle_all_members"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawableStart="@drawable/ic_view_all_circle"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_view_all_members"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/group_block_and_leave_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_membership_card">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/blockGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_block_group"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/core_red" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unblockGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_unblock_group"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/core_red"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/leaveGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_leave_group"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/core_red" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -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">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/recipient_avatar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
|
@ -34,12 +35,12 @@
|
|||
android:id="@+id/recipient_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:maxLines="2"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:maxLines="2"
|
||||
app:layout_constraintBottom_toTopOf="@+id/recipient_about"
|
||||
app:layout_constraintEnd_toStartOf="@+id/admin"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
|
@ -51,20 +52,20 @@
|
|||
android:id="@+id/recipient_about"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="start|center_vertical"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Preview"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
app:emoji_forceCustom="true"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/recipient_avatar"
|
||||
app:layout_constraintEnd_toStartOf="@+id/admin"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@+id/recipient_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@+id/recipient_name"
|
||||
app:emoji_forceCustom="true"
|
||||
tools:text="Hangin' around the web" />
|
||||
|
||||
<TextView
|
||||
|
@ -74,9 +75,10 @@
|
|||
android:layout_marginEnd="16dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:text="@string/GroupRecipientListItem_admin"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Subtitle2"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:visibility="gone"
|
||||
app:layout_goneMarginEnd="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/popupMenuProgressContainer"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
|
@ -88,7 +90,6 @@
|
|||
android:id="@+id/popupMenuProgressContainer"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -99,7 +100,6 @@
|
|||
android:id="@+id/popupMenu"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:visibility="gone"
|
||||
app:background_tint="@color/signal_text_primary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/dsl_preference_item_background"
|
||||
android:minHeight="56dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@drawable/ic_advanced_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@id/summary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:layout_goneMarginBottom="16dp"
|
||||
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||
tools:text="Message font size" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/summary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:lineSpacingExtra="4sp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
android:textColor="@color/text_color_secondary_enabled_selector"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
app:layout_goneMarginStart="@dimen/dsl_settings_gutter"
|
||||
app:layout_goneMarginTop="16dp"
|
||||
tools:text="Some random text to get stuff onto more than one line but not absurdly long like lorem/random"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -8,10 +8,10 @@
|
|||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/rbs_recipient_avatar"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -30,10 +30,12 @@
|
|||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/rbs_full_name"
|
||||
style="@style/TextAppearance.Signal.Body1.Bold"
|
||||
style="@style/Signal.Text.Headline.Medium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:textColor="@color/signal_text_primary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
|
@ -43,27 +45,32 @@
|
|||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/rbs_about"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
style="@style/Signal.Text.Body"
|
||||
app:layout_constraintTop_toBottomOf="@id/rbs_full_name"
|
||||
app:emoji_forceCustom="true"
|
||||
tools:text="🕷🕷🕷Hangin' on the web🕷🕷"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/rbs_full_name"
|
||||
tools:text="🕷🕷🕷Hangin' on the web🕷🕷" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/rbs_username_number"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rbs_about"
|
||||
app:layout_goneMarginTop="8dp"
|
||||
tools:text="\@spidergwen +1 555-654-6657" />
|
||||
|
||||
<TextView
|
||||
|
@ -71,9 +78,9 @@
|
|||
style="@style/TextAppearance.Signal.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:text="@string/ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/signal_text_primary"
|
||||
|
@ -88,57 +95,14 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rbs_note_to_self_description">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="12dp">
|
||||
<include layout="@layout/conversation_settings_button_strip" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/rbs_message_button"
|
||||
style="@style/Widget.Signal.Button.Icon.Circular"
|
||||
android:contentDescription="@string/RecipientBottomSheet_message_description"
|
||||
app:backgroundTint="@color/recipient_contact_button_color"
|
||||
app:icon="@drawable/ic_message_primary_accent_24"
|
||||
app:rippleColor="@color/core_ultramarine" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/rbs_video_call_button"
|
||||
style="@style/Widget.Signal.Button.Icon.Circular"
|
||||
android:layout_marginStart="36dp"
|
||||
android:contentDescription="@string/RecipientBottomSheet_video_call_description"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/recipient_contact_button_color"
|
||||
app:icon="@drawable/ic_video_primary_accent_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/rbs_secure_call_button"
|
||||
style="@style/Widget.Signal.Button.Icon.Circular"
|
||||
android:layout_marginStart="36dp"
|
||||
android:contentDescription="@string/RecipientBottomSheet_voice_call_description"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/recipient_contact_button_color"
|
||||
app:icon="@drawable/ic_phone_right_primary_accent_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/rbs_insecure_call_button"
|
||||
style="@style/Widget.Signal.Button.Icon.Circular"
|
||||
android:layout_marginStart="36dp"
|
||||
android:contentDescription="@string/RecipientBottomSheet_insecure_voice_call_description"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/recipient_contact_button_color"
|
||||
app:icon="@drawable/ic_phone_right_unlock_primary_accent_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
<include layout="@layout/dsl_divider_item" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/rbs_block_button"
|
||||
|
@ -165,39 +129,15 @@
|
|||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/rbs_add_contact_button"
|
||||
android:id="@+id/rbs_remove_from_group_button"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/RecipientBottomSheet_add_to_contacts"
|
||||
android:text="@string/RecipientBottomSheet_remove_from_group"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_plus_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/rbs_add_to_group_button"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_group_24"
|
||||
tools:text="@string/RecipientBottomSheet_add_to_a_group"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/rbs_view_safety_number_button"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/RecipientBottomSheet_view_safety_number"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_info_tinted_24"
|
||||
app:drawableStartCompat="@drawable/ic_leave_tinted_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
|
@ -207,7 +147,7 @@
|
|||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/RecipientBottomSheet_make_group_admin"
|
||||
android:text="@string/RecipientBottomSheet_make_admin"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_group_24"
|
||||
tools:visibility="visible" />
|
||||
|
@ -225,16 +165,42 @@
|
|||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/rbs_remove_from_group_button"
|
||||
android:id="@+id/rbs_add_to_group_button"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/RecipientBottomSheet_remove_from_group"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_leave_tinted_24"
|
||||
app:drawableStartCompat="@drawable/ic_group_24"
|
||||
tools:text="@string/RecipientBottomSheet_add_to_a_group"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/rbs_add_contact_button"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/RecipientBottomSheet_add_to_contacts"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_plus_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/rbs_view_safety_number_button"
|
||||
style="@style/Widget.Signal.Button.TextButton.Drawable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/RecipientBottomSheet_view_safety_number"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/ic_safety_number_24"
|
||||
app:drawableTint="@color/signal_icon_tint_primary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".recipients.ui.managerecipient.ManageRecipientActivity">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
|
@ -1,639 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".recipients.ui.managerecipient.ManageRecipientFragment"
|
||||
tools:theme="@style/TextSecure.LightNoActionBar">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@null"
|
||||
app:contentScrim="?android:attr/windowBackground"
|
||||
app:expandedTitleGravity="center_horizontal"
|
||||
app:expandedTitleMarginTop="156dp"
|
||||
app:expandedTitleTextAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed"
|
||||
app:scrimAnimationDuration="200"
|
||||
app:scrimVisibleHeightTrigger="156dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="128dp"
|
||||
android:layout_gravity="center_horizontal" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="14dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/about"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:gravity="center"
|
||||
app:emoji_forceCustom="true"
|
||||
tools:text="Hangin' around the web" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/username_number"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
tools:text="\@spidergwen +1 555-654-6657" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/recipient_internal_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient_internal_details_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="14dp"
|
||||
android:textAppearance="@style/Signal.Text.Caption"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:gravity="center"
|
||||
android:textIsSelectable="true"
|
||||
tools:text="Internal Details" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/recipient_internal_details_disable_profile_sharing_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Signal.Widget.Button.Medium.Secondary"
|
||||
android:text="@string/preferences__internal_disable_profile_sharing"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/recipient_message"
|
||||
style="@style/Widget.Signal.Button.Icon.Circular"
|
||||
android:contentDescription="@string/ManageRecipientActivity_message_description"
|
||||
app:backgroundTint="@color/recipient_contact_button_color"
|
||||
app:icon="@drawable/ic_message_primary_accent_24"
|
||||
app:rippleColor="@color/core_ultramarine" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/recipient_video_call"
|
||||
style="@style/Widget.Signal.Button.Icon.Circular"
|
||||
android:layout_marginStart="36dp"
|
||||
android:contentDescription="@string/ManageRecipientActivity_video_call_description"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/recipient_contact_button_color"
|
||||
app:icon="@drawable/ic_video_primary_accent_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/recipient_voice_call"
|
||||
style="@style/Widget.Signal.Button.Icon.Circular"
|
||||
android:layout_marginStart="36dp"
|
||||
android:contentDescription="@string/ManageRecipientActivity_voice_call_description"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/recipient_contact_button_color"
|
||||
app:icon="@drawable/ic_phone_right_primary_accent_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/recipient_insecure_voice_call"
|
||||
style="@style/Widget.Signal.Button.Icon.Circular"
|
||||
android:layout_marginStart="36dp"
|
||||
android:contentDescription="@string/ManageRecipientActivity_insecure_voice_call_description"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/recipient_contact_button_color"
|
||||
app:icon="@drawable/ic_phone_right_unlock_primary_accent_24"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:contentInsetStartWithNavigation="0dp"
|
||||
app:layout_collapseMode="pin"
|
||||
app:navigationIcon="@drawable/ic_arrow_left_24">
|
||||
|
||||
<Space
|
||||
android:id="@+id/avatar_target"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:layout_marginEnd="10dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/name_target"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold" />
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/recipient_avatar"
|
||||
android:layout_width="128dp"
|
||||
android:layout_height="128dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:elevation="8dp"
|
||||
android:transitionName="avatar"
|
||||
app:layout_behavior="org.thoughtcrime.securesms.recipients.ui.RecipientSettingsCoordinatorLayoutBehavior" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="158dp"
|
||||
android:elevation="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
|
||||
tools:text="Gwen Stacy" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/preference_divider"
|
||||
android:fillViewport="true"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/recipient_contact_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/recipient_contact_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient_contact_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageRecipientActivity_this_person_is_in_your_contacts"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/recipient_contact_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:minWidth="48dp"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
android:tint="?colorAccent"
|
||||
app:srcCompat="@drawable/ic_profile_circle_outline_24"
|
||||
tools:text="Off" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/recipient_disappearing_messages_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/recipient_contact_card">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/disappearing_messages_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="@dimen/group_manage_fragment_row_height"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageRecipientActivity_disappearing_messages"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/disappearing_messages"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:minWidth="48dp"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:gravity="end"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
tools:text="Off" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/recipient_notifications_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/recipient_disappearing_messages_card"
|
||||
app:layout_goneMarginTop="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/recipient_mute_notifications_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient_mute_notifications"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:gravity="center_vertical|start"
|
||||
android:text="@string/ManageRecipientActivity_mute_notifications"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toTopOf="@id/recipient_mute_notifications_until"
|
||||
app:layout_constraintEnd_toStartOf="@id/recipient_mute_notifications_switch"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient_mute_notifications_until"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/TextSecure.SubtitleTextStyle"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/recipient_mute_notifications"
|
||||
tools:text="Until 12:42 PM"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/recipient_mute_notifications_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"
|
||||
android:enabled="false"
|
||||
android:minWidth="48dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/recipient_mute_notifications"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/recipient_custom_notifications_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient_custom_notifications"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageRecipientActivity_custom_notifications"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/recipient_mute_notifications_switch"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient_custom_notifications_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical|end"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/recipient_custom_notifications"
|
||||
app:layout_constraintTop_toBottomOf="@id/recipient_mute_notifications_switch"
|
||||
tools:text="Off" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/recipient_media_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/recipient_notifications_card"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shared_media_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/recipient_preference_activity__shared_media"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:minWidth="48dp"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageRecipientActivity_see_all"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThreadPhotoRailView
|
||||
android:id="@+id/recent_photos"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="90dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/shared_media_row" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/recipient_chat_color"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/recipient_media_card">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/chat_wallpaper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/preferences__chat_color_and_wallpaper"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/recipient_membership_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/recipient_chat_color">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/groups_in_common_count"
|
||||
style="@style/TextAppearance.Signal.Subtitle2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
tools:text="8 groups in common" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/add_to_a_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawableStart="@drawable/ic_add_members_circle"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageRecipientActivity_add_to_a_group"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button" />
|
||||
|
||||
<org.thoughtcrime.securesms.groups.ui.GroupMemberListView
|
||||
android:id="@+id/shared_group_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:listitem="@layout/group_recipient_list_item" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/toggle_all_groups"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawableStart="@drawable/ic_view_all_circle"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageRecipientActivity_view_all_groups"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/signal_text_secondary"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/recipient_block_and_leave_card"
|
||||
style="@style/Widget.Signal.CardView.PreferenceRow"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/group_manage_fragment_card_vertical_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/recipient_membership_card">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/view_safety_number"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageRecipientActivity_view_safety_number"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/block"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageRecipientActivity_block"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/core_red" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unblock"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical|start"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageRecipientActivity_unblock"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/core_red"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue