mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Implement Stories feature behind flag.
Co-Authored-By: Greyson Parrelli <37311915+greyson-signal@users.noreply.github.com> Co-Authored-By: Rashad Sookram <95182499+rashad-signal@users.noreply.github.com>
This commit is contained in:
@@ -2,8 +2,9 @@ package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
@@ -12,6 +13,7 @@ import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
/**
|
||||
@@ -21,9 +23,12 @@ abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFr
|
||||
|
||||
protected open val peekHeightPercentage: Float = 0.5f
|
||||
|
||||
@StyleRes
|
||||
protected open val themeResId: Int = R.style.Widget_Signal_FixedRoundedCorners
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NORMAL, R.style.Widget_Signal_FixedRoundedCorners)
|
||||
setStyle(STYLE_NORMAL, themeResId)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
@@ -38,7 +43,8 @@ abstract class FixedRoundedCornerBottomSheetDialogFragment : BottomSheetDialogFr
|
||||
|
||||
val dialogBackground = MaterialShapeDrawable(shapeAppearanceModel)
|
||||
|
||||
dialogBackground.setTint(ContextCompat.getColor(requireContext(), R.color.signal_background_dialog))
|
||||
val bottomSheetStyle = ThemeUtil.getThemedResourceId(ContextThemeWrapper(requireContext(), themeResId), R.attr.bottomSheetStyle)
|
||||
dialogBackground.setTint(ThemeUtil.getThemedColor(ContextThemeWrapper(requireContext(), bottomSheetStyle), R.attr.backgroundTint))
|
||||
|
||||
dialog.behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
|
||||
/**
|
||||
* Activity that wraps a given fragment
|
||||
*/
|
||||
abstract class FragmentWrapperActivity : PassphraseRequiredActivity() {
|
||||
|
||||
protected open val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
setContentView(R.layout.fragment_container)
|
||||
dynamicTheme.onCreate(this)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, getFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getFragment(): Fragment
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
|
||||
|
||||
private var hasShown = false
|
||||
|
||||
protected open val withDim: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setStyle(STYLE_NORMAL, R.style.Theme_Signal_RoundedBottomSheet)
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -29,7 +31,10 @@ abstract class KeyboardEntryDialogFragment(@LayoutRes contentLayoutId: Int) :
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
|
||||
dialog.window?.setDimAmount(0f)
|
||||
if (!withDim) {
|
||||
dialog.window?.setDimAmount(0f)
|
||||
}
|
||||
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
||||
return dialog
|
||||
|
||||
@@ -29,13 +29,17 @@ public class OutlinedThumbnailView extends ThumbnailView {
|
||||
cornerMask = new CornerMask(this);
|
||||
outliner = new Outliner();
|
||||
|
||||
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
|
||||
int defaultOutlinerColor = ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20);
|
||||
outliner.setColor(defaultOutlinerColor);
|
||||
|
||||
int radius = 0;
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.OutlinedThumbnailView, 0, 0);
|
||||
radius = typedArray.getDimensionPixelOffset(R.styleable.OutlinedThumbnailView_otv_cornerRadius, 0);
|
||||
|
||||
outliner.setStrokeWidth(typedArray.getDimensionPixelSize(R.styleable.OutlinedThumbnailView_otv_strokeWidth, 1));
|
||||
outliner.setColor(typedArray.getColor(R.styleable.OutlinedThumbnailView_otv_strokeColor, defaultOutlinerColor));
|
||||
}
|
||||
|
||||
setRadius(radius);
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
@@ -19,7 +20,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
@@ -45,9 +46,10 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
|
||||
private static final String TAG = Log.tag(QuoteView.class);
|
||||
|
||||
private static final int MESSAGE_TYPE_PREVIEW = 0;
|
||||
private static final int MESSAGE_TYPE_OUTGOING = 1;
|
||||
private static final int MESSAGE_TYPE_INCOMING = 2;
|
||||
private static final int MESSAGE_TYPE_PREVIEW = 0;
|
||||
private static final int MESSAGE_TYPE_OUTGOING = 1;
|
||||
private static final int MESSAGE_TYPE_INCOMING = 2;
|
||||
private static final int MESSAGE_TYPE_STORY_REPLY = 3;
|
||||
|
||||
private ViewGroup mainView;
|
||||
private ViewGroup footerView;
|
||||
@@ -71,6 +73,8 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
private int smallCornerRadius;
|
||||
private CornerMask cornerMask;
|
||||
|
||||
private int thumbHeight;
|
||||
private int thumbWidth;
|
||||
|
||||
public QuoteView(Context context) {
|
||||
super(context);
|
||||
@@ -136,6 +140,21 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
}
|
||||
}
|
||||
|
||||
if (messageType == MESSAGE_TYPE_STORY_REPLY) {
|
||||
thumbWidth = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_width);
|
||||
thumbHeight = getResources().getDimensionPixelOffset(R.dimen.quote_story_thumb_height);
|
||||
|
||||
mainView.setMinimumHeight(thumbHeight);
|
||||
|
||||
ViewGroup.LayoutParams params = thumbnailView.getLayoutParams();
|
||||
params.height = thumbHeight;
|
||||
params.width = thumbWidth;
|
||||
|
||||
thumbnailView.setLayoutParams(params);
|
||||
} else {
|
||||
thumbWidth = thumbHeight = getResources().getDimensionPixelSize(R.dimen.quote_thumb_size);
|
||||
}
|
||||
|
||||
dismissView.setOnClickListener(view -> setVisibility(GONE));
|
||||
}
|
||||
|
||||
@@ -209,10 +228,14 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
|
||||
private void setQuoteAuthor(@NonNull Recipient author) {
|
||||
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
|
||||
boolean preview = messageType == MESSAGE_TYPE_PREVIEW;
|
||||
boolean preview = messageType == MESSAGE_TYPE_PREVIEW || messageType == MESSAGE_TYPE_STORY_REPLY;
|
||||
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
|
||||
: author.getDisplayName(getContext()));
|
||||
if (messageType == MESSAGE_TYPE_STORY_REPLY && author.isGroup()) {
|
||||
authorView.setText(getContext().getString(R.string.QuoteView_s_story, author.getDisplayName(getContext())));
|
||||
} else {
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
|
||||
: author.getDisplayName(getContext()));
|
||||
}
|
||||
|
||||
quoteBarView.setBackgroundColor(ContextCompat.getColor(getContext(), outgoing ? R.color.core_white : android.R.color.transparent));
|
||||
mainView.setBackgroundColor(ContextCompat.getColor(getContext(), preview ? R.color.quote_preview_background : R.color.quote_view_background));
|
||||
@@ -279,7 +302,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
}
|
||||
glideRequests.load(new DecryptableUri(imageVideoSlide.getUri()))
|
||||
.centerCrop()
|
||||
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
|
||||
.override(thumbWidth, thumbHeight)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.into(thumbnailView);
|
||||
} else if (documentSlide != null){
|
||||
|
||||
@@ -5,10 +5,6 @@ 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.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
@@ -19,7 +15,6 @@ 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;
|
||||
@@ -33,7 +28,6 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -45,11 +39,8 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -9,6 +10,8 @@ import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.res.TypedArrayUtils;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
@@ -20,8 +23,8 @@ import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment;
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemedFragment;
|
||||
|
||||
public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
@@ -34,6 +37,7 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
private State keyboardState;
|
||||
private KeyboardPagerFragment keyboardPagerFragment;
|
||||
private FragmentManager fragmentManager;
|
||||
private int mediaKeyboardTheme;
|
||||
|
||||
public MediaKeyboard(Context context) {
|
||||
this(context, null);
|
||||
@@ -41,6 +45,12 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
public MediaKeyboard(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MediaKeyboard);
|
||||
mediaKeyboardTheme = array.getResourceId(R.styleable.MediaKeyboard_media_keyboard_theme, -1);
|
||||
array.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFragmentManager(@NonNull FragmentManager fragmentManager) {
|
||||
@@ -70,6 +80,10 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
show();
|
||||
}
|
||||
|
||||
public boolean isInitialised() {
|
||||
return isInitialised;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
if (!isInitialised) initView();
|
||||
|
||||
@@ -122,9 +136,14 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
|
||||
keyboardState = State.EMOJI_SEARCH;
|
||||
|
||||
EmojiSearchFragment emojiSearchFragment = new EmojiSearchFragment();
|
||||
if (mediaKeyboardTheme != -1) {
|
||||
ThemedFragment.withTheme(emojiSearchFragment, mediaKeyboardTheme);
|
||||
}
|
||||
|
||||
fragmentManager.beginTransaction()
|
||||
.hide(keyboardPagerFragment)
|
||||
.add(R.id.media_keyboard_fragment_container, new EmojiSearchFragment(), EMOJI_SEARCH)
|
||||
.add(R.id.media_keyboard_fragment_container, emojiSearchFragment, EMOJI_SEARCH)
|
||||
.runOnCommit(() -> show(latestKeyboardHeight, true))
|
||||
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
|
||||
.commitAllowingStateLoss();
|
||||
@@ -141,6 +160,10 @@ public class MediaKeyboard extends FrameLayout implements InputView {
|
||||
}
|
||||
|
||||
keyboardPagerFragment = new KeyboardPagerFragment();
|
||||
if (mediaKeyboardTheme != -1) {
|
||||
ThemedFragment.withTheme(keyboardPagerFragment, mediaKeyboardTheme);
|
||||
}
|
||||
|
||||
fragmentManager.beginTransaction()
|
||||
.replace(R.id.media_keyboard_fragment_container, keyboardPagerFragment)
|
||||
.commitNowAllowingStateLoss();
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Tiago Ornelas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.segmentedprogressbar
|
||||
|
||||
/**
|
||||
* Created by Tiago Ornelas on 18/04/2020.
|
||||
* Model that holds the segment state
|
||||
*/
|
||||
class Segment(val animationDurationMillis: Long) {
|
||||
|
||||
private var animationProgress: Int = 0
|
||||
|
||||
var animationState: AnimationState = AnimationState.IDLE
|
||||
set(value) {
|
||||
animationProgress = when (value) {
|
||||
AnimationState.ANIMATED -> 100
|
||||
AnimationState.IDLE -> 0
|
||||
else -> animationProgress
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents possible drawing states of the segment
|
||||
*/
|
||||
enum class AnimationState {
|
||||
ANIMATED,
|
||||
ANIMATING,
|
||||
IDLE
|
||||
}
|
||||
|
||||
val progressPercentage: Float
|
||||
get() = animationProgress.toFloat() / 100
|
||||
|
||||
fun progress() = animationProgress++
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Tiago Ornelas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.segmentedprogressbar
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Path
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Created by Tiago Ornelas on 18/04/2020.
|
||||
* Represents a segmented progress bar on which, the progress is set by segments
|
||||
* @see Segment
|
||||
* And the progress of each segment is animated based on a set speed
|
||||
*/
|
||||
class SegmentedProgressBar : View, Runnable, ViewPager.OnPageChangeListener, View.OnTouchListener {
|
||||
|
||||
private val path = Path()
|
||||
private val corners = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
|
||||
|
||||
/**
|
||||
* Number of total segments to draw
|
||||
*/
|
||||
var segmentCount: Int = resources.getInteger(R.integer.segmentedprogressbar_default_segments_count)
|
||||
set(value) {
|
||||
field = value
|
||||
this.initSegments()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping of segment index -> duration in millis
|
||||
*/
|
||||
var segmentDurations: Map<Int, Long> = mapOf()
|
||||
set(value) {
|
||||
field = value
|
||||
this.initSegments()
|
||||
}
|
||||
|
||||
var margin: Int = resources.getDimensionPixelSize(R.dimen.segmentedprogressbar_default_segment_margin)
|
||||
private set
|
||||
var radius: Int = resources.getDimensionPixelSize(R.dimen.segmentedprogressbar_default_corner_radius)
|
||||
private set
|
||||
var segmentStrokeWidth: Int =
|
||||
resources.getDimensionPixelSize(R.dimen.segmentedprogressbar_default_segment_stroke_width)
|
||||
private set
|
||||
|
||||
var segmentBackgroundColor: Int = Color.WHITE
|
||||
private set
|
||||
var segmentSelectedBackgroundColor: Int =
|
||||
context.getThemeColor(R.attr.colorAccent)
|
||||
private set
|
||||
var segmentStrokeColor: Int = Color.BLACK
|
||||
private set
|
||||
var segmentSelectedStrokeColor: Int = Color.BLACK
|
||||
private set
|
||||
|
||||
var timePerSegmentMs: Long =
|
||||
resources.getInteger(R.integer.segmentedprogressbar_default_time_per_segment_ms).toLong()
|
||||
private set
|
||||
|
||||
private var segments = mutableListOf<Segment>()
|
||||
private val selectedSegment: Segment?
|
||||
get() = segments.firstOrNull { it.animationState == Segment.AnimationState.ANIMATING }
|
||||
private val selectedSegmentIndex: Int
|
||||
get() = segments.indexOf(this.selectedSegment)
|
||||
|
||||
private val animationHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
// Drawing
|
||||
val strokeApplicable: Boolean
|
||||
get() = segmentStrokeWidth * 4 <= measuredHeight
|
||||
|
||||
val segmentWidth: Float
|
||||
get() = (measuredWidth - margin * (segmentCount - 1)).toFloat() / segmentCount
|
||||
|
||||
var viewPager: ViewPager? = null
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
set(value) {
|
||||
field = value
|
||||
if (value == null) {
|
||||
viewPager?.removeOnPageChangeListener(this)
|
||||
viewPager?.setOnTouchListener(null)
|
||||
} else {
|
||||
viewPager?.addOnPageChangeListener(this)
|
||||
viewPager?.setOnTouchListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets callbacks for progress bar state changes
|
||||
* @see SegmentedProgressBarListener
|
||||
*/
|
||||
var listener: SegmentedProgressBarListener? = null
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
|
||||
val typedArray =
|
||||
context.theme.obtainStyledAttributes(attrs, R.styleable.SegmentedProgressBar, 0, 0)
|
||||
|
||||
segmentCount =
|
||||
typedArray.getInt(R.styleable.SegmentedProgressBar_totalSegments, segmentCount)
|
||||
|
||||
margin =
|
||||
typedArray.getDimensionPixelSize(
|
||||
R.styleable.SegmentedProgressBar_segmentMargins,
|
||||
margin
|
||||
)
|
||||
radius =
|
||||
typedArray.getDimensionPixelSize(
|
||||
R.styleable.SegmentedProgressBar_segmentCornerRadius,
|
||||
radius
|
||||
)
|
||||
segmentStrokeWidth =
|
||||
typedArray.getDimensionPixelSize(
|
||||
R.styleable.SegmentedProgressBar_segmentStrokeWidth,
|
||||
segmentStrokeWidth
|
||||
)
|
||||
|
||||
segmentBackgroundColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.SegmentedProgressBar_segmentBackgroundColor,
|
||||
segmentBackgroundColor
|
||||
)
|
||||
segmentSelectedBackgroundColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.SegmentedProgressBar_segmentSelectedBackgroundColor,
|
||||
segmentSelectedBackgroundColor
|
||||
)
|
||||
|
||||
segmentStrokeColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.SegmentedProgressBar_segmentStrokeColor,
|
||||
segmentStrokeColor
|
||||
)
|
||||
segmentSelectedStrokeColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.SegmentedProgressBar_segmentSelectedStrokeColor,
|
||||
segmentSelectedStrokeColor
|
||||
)
|
||||
|
||||
timePerSegmentMs =
|
||||
typedArray.getInt(
|
||||
R.styleable.SegmentedProgressBar_timePerSegment,
|
||||
timePerSegmentMs.toInt()
|
||||
).toLong()
|
||||
|
||||
typedArray.recycle()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
init {
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
segments.forEachIndexed { index, segment ->
|
||||
val drawingComponents = getDrawingComponents(segment, index)
|
||||
|
||||
when (index) {
|
||||
0 -> {
|
||||
corners.indices.forEach { corners[it] = 0f }
|
||||
corners[0] = radius.toFloat()
|
||||
corners[1] = radius.toFloat()
|
||||
corners[6] = radius.toFloat()
|
||||
corners[7] = radius.toFloat()
|
||||
}
|
||||
segments.lastIndex -> {
|
||||
corners.indices.forEach { corners[it] = 0f }
|
||||
corners[2] = radius.toFloat()
|
||||
corners[3] = radius.toFloat()
|
||||
corners[4] = radius.toFloat()
|
||||
corners[5] = radius.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
drawingComponents.first.forEachIndexed { drawingIndex, rectangle ->
|
||||
when (index) {
|
||||
0, segments.lastIndex -> {
|
||||
path.reset()
|
||||
path.addRoundRect(rectangle, corners, Path.Direction.CW)
|
||||
canvas?.drawPath(path, drawingComponents.second[drawingIndex])
|
||||
}
|
||||
else -> canvas?.drawRect(
|
||||
rectangle,
|
||||
drawingComponents.second[drawingIndex]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start/Resume progress animation
|
||||
*/
|
||||
fun start() {
|
||||
pause()
|
||||
val segment = selectedSegment
|
||||
if (segment == null)
|
||||
next()
|
||||
else
|
||||
animationHandler.postDelayed(this, segment.animationDurationMillis / 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the animation process
|
||||
*/
|
||||
fun pause() {
|
||||
animationHandler.removeCallbacks(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the whole animation state and selected segments
|
||||
* !Doesn't restart it!
|
||||
* To restart, call the start() method
|
||||
*/
|
||||
fun reset() {
|
||||
this.segments.map { it.animationState = Segment.AnimationState.IDLE }
|
||||
this.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts animation for the following segment
|
||||
*/
|
||||
fun next() {
|
||||
loadSegment(offset = 1, userAction = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts animation for the previous segment
|
||||
*/
|
||||
fun previous() {
|
||||
loadSegment(offset = -1, userAction = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts animation for the current segment
|
||||
*/
|
||||
fun restartSegment() {
|
||||
loadSegment(offset = 0, userAction = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips a number of segments
|
||||
* @param offset number o segments fo skip
|
||||
*/
|
||||
fun skip(offset: Int) {
|
||||
loadSegment(offset = offset, userAction = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current segment to the
|
||||
* @param position index
|
||||
*/
|
||||
fun setPosition(position: Int) {
|
||||
loadSegment(offset = position - this.selectedSegmentIndex, userAction = true)
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private fun loadSegment(offset: Int, userAction: Boolean) {
|
||||
val oldSegmentIndex = this.segments.indexOf(this.selectedSegment)
|
||||
|
||||
val nextSegmentIndex = oldSegmentIndex + offset
|
||||
|
||||
// Index out of bounds, ignore operation
|
||||
if (userAction && nextSegmentIndex !in 0 until segmentCount) {
|
||||
if (nextSegmentIndex >= segmentCount) {
|
||||
this.listener?.onFinished()
|
||||
} else {
|
||||
restartSegment()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
segments.mapIndexed { index, segment ->
|
||||
if (offset > 0) {
|
||||
if (index < nextSegmentIndex) segment.animationState =
|
||||
Segment.AnimationState.ANIMATED
|
||||
} else if (offset < 0) {
|
||||
if (index > nextSegmentIndex - 1) segment.animationState =
|
||||
Segment.AnimationState.IDLE
|
||||
} else if (offset == 0) {
|
||||
if (index == nextSegmentIndex) segment.animationState = Segment.AnimationState.IDLE
|
||||
}
|
||||
}
|
||||
|
||||
val nextSegment = this.segments.getOrNull(nextSegmentIndex)
|
||||
|
||||
// Handle next segment transition/ending
|
||||
if (nextSegment != null) {
|
||||
pause()
|
||||
nextSegment.animationState = Segment.AnimationState.ANIMATING
|
||||
animationHandler.postDelayed(this, nextSegment.animationDurationMillis / 100)
|
||||
this.listener?.onPage(oldSegmentIndex, this.selectedSegmentIndex)
|
||||
viewPager?.currentItem = this.selectedSegmentIndex
|
||||
} else {
|
||||
animationHandler.removeCallbacks(this)
|
||||
this.listener?.onFinished()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSegments() {
|
||||
this.segments.clear()
|
||||
segments.addAll(
|
||||
List(segmentCount) {
|
||||
val duration = segmentDurations[it] ?: timePerSegmentMs
|
||||
Segment(duration)
|
||||
}
|
||||
)
|
||||
this.invalidate()
|
||||
reset()
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
if (this.selectedSegment?.progress() ?: 0 >= 100) {
|
||||
loadSegment(offset = 1, userAction = false)
|
||||
} else {
|
||||
this.invalidate()
|
||||
animationHandler.postDelayed(this, this.selectedSegment?.animationDurationMillis?.let { it / 100 } ?: (timePerSegmentMs / 100))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) {}
|
||||
|
||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
this.setPosition(position)
|
||||
}
|
||||
|
||||
override fun onTouch(p0: View?, p1: MotionEvent?): Boolean {
|
||||
when (p1?.action) {
|
||||
MotionEvent.ACTION_DOWN -> pause()
|
||||
MotionEvent.ACTION_UP -> start()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Tiago Ornelas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.segmentedprogressbar
|
||||
|
||||
/**
|
||||
* Created by Tiago Ornelas on 18/04/2020.
|
||||
* Interface to communicate progress events
|
||||
*/
|
||||
interface SegmentedProgressBarListener {
|
||||
/**
|
||||
* Notifies when selected segment changed
|
||||
*/
|
||||
fun onPage(oldPageIndex: Int, newPageIndex: Int)
|
||||
|
||||
/**
|
||||
* Notifies when last segment finished animating
|
||||
*/
|
||||
fun onFinished()
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Tiago Ornelas
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.components.segmentedprogressbar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.util.TypedValue
|
||||
|
||||
fun Context.getThemeColor(attributeColor: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
this.theme.resolveAttribute(attributeColor, typedValue, true)
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
fun SegmentedProgressBar.getDrawingComponents(
|
||||
segment: Segment,
|
||||
segmentIndex: Int
|
||||
): Pair<MutableList<RectF>, MutableList<Paint>> {
|
||||
|
||||
val rectangles = mutableListOf<RectF>()
|
||||
val paints = mutableListOf<Paint>()
|
||||
val segmentWidth = segmentWidth
|
||||
val startBound = segmentIndex * segmentWidth + ((segmentIndex) * margin)
|
||||
val endBound = startBound + segmentWidth
|
||||
val stroke = if (!strokeApplicable) 0f else this.segmentStrokeWidth.toFloat()
|
||||
|
||||
val backgroundPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
color = segmentBackgroundColor
|
||||
}
|
||||
|
||||
val selectedBackgroundPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
color = segmentSelectedBackgroundColor
|
||||
}
|
||||
|
||||
val strokePaint = Paint().apply {
|
||||
color =
|
||||
if (segment.animationState == Segment.AnimationState.IDLE) segmentStrokeColor else segmentSelectedStrokeColor
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = stroke
|
||||
}
|
||||
|
||||
// Background component
|
||||
if (segment.animationState == Segment.AnimationState.ANIMATED) {
|
||||
rectangles.add(RectF(startBound + stroke, height - stroke, endBound - stroke, stroke))
|
||||
paints.add(selectedBackgroundPaint)
|
||||
} else {
|
||||
rectangles.add(RectF(startBound + stroke, height - stroke, endBound - stroke, stroke))
|
||||
paints.add(backgroundPaint)
|
||||
}
|
||||
|
||||
// Progress component
|
||||
if (segment.animationState == Segment.AnimationState.ANIMATING) {
|
||||
rectangles.add(
|
||||
RectF(
|
||||
startBound + stroke,
|
||||
height - stroke,
|
||||
startBound + segment.progressPercentage * segmentWidth,
|
||||
stroke
|
||||
)
|
||||
)
|
||||
paints.add(selectedBackgroundPaint)
|
||||
}
|
||||
|
||||
// Stroke component
|
||||
if (stroke > 0) {
|
||||
rectangles.add(RectF(startBound + stroke, height - stroke, endBound - stroke, stroke))
|
||||
paints.add(strokePaint)
|
||||
}
|
||||
|
||||
return Pair(rectangles, paints)
|
||||
}
|
||||
@@ -25,35 +25,44 @@ abstract class DSLSettingsFragment(
|
||||
protected var layoutManagerProducer: (Context) -> RecyclerView.LayoutManager = { context -> LinearLayoutManager(context) }
|
||||
) : Fragment(layoutId) {
|
||||
|
||||
private var recyclerView: RecyclerView? = null
|
||||
protected var recyclerView: RecyclerView? = null
|
||||
private set
|
||||
|
||||
private var scrollAnimationHelper: OnScrollAnimationHelper? = null
|
||||
|
||||
@CallSuper
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
val toolbarShadow: View = view.findViewById(R.id.toolbar_shadow)
|
||||
val toolbar: Toolbar? = view.findViewById(R.id.toolbar)
|
||||
val toolbarShadow: View? = view.findViewById(R.id.toolbar_shadow)
|
||||
|
||||
if (titleId != -1) {
|
||||
toolbar.setTitle(titleId)
|
||||
toolbar?.setTitle(titleId)
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
toolbar?.setNavigationOnClickListener {
|
||||
requireActivity().onBackPressed()
|
||||
}
|
||||
|
||||
if (menuId != -1) {
|
||||
toolbar.inflateMenu(menuId)
|
||||
toolbar.setOnMenuItemClickListener { onOptionsItemSelected(it) }
|
||||
toolbar?.inflateMenu(menuId)
|
||||
toolbar?.setOnMenuItemClickListener { onOptionsItemSelected(it) }
|
||||
}
|
||||
|
||||
if (toolbarShadow != null) {
|
||||
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
||||
}
|
||||
|
||||
scrollAnimationHelper = getOnScrollAnimationHelper(toolbarShadow)
|
||||
val settingsAdapter = DSLSettingsAdapter()
|
||||
|
||||
recyclerView = view.findViewById<RecyclerView>(R.id.recycler).apply {
|
||||
edgeEffectFactory = EdgeEffectFactory()
|
||||
layoutManager = layoutManagerProducer(requireContext())
|
||||
adapter = settingsAdapter
|
||||
addOnScrollListener(scrollAnimationHelper!!)
|
||||
|
||||
val helper = scrollAnimationHelper
|
||||
if (helper != null) {
|
||||
addOnScrollListener(helper)
|
||||
}
|
||||
}
|
||||
|
||||
bindAdapter(settingsAdapter)
|
||||
|
||||
@@ -4,8 +4,11 @@ import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.InsetDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.Px
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@@ -24,6 +27,23 @@ sealed class DSLSettingsIcon {
|
||||
}
|
||||
}
|
||||
|
||||
private data class FromResourceWithBackground(
|
||||
@DrawableRes private val iconId: Int,
|
||||
@ColorRes private val iconTintId: Int,
|
||||
@DrawableRes private val backgroundId: Int,
|
||||
@ColorRes private val backgroundTint: Int,
|
||||
@Px private val insetPx: Int,
|
||||
) : DSLSettingsIcon() {
|
||||
override fun resolve(context: Context): Drawable {
|
||||
return LayerDrawable(
|
||||
arrayOf(
|
||||
FromResource(backgroundId, backgroundTint).resolve(context),
|
||||
InsetDrawable(FromResource(iconId, iconTintId).resolve(context), insetPx, insetPx, insetPx, insetPx)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class FromDrawable(
|
||||
private val drawable: Drawable
|
||||
) : DSLSettingsIcon() {
|
||||
@@ -33,6 +53,17 @@ sealed class DSLSettingsIcon {
|
||||
abstract fun resolve(context: Context): Drawable
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(
|
||||
@DrawableRes iconId: Int,
|
||||
@ColorRes iconTintId: Int,
|
||||
@DrawableRes backgroundId: Int,
|
||||
@ColorRes backgroundTint: Int,
|
||||
@Px insetPx: Int = 0
|
||||
): DSLSettingsIcon {
|
||||
return FromResourceWithBackground(iconId, iconTintId, backgroundId, backgroundTint, insetPx)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun from(@DrawableRes iconId: Int, @ColorRes iconTintId: Int = R.color.signal_icon_tint_primary): DSLSettingsIcon = FromResource(iconId, iconTintId)
|
||||
|
||||
|
||||
@@ -366,6 +366,17 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
SignalStore.releaseChannelValues().highestVersionNoteReceived = max(SignalStore.releaseChannelValues().highestVersionNoteReceived - 10, 0)
|
||||
}
|
||||
)
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.ConversationListTabs__stories)
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_disable_stories),
|
||||
isChecked = state.disableStories,
|
||||
onClick = {
|
||||
viewModel.toggleStories()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,4 +20,5 @@ data class InternalSettingsState(
|
||||
val removeSenderKeyMinimium: Boolean,
|
||||
val delayResends: Boolean,
|
||||
val disableStorageService: Boolean,
|
||||
val disableStories: Boolean
|
||||
)
|
||||
|
||||
@@ -96,6 +96,12 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun toggleStories() {
|
||||
val newState = !SignalStore.storyValues().isFeatureDisabled
|
||||
SignalStore.storyValues().isFeatureDisabled = newState
|
||||
store.update { getState().copy(disableStories = newState) }
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
store.update { getState().copy(emojiVersion = it.emojiVersion) }
|
||||
}
|
||||
@@ -116,7 +122,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
emojiVersion = null,
|
||||
removeSenderKeyMinimium = SignalStore.internalValues().removeSenderKeyMinimum(),
|
||||
delayResends = SignalStore.internalValues().delayResends(),
|
||||
disableStorageService = SignalStore.internalValues().storageServiceDisabled()
|
||||
disableStorageService = SignalStore.internalValues().storageServiceDisabled(),
|
||||
disableStories = SignalStore.storyValues().isFeatureDisabled
|
||||
)
|
||||
|
||||
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
|
||||
|
||||
@@ -15,10 +15,12 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import mobi.upod.timedurationpicker.TimeDurationPicker
|
||||
import mobi.upod.timedurationpicker.TimeDurationPickerDialog
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.PassphraseChangeActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -34,7 +36,11 @@ import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberListingMode
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.stories.settings.custom.PrivateStorySettingsFragmentArgs
|
||||
import org.thoughtcrime.securesms.stories.settings.story.PrivateStoryItem
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
@@ -71,6 +77,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
adapter.registerFactory(ValueClickPreference::class.java, LayoutFactory(::ValueClickPreferenceViewHolder, R.layout.value_click_preference_item))
|
||||
PrivateStoryItem.register(adapter)
|
||||
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val repository = PrivacySettingsRepository()
|
||||
@@ -288,6 +295,55 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
summary = DSLSettingsText.from(incognitoSummary),
|
||||
)
|
||||
|
||||
if (FeatureFlags.stories()) {
|
||||
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(R.string.ConversationListTabs__stories)
|
||||
|
||||
if (!SignalStore.storyValues().isFeatureDisabled) {
|
||||
customPref(
|
||||
PrivateStoryItem.RecipientModel(
|
||||
recipient = Recipient.self(),
|
||||
onClick = { findNavController().safeNavigate(R.id.action_privacySettings_to_myStorySettings) }
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(24f).toInt())
|
||||
|
||||
customPref(
|
||||
PrivateStoryItem.NewModel(
|
||||
onClick = {
|
||||
findNavController().safeNavigate(R.id.action_privacySettings_to_newPrivateStory)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
state.privateStories.forEach {
|
||||
customPref(
|
||||
PrivateStoryItem.PartialModel(
|
||||
privateStoryItemData = it,
|
||||
onClick = { model ->
|
||||
findNavController().safeNavigate(
|
||||
R.id.action_privacySettings_to_privateStorySettings,
|
||||
PrivateStorySettingsFragmentArgs.Builder(model.privateStoryItemData.id).build().toBundle()
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from(R.string.PrivacySettingsFragment__share_and_view_stories),
|
||||
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__you_will_no_longer_be_able),
|
||||
isChecked = state.isStoriesEnabled,
|
||||
onClick = {
|
||||
viewModel.setStoriesEnabled(!state.isStoriesEnabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
dividerPref()
|
||||
|
||||
clickPref(
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.privacy
|
||||
import android.content.Context
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -22,6 +23,12 @@ class PrivacySettingsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fun getPrivateStories(consumer: (List<DistributionListPartialRecord>) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(SignalDatabase.distributionLists.getCustomListsForUi())
|
||||
}
|
||||
}
|
||||
|
||||
fun syncReadReceiptState() {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
|
||||
data class PrivacySettingsState(
|
||||
@@ -15,5 +16,7 @@ data class PrivacySettingsState(
|
||||
val isObsoletePasswordEnabled: Boolean,
|
||||
val isObsoletePasswordTimeoutEnabled: Boolean,
|
||||
val obsoletePasswordTimeout: Int,
|
||||
val universalExpireTimer: Int
|
||||
val universalExpireTimer: Int,
|
||||
val privateStories: List<DistributionListPartialRecord>,
|
||||
val isStoriesEnabled: Boolean
|
||||
)
|
||||
|
||||
@@ -26,6 +26,11 @@ class PrivacySettingsViewModel(
|
||||
store.update { it.copy(blockedCount = count) }
|
||||
refresh()
|
||||
}
|
||||
|
||||
repository.getPrivateStories { privateStories ->
|
||||
store.update { it.copy(privateStories = privateStories) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun setReadReceiptsEnabled(enabled: Boolean) {
|
||||
@@ -83,6 +88,11 @@ class PrivacySettingsViewModel(
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setStoriesEnabled(isStoriesEnabled: Boolean) {
|
||||
SignalStore.storyValues().isFeatureDisabled = !isStoriesEnabled
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
store.update(this::updateState)
|
||||
}
|
||||
@@ -101,12 +111,14 @@ class PrivacySettingsViewModel(
|
||||
isObsoletePasswordEnabled = !TextSecurePreferences.isPasswordDisabled(ApplicationDependencies.getApplication()),
|
||||
isObsoletePasswordTimeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(ApplicationDependencies.getApplication()),
|
||||
obsoletePasswordTimeout = TextSecurePreferences.getPassphraseTimeoutInterval(ApplicationDependencies.getApplication()),
|
||||
universalExpireTimer = SignalStore.settings().universalExpireTimer
|
||||
universalExpireTimer = SignalStore.settings().universalExpireTimer,
|
||||
privateStories = emptyList(),
|
||||
isStoriesEnabled = !SignalStore.storyValues().isFeatureDisabled
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateState(state: PrivacySettingsState): PrivacySettingsState {
|
||||
return getState().copy(blockedCount = state.blockedCount)
|
||||
return getState().copy(blockedCount = state.blockedCount, privateStories = state.privateStories)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
|
||||
@@ -268,6 +268,9 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
recipient = state.recipient,
|
||||
onAvatarClick = { avatar ->
|
||||
if (!state.recipient.isSelf) {
|
||||
// startActivity(StoryViewerActivity.createIntent(requireContext(), state.recipient.id))
|
||||
|
||||
// TODO [stories] -- If recipient has a story, go to story viewer.
|
||||
requireActivity().apply {
|
||||
startActivity(
|
||||
AvatarPreviewActivity.intentFromRecipientId(this, state.recipient.id),
|
||||
|
||||
@@ -195,6 +195,8 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
colorize("SenderKey", recipient.senderKeyCapability),
|
||||
", ",
|
||||
colorize("ChangeNumber", recipient.changeNumberCapability),
|
||||
", ",
|
||||
colorize("Stories", recipient.storiesCapability),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ 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.avatar.view.AvatarView
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto
|
||||
@@ -39,7 +39,7 @@ object AvatarPreference {
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
private val avatar: AvatarImageView = itemView.findViewById<AvatarImageView>(R.id.bio_preference_avatar).apply {
|
||||
private val avatar: AvatarView = itemView.findViewById<AvatarView>(R.id.bio_preference_avatar).apply {
|
||||
setFallbackPhotoProvider(AvatarPreferenceFallbackPhotoProvider())
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ object AvatarPreference {
|
||||
}
|
||||
}
|
||||
|
||||
avatar.setAvatar(model.recipient)
|
||||
avatar.displayChatAvatar(model.recipient)
|
||||
avatar.disableQuickContact()
|
||||
avatar.setOnClickListener { model.onAvatarClick(avatar) }
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ object LargeIconClickPreference {
|
||||
class Model(
|
||||
override val title: DSLSettingsText?,
|
||||
override val icon: DSLSettingsIcon,
|
||||
override val summary: DSLSettingsText? = null,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<Model>()
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ fun configure(init: DSLConfiguration.() -> Unit): DSLConfiguration {
|
||||
}
|
||||
|
||||
class DSLConfiguration {
|
||||
private val children = arrayListOf<PreferenceModel<*>>()
|
||||
private val children = arrayListOf<MappingModel<*>>()
|
||||
|
||||
fun customPref(customPreference: PreferenceModel<*>) {
|
||||
fun customPref(customPreference: MappingModel<*>) {
|
||||
children.add(customPreference)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user