Refactor conversation settings screens into a single fragment with new UI.

fork-5.53.8
Alex Hart 2021-06-24 13:52:54 -03:00 zatwierdzone przez Cody Henthorne
rodzic f19033a7a2
commit da2ee33dff
121 zmienionych plików z 4394 dodań i 4076 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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