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:
Alex Hart
2022-02-24 13:40:28 -04:00
parent 765185952e
commit 174cd860a0
416 changed files with 19506 additions and 857 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,4 +20,5 @@ data class InternalSettingsState(
val removeSenderKeyMinimium: Boolean,
val delayResends: Boolean,
val disableStorageService: Boolean,
val disableStories: Boolean
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -195,6 +195,8 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
colorize("SenderKey", recipient.senderKeyCapability),
", ",
colorize("ChangeNumber", recipient.changeNumberCapability),
", ",
colorize("Stories", recipient.storiesCapability),
)
}

View File

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

View File

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

View File

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