Fix several issues with multiforwarding.

* Better forwarding and animations.
* Handle audio with text.
* Increase max forwardable count to 32
* Onboarding dialog.
* Send forth link previews.
* Safety number support.
* Fix slide behaviour.
fork-5.53.8
Alex Hart 2021-08-17 16:15:09 -03:00 zatwierdzone przez GitHub
rodzic 0b37b0ee16
commit c65761a034
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
22 zmienionych plików z 662 dodań i 117 usunięć

Wyświetl plik

@ -26,12 +26,9 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.UsernameUtil;
@ -48,15 +45,16 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
private static final String TAG = Log.tag(ContactsCursorLoader.class);
public static final class DisplayMode {
public static final int FLAG_PUSH = 1;
public static final int FLAG_SMS = 1 << 1;
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
public static final int FLAG_SELF = 1 << 4;
public static final int FLAG_BLOCK = 1 << 5;
public static final int FLAG_HIDE_GROUPS_V1 = 1 << 5;
public static final int FLAG_HIDE_NEW = 1 << 6;
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
public static final int FLAG_PUSH = 1;
public static final int FLAG_SMS = 1 << 1;
public static final int FLAG_ACTIVE_GROUPS = 1 << 2;
public static final int FLAG_INACTIVE_GROUPS = 1 << 3;
public static final int FLAG_SELF = 1 << 4;
public static final int FLAG_BLOCK = 1 << 5;
public static final int FLAG_HIDE_GROUPS_V1 = 1 << 5;
public static final int FLAG_HIDE_NEW = 1 << 6;
public static final int FLAG_HIDE_RECENT_HEADER = 1 << 7;
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
}
private static final int RECENT_CONVERSATION_MAX = 25;
@ -115,7 +113,9 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
Cursor recentConversations = getRecentConversationsCursor();
if (recentConversations.getCount() > 0) {
cursorList.add(ContactsCursorRows.forRecentsHeader(getContext()));
if (!hideRecentsHeader(mode)) {
cursorList.add(ContactsCursorRows.forRecentsHeader(getContext()));
}
cursorList.add(recentConversations);
}
}
@ -139,7 +139,9 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
Cursor groups = getRecentConversationsCursor(true);
if (groups.getCount() > 0) {
cursorList.add(ContactsCursorRows.forRecentsHeader(getContext()));
if (!hideRecentsHeader(mode)) {
cursorList.add(ContactsCursorRows.forRecentsHeader(getContext()));
}
cursorList.add(groups);
}
}
@ -279,6 +281,10 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
return flagSet(mode, DisplayMode.FLAG_HIDE_NEW);
}
private static boolean hideRecentsHeader(int mode) {
return flagSet(mode, DisplayMode.FLAG_HIDE_RECENT_HEADER);
}
private static boolean flagSet(int mode, int flag) {
return (mode & flag) > 0;
}

Wyświetl plik

@ -16,6 +16,7 @@
*/
package org.thoughtcrime.securesms.conversation;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
@ -567,6 +568,10 @@ public class ConversationAdapter
return new HashSet<>(selected);
}
public void removeFromSelection(@NonNull Set<MultiselectPart> parts) {
selected.removeAll(parts);
}
/**
* Clears all selected records from multi-select mode.
*/

Wyświetl plik

@ -89,6 +89,7 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderV
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.colors.ColorizerView;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemAnimator;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemDecoration;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
@ -169,6 +170,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@ -258,11 +260,33 @@ public class ConversationFragment extends LoggingFragment {
ConversationIntents.Args args = ConversationIntents.Args.from(requireActivity().getIntent());
colorizerView.setBackground(args.getChatColors().getChatBubbleMask());
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
final MultiselectItemAnimator multiselectItemAnimator = new MultiselectItemAnimator(() -> {
ConversationAdapter adapter = getListAdapter();
if (adapter == null) {
return false;
} else {
return Util.hasItems(adapter.getSelectedItems());
}
}, multiselectPart -> {
ConversationAdapter adapter = getListAdapter();
if (adapter == null) {
return false;
} else {
return adapter.getSelectedItems().contains(multiselectPart);
}
});
MultiselectItemDecoration multiselectItemDecoration = new MultiselectItemDecoration(requireContext(),
() -> conversationViewModel.getWallpaper().getValue(),
multiselectItemAnimator::getSelectedProgressForPart,
multiselectItemAnimator::isInitialAnimation);
list.setHasFixedSize(false);
list.setLayoutManager(layoutManager);
list.addItemDecoration(new MultiselectItemDecoration(requireContext(), () -> conversationViewModel.getWallpaper().getValue()));
list.setItemAnimator(null);
list.addItemDecoration(multiselectItemDecoration);
list.setItemAnimator(multiselectItemAnimator);
getViewLifecycleOwner().getLifecycle().addObserver(multiselectItemDecoration);
if (Build.VERSION.SDK_INT >= 31) {
list.setOverScrollMode(View.OVER_SCROLL_NEVER);
@ -675,6 +699,7 @@ public class ConversationFragment extends LoggingFragment {
ConversationAdapter.initializePool(list.getRecycledViewPool());
adapter.registerAdapterDataObserver(snapToTopDataObserver);
adapter.registerAdapterDataObserver(new CheckExpirationDataObserver());
setLastSeen(conversationViewModel.getLastSeen());
@ -1706,6 +1731,33 @@ public class ConversationFragment extends LoggingFragment {
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
}
private final class CheckExpirationDataObserver extends RecyclerView.AdapterDataObserver {
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
ConversationAdapter adapter = getListAdapter();
if (adapter == null || actionMode == null) {
return;
}
Set<MultiselectPart> selected = adapter.getSelectedItems();
Set<MultiselectPart> expired = new HashSet<>();
for (final MultiselectPart multiselectPart : selected) {
if (multiselectPart.isExpired()) {
expired.add(multiselectPart);
}
}
adapter.removeFromSelection(expired);
if (adapter.getSelectedItems().isEmpty()) {
actionMode.finish();
} else {
actionMode.setTitle(String.valueOf(adapter.getSelectedItems().size()));
}
}
}
private final class ConversationSnapToTopDataObserver extends SnapToTopDataObserver {
public ConversationSnapToTopDataObserver(@NonNull RecyclerView recyclerView,

Wyświetl plik

@ -24,7 +24,6 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Typeface;
@ -141,7 +140,6 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@ -533,63 +531,69 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
MultiselectPart bottom = parts.asDouble().getBottomPart();
if (hasThumbnail(messageRecord)) {
Projection thumbnailProjection = Projection.relativeToParent(this, mediaThumbnailStub.require(), null);
float mediaBoundary = thumbnailProjection.getY() + thumbnailProjection.getHeight();
if (lastYDownRelativeToThis > mediaBoundary) {
return bottom;
} else {
return top;
}
return isTouchBelowBoundary(mediaThumbnailStub.require()) ? bottom : top;
} else if (hasDocument(messageRecord)) {
Projection documentProjection = Projection.relativeToParent(this, documentViewStub.get(), null);
float documentBoundary = documentProjection.getY() + documentProjection.getHeight();
if (lastYDownRelativeToThis > documentBoundary) {
return bottom;
} else {
return top;
}
} else {
return isTouchBelowBoundary(documentViewStub.get()) ? bottom : top;
} else if (hasAudio(messageRecord)) {
return isTouchBelowBoundary(audioViewStub.get()) ? bottom : top;
} {
throw new IllegalStateException("Found a situation where we have something other than a thumbnail or a document.");
}
}
private boolean isTouchBelowBoundary(@NonNull View child) {
Projection childProjection = Projection.relativeToParent(this, child, null);
float childBoundary = childProjection.getY() + childProjection.getHeight();
return lastYDownRelativeToThis > childBoundary;
}
@Override
public int getTopBoundaryOfMultiselectPart(@NonNull MultiselectPart multiselectPart) {
if (multiselectPart instanceof MultiselectPart.Attachments && hasThumbnail(messageRecord)) {
Projection projection = Projection.relativeToViewRoot(mediaThumbnailStub.require(), null);
return (int) projection.getY();
} if (multiselectPart instanceof MultiselectPart.Attachments && hasDocument(messageRecord)) {
Projection projection = Projection.relativeToViewRoot(documentViewStub.get(), null);
return (int) projection.getY();
} else if (multiselectPart instanceof MultiselectPart.Text && hasThumbnail(messageRecord)) {
Projection projection = Projection.relativeToViewRoot(mediaThumbnailStub.require(), null);
return (int) projection.getY() + projection.getHeight();
} else if (multiselectPart instanceof MultiselectPart.Text && hasDocument(messageRecord)) {
Projection projection = Projection.relativeToViewRoot(documentViewStub.get(), null);
return (int) projection.getY() + projection.getHeight();
boolean isTextPart = multiselectPart instanceof MultiselectPart.Text;
boolean isAttachmentPart = multiselectPart instanceof MultiselectPart.Attachments;
if (hasThumbnail(messageRecord) && isAttachmentPart) {
return getProjectionTop(mediaThumbnailStub.require());
} else if (hasThumbnail(messageRecord) && isTextPart) {
return getProjectionBottom(mediaThumbnailStub.require());
} else if (hasDocument(messageRecord) && isAttachmentPart) {
return getProjectionTop(documentViewStub.get());
} else if (hasDocument(messageRecord) && isTextPart) {
return getProjectionBottom(documentViewStub.get());
} else if (hasAudio(messageRecord) && isAttachmentPart) {
return getProjectionTop(audioViewStub.get());
} else if (hasAudio(messageRecord) && isTextPart) {
return getProjectionBottom(audioViewStub.get());
} else if (hasNoBubble(messageRecord)) {
return getTop();
} else {
Projection projection = Projection.relativeToViewRoot(bodyBubble, null);
return (int) projection.getY();
return getProjectionTop(bodyBubble);
}
}
private static int getProjectionTop(@NonNull View child) {
return (int) Projection.relativeToViewRoot(child, null).getY();
}
private static int getProjectionBottom(@NonNull View child) {
Projection projection = Projection.relativeToViewRoot(child, null);
return (int) projection.getY() + projection.getHeight();
}
@Override
public int getBottomBoundaryOfMultiselectPart(@NonNull MultiselectPart multiselectPart) {
if (multiselectPart instanceof MultiselectPart.Attachments && hasThumbnail(messageRecord)) {
Projection projection = Projection.relativeToViewRoot(mediaThumbnailStub.require(), null);
return (int) projection.getY() + projection.getHeight();
return getProjectionBottom(mediaThumbnailStub.require());
} else if (multiselectPart instanceof MultiselectPart.Attachments && hasDocument(messageRecord)) {
Projection projection = Projection.relativeToViewRoot(documentViewStub.get(), null);
return (int) projection.getY() + projection.getHeight();
return getProjectionBottom(documentViewStub.get());
} else if (multiselectPart instanceof MultiselectPart.Attachments && hasAudio(messageRecord)) {
return getProjectionBottom(audioViewStub.get());
} else if (hasNoBubble(messageRecord)) {
return getBottom();
} else {
Projection projection = Projection.relativeToViewRoot(bodyBubble, null);
return (int) projection.getY() + projection.getHeight();
return getProjectionBottom(bodyBubble);
}
}
@ -1731,6 +1735,17 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
return projections;
}
@Override
public @Nullable View getHorizontalTranslationTarget() {
if (messageRecord.isOutgoing()) {
return null;
} else if (groupThread) {
return contactPhotoHolder;
} else {
return bodyBubble;
}
}
private class SharedContactEventListener implements SharedContactView.EventListener {
@Override
public void onAddToContactsClicked(@NonNull Contact contact) {

Wyświetl plik

@ -230,6 +230,11 @@ public final class ConversationUpdateItem extends FrameLayout
return Collections.emptyList();
}
@Override
public @Nullable View getHorizontalTranslationTarget() {
return null;
}
static final class RecipientObserverManager {
private final Observer<Recipient> recipientObserver;

Wyświetl plik

@ -14,6 +14,8 @@ import java.util.stream.Collectors;
final class MenuState {
private static final int MAX_FORWARDABLE_COUNT = 32;
private final boolean forward;
private final boolean reply;
private final boolean details;
@ -114,7 +116,7 @@ final class MenuState {
!viewOnce &&
!remoteDelete &&
!hasPendingMedia &&
((FeatureFlags.forwardMultipleMessages() && selectedParts.size() <= 5) || selectedParts.size() == 1);
((FeatureFlags.forwardMultipleMessages() && selectedParts.size() <= MAX_FORWARDABLE_COUNT) || selectedParts.size() == 1);
int uniqueRecords = selectedParts.stream()
.map(MultiselectPart::getMessageRecord)

Wyświetl plik

@ -35,6 +35,8 @@ sealed class MultiselectCollection {
}
}
fun isExpired(): Boolean = toSet().any(MultiselectPart::isExpired)
fun isTextSelected(selectedParts: Set<MultiselectPart>): Boolean {
val textParts: Set<MultiselectPart> = toSet().filter(this::couldContainText).toSet()

Wyświetl plik

@ -0,0 +1,154 @@
package org.thoughtcrime.securesms.conversation.mutiselect
import android.animation.ValueAnimator
import androidx.core.animation.doOnEnd
import androidx.recyclerview.widget.RecyclerView
/**
* Class for managing the triggering of item animations (here in the form of decoration redraws) whenever
* there is a "selection" edge detected.
*
* Can be expanded upon in the future to animate other things, such as message sends.
*/
class MultiselectItemAnimator(
private val isInMultiSelectMode: () -> Boolean,
private val isPartSelected: (MultiselectPart) -> Boolean
) : RecyclerView.ItemAnimator() {
private data class Selection(
val multiselectPart: MultiselectPart,
val viewHolder: RecyclerView.ViewHolder
)
var isInitialAnimation: Boolean = true
private set
private val selected: MutableSet<MultiselectPart> = mutableSetOf()
private val pendingSelectedAnimations: MutableSet<Selection> = mutableSetOf()
private val selectedAnimations: MutableMap<Selection, ValueAnimator> = mutableMapOf()
fun getSelectedProgressForPart(multiselectPart: MultiselectPart): Float {
return if (pendingSelectedAnimations.any { it.multiselectPart == multiselectPart }) {
0f
} else {
selectedAnimations.filter { it.key.multiselectPart == multiselectPart }.values.firstOrNull()?.animatedFraction ?: 1f
}
}
override fun animateDisappearance(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo?): Boolean {
dispatchAnimationFinished(viewHolder)
return false
}
override fun animateAppearance(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo?, postLayoutInfo: ItemHolderInfo): Boolean {
dispatchAnimationFinished(viewHolder)
return false
}
override fun animatePersistence(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo): Boolean {
dispatchAnimationFinished(viewHolder)
return false
}
override fun animateChange(oldHolder: RecyclerView.ViewHolder, newHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo, postLayoutInfo: ItemHolderInfo): Boolean {
if (oldHolder != newHolder) {
dispatchAnimationFinished(oldHolder)
}
val isInMultiSelectMode = isInMultiSelectMode()
if (!isInMultiSelectMode) {
selected.clear()
isInitialAnimation = true
dispatchAnimationFinished(newHolder)
return false
}
var isAnimationStarted = false
val parts: MultiselectCollection? = (newHolder.itemView as? Multiselectable)?.conversationMessage?.multiselectCollection
if (parts == null || parts.isExpired()) {
dispatchAnimationFinished(newHolder)
return false
}
parts.toSet().forEach { part ->
val partIsSelected = isPartSelected(part)
if (selected.contains(part) && !partIsSelected) {
pendingSelectedAnimations.add(Selection(part, newHolder))
selected.remove(part)
isAnimationStarted = true
} else if (!selected.contains(part) && partIsSelected) {
pendingSelectedAnimations.add(Selection(part, newHolder))
selected.add(part)
isAnimationStarted = true
} else if (isInitialAnimation) {
pendingSelectedAnimations.add(Selection(part, newHolder))
isAnimationStarted = true
}
}
if (isAnimationStarted) {
dispatchAnimationStarted(newHolder)
} else {
dispatchAnimationFinished(newHolder)
}
return isAnimationStarted
}
override fun runPendingAnimations() {
for (selection in pendingSelectedAnimations) {
val animator = ValueAnimator.ofFloat(0f, 1f)
selectedAnimations[selection] = animator
animator.duration = 150L
animator.addUpdateListener {
(selection.viewHolder.itemView.parent as RecyclerView).invalidateItemDecorations()
}
animator.doOnEnd {
dispatchAnimationFinished(selection.viewHolder)
selectedAnimations.remove(selection)
isInitialAnimation = false
}
animator.start()
}
pendingSelectedAnimations.clear()
}
override fun endAnimation(item: RecyclerView.ViewHolder) {
endSelectedAnimation(item)
}
override fun endAnimations() {
endSelectedAnimations()
dispatchAnimationsFinished()
}
override fun isRunning(): Boolean {
return selectedAnimations.values.any { it.isRunning }
}
override fun onAnimationFinished(viewHolder: RecyclerView.ViewHolder) {
dispatchItemDecorationRedraw(viewHolder)
}
private fun dispatchItemDecorationRedraw(viewHolder: RecyclerView.ViewHolder) {
val parent = (viewHolder.itemView.parent as RecyclerView)
parent.post { parent.invalidateItemDecorations() }
}
private fun endSelectedAnimation(item: RecyclerView.ViewHolder) {
val selections = selectedAnimations.filter { (k, _) -> k.viewHolder == item }
selections.forEach { (k, v) ->
v.end()
selectedAnimations.remove(k)
}
}
fun endSelectedAnimations() {
selectedAnimations.values.forEach { it.end() }
selectedAnimations.clear()
}
}

Wyświetl plik

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.mutiselect
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
@ -11,6 +12,8 @@ import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.view.children
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.SimpleColorFilter
import org.thoughtcrime.securesms.R
@ -20,11 +23,17 @@ import org.thoughtcrime.securesms.util.SetUtil
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import java.lang.Integer.max
/**
* Decoration which renders the background shade and selection bubble for a {@link Multiselectable} item.
*/
class MultiselectItemDecoration(context: Context, private val chatWallpaperProvider: () -> ChatWallpaper?) : RecyclerView.ItemDecoration() {
class MultiselectItemDecoration(
context: Context,
private val chatWallpaperProvider: () -> ChatWallpaper?,
private val selectedAnimationProgressProvider: (MultiselectPart) -> Float,
private val isInitialAnimation: () -> Boolean
) : RecyclerView.ItemDecoration(), DefaultLifecycleObserver {
private val path = Path()
private val rect = Rect()
@ -43,6 +52,21 @@ class MultiselectItemDecoration(context: Context, private val chatWallpaperProvi
private val ultramarine30 = ContextCompat.getColor(context, R.color.core_ultramarine_33)
private val ultramarine = ContextCompat.getColor(context, R.color.signal_accent_primary)
private var checkedBitmap: Bitmap? = null
override fun onCreate(owner: LifecycleOwner) {
val bitmap = Bitmap.createBitmap(circleRadius * 2, circleRadius * 2, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
checkDrawable.draw(canvas)
checkedBitmap = bitmap
}
override fun onDestroy(owner: LifecycleOwner) {
checkedBitmap?.recycle()
checkedBitmap = null
}
private val unselectedPaint = Paint().apply {
isAntiAlias = true
strokeWidth = 1.5f
@ -60,20 +84,43 @@ class MultiselectItemDecoration(context: Context, private val chatWallpaperProvi
color = transparentBlack20
}
private val checkPaint = Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val adapter = parent.adapter as ConversationAdapter
val isLtr = ViewUtil.isLtr(view)
if (adapter.selectedItems.isNotEmpty() && view is Multiselectable) {
outRect.set(
if (isLtr) gutter else 0,
0,
if (isLtr) 0 else gutter,
0
)
} else {
outRect.setEmpty()
val firstPart = view.conversationMessage.multiselectCollection.toSet().first()
val target = view.getHorizontalTranslationTarget()
if (target != null) {
val start = if (isLtr) {
target.left
} else {
parent.right - target.right
}
val translation: Float = if (isInitialAnimation()) {
max(0, gutter - start) * selectedAnimationProgressProvider(firstPart)
} else {
max(0, gutter - start).toFloat()
}
view.translationX = if (isLtr) {
translation
} else {
-translation
}
}
} else if (view is Multiselectable) {
view.translationX = 0f
}
outRect.setEmpty()
}
/**
@ -111,7 +158,7 @@ class MultiselectItemDecoration(context: Context, private val chatWallpaperProvi
val shadeAll = selectedParts.size == parts.size || (selectedPart is MultiselectPart.Text && child.hasNonSelectableMedia())
if (shadeAll) {
rect.set(0, view.top, parent.right, view.bottom)
rect.set(0, view.top, view.right, view.bottom)
} else {
rect.set(0, child.getTopBoundaryOfMultiselectPart(selectedPart), parent.right, child.getBottomBoundaryOfMultiselectPart(selectedPart))
}
@ -144,9 +191,9 @@ class MultiselectItemDecoration(context: Context, private val chatWallpaperProvi
}
if (chatWallpaperProvider() == null && !isDarkTheme) {
checkDrawable.colorFilter = SimpleColorFilter(ultramarine)
checkPaint.colorFilter = SimpleColorFilter(ultramarine)
} else {
checkDrawable.clearColorFilter()
checkPaint.colorFilter = null
}
multiselectChildren.forEach { child ->
@ -159,10 +206,15 @@ class MultiselectItemDecoration(context: Context, private val chatWallpaperProvi
drawPhotoCircle(canvas, parent, topBoundary, bottomBoundary)
}
val alphaProgress = selectedAnimationProgressProvider(it)
if (adapter.selectedItems.contains(it)) {
drawSelectedCircle(canvas, parent, topBoundary, bottomBoundary)
drawUnselectedCircle(canvas, parent, topBoundary, bottomBoundary, 1f - alphaProgress)
drawSelectedCircle(canvas, parent, topBoundary, bottomBoundary, alphaProgress)
} else {
drawUnselectedCircle(canvas, parent, topBoundary, bottomBoundary)
drawUnselectedCircle(canvas, parent, topBoundary, bottomBoundary, alphaProgress)
if (!isInitialAnimation()) {
drawSelectedCircle(canvas, parent, topBoundary, bottomBoundary, 1f - alphaProgress)
}
}
}
}
@ -187,7 +239,7 @@ class MultiselectItemDecoration(context: Context, private val chatWallpaperProvi
/**
* Draws the checkmark for selected content
*/
private fun drawSelectedCircle(canvas: Canvas, parent: RecyclerView, topBoundary: Int, bottomBoundary: Int) {
private fun drawSelectedCircle(canvas: Canvas, parent: RecyclerView, topBoundary: Int, bottomBoundary: Int, alphaProgress: Float) {
val topX: Float = if (ViewUtil.isLtr(parent)) {
paddingStart
} else {
@ -195,25 +247,33 @@ class MultiselectItemDecoration(context: Context, private val chatWallpaperProvi
}.toFloat()
val topY: Float = topBoundary + (bottomBoundary - topBoundary).toFloat() / 2 - circleRadius
val bitmap = checkedBitmap
canvas.save()
canvas.translate(topX, topY)
checkDrawable.draw(canvas)
canvas.restore()
val alpha = checkPaint.alpha
checkPaint.alpha = (alpha * alphaProgress).toInt()
if (bitmap != null) {
canvas.drawBitmap(bitmap, topX, topY, checkPaint)
}
checkPaint.alpha = alpha
}
/**
* Draws the empty circle for unselected content
*/
private fun drawUnselectedCircle(c: Canvas, parent: RecyclerView, topBoundary: Int, bottomBoundary: Int) {
private fun drawUnselectedCircle(c: Canvas, parent: RecyclerView, topBoundary: Int, bottomBoundary: Int, alphaProgress: Float) {
val centerX: Float = if (ViewUtil.isLtr(parent)) {
paddingStart + circleRadius
} else {
parent.right - circleRadius - paddingStart
}.toFloat()
val alpha = unselectedPaint.alpha
unselectedPaint.alpha = (alpha * alphaProgress).toInt()
val centerY: Float = topBoundary + (bottomBoundary - topBoundary).toFloat() / 2
c.drawCircle(centerX, centerY, circleRadius.toFloat(), unselectedPaint)
unselectedPaint.alpha = alpha
}
}

Wyświetl plik

@ -10,6 +10,12 @@ sealed class MultiselectPart(open val conversationMessage: ConversationMessage)
fun getMessageRecord(): MessageRecord = conversationMessage.messageRecord
fun isExpired(): Boolean {
val expiresAt = conversationMessage.messageRecord.expireStarted + conversationMessage.messageRecord.expiresIn
return expiresAt > 0 && expiresAt < System.currentTimeMillis()
}
/**
* Represents the body of the message
*/

Wyświetl plik

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.mutiselect
import android.view.View
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.Colorizable
@ -12,5 +13,7 @@ interface Multiselectable : Colorizable {
fun getMultiselectPartForLatestTouch(): MultiselectPart
fun getHorizontalTranslationTarget(): View?
fun hasNonSelectableMedia(): Boolean
}

Wyświetl plik

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.conversation.mutiselect.forward
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -8,18 +10,23 @@ import android.view.animation.AnimationUtils
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.Toast
import androidx.annotation.PluralsRes
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.ContactSelectionListFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ContactFilterView
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.thoughtcrime.securesms.sharing.ShareSelectionAdapter
@ -30,13 +37,18 @@ import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
import org.thoughtcrime.securesms.util.visible
import org.whispersystems.libsignal.util.guava.Optional
import java.lang.UnsupportedOperationException
import java.util.function.Consumer
private const val ARG_MULTISHARE_ARGS = "multiselect.forward.fragment.arg.multishare.args"
private const val ARG_CAN_SEND_TO_NON_PUSH = "multiselect.forward.fragment.arg.can.send.to.non.push"
private val TAG = Log.tag(MultiselectForwardFragment::class.java)
class MultiselectForwardFragment : FixedRoundedCornerBottomSheetDialogFragment(), ContactSelectionListFragment.OnContactSelectedListener, ContactSelectionListFragment.OnSelectionLimitReachedListener {
class MultiselectForwardFragment :
FixedRoundedCornerBottomSheetDialogFragment(),
ContactSelectionListFragment.OnContactSelectedListener,
ContactSelectionListFragment.OnSelectionLimitReachedListener,
SafetyNumberChangeDialog.Callback {
override val peekHeightPercentage: Float = 0.67f
@ -44,9 +56,12 @@ class MultiselectForwardFragment : FixedRoundedCornerBottomSheetDialogFragment()
private lateinit var selectionFragment: ContactSelectionListFragment
private lateinit var contactFilterView: ContactFilterView
private lateinit var addMessage: EditText
private var dismissibleDialog: SimpleProgressDialog.DismissibleDialog? = null
private var handler: Handler? = null
private fun createViewModelFactory(): MultiselectForwardViewModel.Factory {
return MultiselectForwardViewModel.Factory(getMultiShareArgs(), MultiselectForwardRepository(requireContext()))
}
@ -99,15 +114,14 @@ class MultiselectForwardFragment : FixedRoundedCornerBottomSheetDialogFragment()
val shareSelectionRecycler: RecyclerView = bottomBar.findViewById(R.id.selected_list)
val shareSelectionAdapter = ShareSelectionAdapter()
val sendButton: View = bottomBar.findViewById(R.id.share_confirm)
val addMessage: EditText = bottomBar.findViewById(R.id.add_message)
val addMessageWrapper: View = bottomBar.findViewById(R.id.add_message_wrapper)
addMessage = bottomBar.findViewById(R.id.add_message)
addMessageWrapper.visible = FeatureFlags.forwardMultipleMessages()
sendButton.setOnClickListener {
it.isEnabled = false
dismissibleDialog = SimpleProgressDialog.showDelayed(requireContext())
sendButton.isEnabled = false
viewModel.send(addMessage.text.toString())
}
@ -130,20 +144,22 @@ class MultiselectForwardFragment : FixedRoundedCornerBottomSheetDialogFragment()
}
viewModel.state.observe(viewLifecycleOwner) {
val toastTextResId: Int? = when (it.stage) {
MultiselectForwardState.Stage.SELECTION -> null
MultiselectForwardState.Stage.SOME_FAILED -> R.plurals.MultiselectForwardFragment_messages_sent
MultiselectForwardState.Stage.ALL_FAILED -> R.plurals.MultiselectForwardFragment_messages_failed_to_send
MultiselectForwardState.Stage.SUCCESS -> R.plurals.MultiselectForwardFragment_messages_sent
when (it.stage) {
MultiselectForwardState.Stage.Selection -> { }
MultiselectForwardState.Stage.FirstConfirmation -> displayFirstSendConfirmation()
is MultiselectForwardState.Stage.SafetyConfirmation -> displaySafetyNumberConfirmation(it.stage.identities)
MultiselectForwardState.Stage.LoadingIdentities -> {}
MultiselectForwardState.Stage.SendPending -> {
handler?.removeCallbacksAndMessages(null)
dismissibleDialog?.dismiss()
dismissibleDialog = SimpleProgressDialog.showDelayed(requireContext())
}
MultiselectForwardState.Stage.SomeFailed -> dismissAndShowToast(R.plurals.MultiselectForwardFragment_messages_sent)
MultiselectForwardState.Stage.AllFailed -> dismissAndShowToast(R.plurals.MultiselectForwardFragment_messages_failed_to_send)
MultiselectForwardState.Stage.Success -> dismissAndShowToast(R.plurals.MultiselectForwardFragment_messages_sent)
}
if (toastTextResId != null) {
val argCount = getMultiShareArgs().size
dismissibleDialog?.dismiss()
Toast.makeText(requireContext(), requireContext().resources.getQuantityString(toastTextResId, argCount), Toast.LENGTH_SHORT).show()
dismissAllowingStateLoss()
}
sendButton.isEnabled = it.stage == MultiselectForwardState.Stage.Selection
}
bottomBar.addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
@ -151,8 +167,75 @@ class MultiselectForwardFragment : FixedRoundedCornerBottomSheetDialogFragment()
}
}
override fun onResume() {
super.onResume()
val now = System.currentTimeMillis()
val expiringMessages = getMultiShareArgs().filter { it.expiresAt > 0L }
val firstToExpire = expiringMessages.minByOrNull { it.expiresAt }
val earliestExpiration = firstToExpire?.expiresAt ?: -1L
if (earliestExpiration > 0) {
if (earliestExpiration <= now) {
handleMessageExpired()
} else {
handler = Handler(Looper.getMainLooper())
handler?.postDelayed(this::handleMessageExpired, earliestExpiration - now)
}
}
}
override fun onPause() {
super.onPause()
handler?.removeCallbacksAndMessages(null)
}
private fun displayFirstSendConfirmation() {
SignalStore.tooltips().markMultiForwardDialogSeen()
val messageCount = getMessageCount()
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.MultiselectForwardFragment__faster_forwards)
.setMessage(R.string.MultiselectForwardFragment__forwarded_messages_are_now)
.setPositiveButton(resources.getQuantityString(R.plurals.MultiselectForwardFragment_send_d_messages, messageCount, messageCount)) { d, _ ->
d.dismiss()
viewModel.confirmFirstSend(addMessage.text.toString())
}
.setNegativeButton(android.R.string.cancel) { d, _ ->
d.dismiss()
viewModel.cancelSend()
}
.show()
}
private fun displaySafetyNumberConfirmation(identityRecords: List<IdentityDatabase.IdentityRecord>) {
SafetyNumberChangeDialog.show(childFragmentManager, identityRecords)
}
private fun dismissAndShowToast(@PluralsRes toastTextResId: Int) {
val argCount = getMessageCount()
dismissibleDialog?.dismiss()
Toast.makeText(requireContext(), requireContext().resources.getQuantityString(toastTextResId, argCount), Toast.LENGTH_SHORT).show()
dismissAllowingStateLoss()
}
private fun getMessageCount(): Int = getMultiShareArgs().size + if (addMessage.text.isNotEmpty()) 1 else 0
private fun handleMessageExpired() {
dismissAllowingStateLoss()
dismissibleDialog?.dismiss()
Toast.makeText(requireContext(), resources.getQuantityString(R.plurals.MultiselectForwardFragment__couldnt_forward_messages, getMultiShareArgs().size), Toast.LENGTH_SHORT).show()
}
private fun getDefaultDisplayMode(): Int {
var mode = ContactsCursorLoader.DisplayMode.FLAG_PUSH or ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS or ContactsCursorLoader.DisplayMode.FLAG_SELF or ContactsCursorLoader.DisplayMode.FLAG_HIDE_NEW
var mode = ContactsCursorLoader.DisplayMode.FLAG_PUSH or
ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS or
ContactsCursorLoader.DisplayMode.FLAG_SELF or
ContactsCursorLoader.DisplayMode.FLAG_HIDE_NEW or
ContactsCursorLoader.DisplayMode.FLAG_HIDE_RECENT_HEADER
if (Util.isDefaultSmsProvider(requireContext()) && requireArguments().getBoolean(ARG_CAN_SEND_TO_NON_PUSH)) {
mode = mode or ContactsCursorLoader.DisplayMode.FLAG_SMS
@ -186,6 +269,18 @@ class MultiselectForwardFragment : FixedRoundedCornerBottomSheetDialogFragment()
Toast.makeText(requireContext(), R.string.MultiselectForwardFragment__limit_reached, Toast.LENGTH_SHORT).show()
}
override fun onSendAnywayAfterSafetyNumberChange(changedRecipients: MutableList<RecipientId>) {
viewModel.confirmSafetySend(addMessage.text.toString())
}
override fun onMessageResentAfterSafetyNumberChange() {
throw UnsupportedOperationException()
}
override fun onCanceled() {
viewModel.cancelSend()
}
companion object {
@JvmStatic
fun show(supportFragmentManager: FragmentManager, multiselectForwardFragmentArgs: MultiselectForwardFragmentArgs) {

Wyświetl plik

@ -42,7 +42,10 @@ class MultiselectForwardFragmentArgs(
@WorkerThread
private fun buildMultiShareArgs(context: Context, conversationMessage: ConversationMessage, selectedParts: Set<MultiselectPart>): MultiShareArgs {
val builder = MultiShareArgs.Builder(setOf()).withMentions(conversationMessage.mentions)
val builder = MultiShareArgs.Builder(setOf())
.withMentions(conversationMessage.mentions)
.withTimestamp(conversationMessage.messageRecord.timestamp)
.withExpiration(conversationMessage.messageRecord.expireStarted + conversationMessage.messageRecord.expiresIn)
if (conversationMessage.multiselectCollection.isTextSelected(selectedParts)) {
val mediaMessage: MmsMessageRecord? = conversationMessage.messageRecord as? MmsMessageRecord
@ -56,6 +59,9 @@ class MultiselectForwardFragmentArgs(
} else {
builder.withDraftText(conversationMessage.getDisplayBody(context).toString())
}
val linkPreview = mediaMessage?.linkPreviews?.firstOrNull()
builder.withLinkPreview(linkPreview)
}
if (conversationMessage.messageRecord.isMms && conversationMessage.multiselectCollection.isMediaSelected(selectedParts)) {
@ -96,6 +102,7 @@ class MultiselectForwardFragmentArgs(
val media = firstSlide.asAttachment().toMedia()
if (media != null) {
builder.asBorderless(media.isBorderless)
builder.withMedia(listOf(media))
}
}

Wyświetl plik

@ -1,9 +1,12 @@
package org.thoughtcrime.securesms.conversation.mutiselect.forward
import android.content.Context
import androidx.core.util.Consumer
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.thoughtcrime.securesms.sharing.MultiShareSender
@ -20,6 +23,16 @@ class MultiselectForwardRepository(context: Context) {
val onAllMessagesFailed: () -> Unit
)
fun checkForBadIdentityRecords(shareContacts: List<ShareContact>, consumer: Consumer<List<IdentityDatabase.IdentityRecord>>) {
SignalExecutors.BOUNDED.execute {
val identityDatabase: IdentityDatabase = DatabaseFactory.getIdentityDatabase(context)
val recipients: List<Recipient> = shareContacts.map { Recipient.resolved(it.recipientId.get()) }
val identityRecordList: IdentityRecordList = identityDatabase.getIdentities(recipients)
consumer.accept(identityRecordList.untrustedRecords)
}
}
fun send(
additionalMessage: String,
multiShareArgs: List<MultiShareArgs>,
@ -38,7 +51,7 @@ class MultiselectForwardRepository(context: Context) {
.toSet()
val mappedArgs: List<MultiShareArgs> = multiShareArgs.map { it.buildUpon(sharedContactsAndThreads).build() }
val results = mappedArgs.map { MultiShareSender.sendSync(it) }
val results = mappedArgs.sortedBy { it.timestamp }.map { MultiShareSender.sendSync(it) }
if (additionalMessage.isNotEmpty()) {
val additional = MultiShareArgs.Builder(sharedContactsAndThreads)

Wyświetl plik

@ -1,15 +1,20 @@
package org.thoughtcrime.securesms.conversation.mutiselect.forward
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.sharing.ShareContact
data class MultiselectForwardState(
val selectedContacts: List<ShareContact> = emptyList(),
val stage: Stage = Stage.SELECTION
val stage: Stage = Stage.Selection
) {
enum class Stage {
SELECTION,
SOME_FAILED,
ALL_FAILED,
SUCCESS
sealed class Stage {
object Selection : Stage()
object FirstConfirmation : Stage()
object LoadingIdentities : Stage()
data class SafetyConfirmation(val identities: List<IdentityDatabase.IdentityRecord>) : Stage()
object SendPending : Stage()
object SomeFailed : Stage()
object AllFailed : Stage()
object Success : Stage()
}
}

Wyświetl plik

@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.thoughtcrime.securesms.sharing.ShareContact
@ -31,14 +32,43 @@ class MultiselectForwardViewModel(
}
fun send(additionalMessage: String) {
if (SignalStore.tooltips().showMultiForwardDialog()) {
SignalStore.tooltips().markMultiForwardDialogSeen()
store.update { it.copy(stage = MultiselectForwardState.Stage.FirstConfirmation) }
} else {
store.update { it.copy(stage = MultiselectForwardState.Stage.LoadingIdentities) }
repository.checkForBadIdentityRecords(store.state.selectedContacts) { identityRecords ->
if (identityRecords.isEmpty()) {
performSend(additionalMessage)
} else {
store.update { it.copy(stage = MultiselectForwardState.Stage.SafetyConfirmation(identityRecords)) }
}
}
}
}
fun confirmFirstSend(additionalMessage: String) {
send(additionalMessage)
}
fun confirmSafetySend(additionalMessage: String) {
send(additionalMessage)
}
fun cancelSend() {
store.update { it.copy(stage = MultiselectForwardState.Stage.Selection) }
}
private fun performSend(additionalMessage: String) {
store.update { it.copy(stage = MultiselectForwardState.Stage.SendPending) }
repository.send(
additionalMessage = additionalMessage,
multiShareArgs = records,
shareContacts = store.state.selectedContacts,
MultiselectForwardRepository.MultiselectForwardResultHandlers(
onAllMessageSentSuccessfully = { store.update { it.copy(stage = MultiselectForwardState.Stage.SUCCESS) } },
onAllMessagesFailed = { store.update { it.copy(stage = MultiselectForwardState.Stage.ALL_FAILED) } },
onSomeMessagesFailed = { store.update { it.copy(stage = MultiselectForwardState.Stage.SOME_FAILED) } }
onAllMessageSentSuccessfully = { store.update { it.copy(stage = MultiselectForwardState.Stage.Success) } },
onAllMessagesFailed = { store.update { it.copy(stage = MultiselectForwardState.Stage.AllFailed) } },
onSomeMessagesFailed = { store.update { it.copy(stage = MultiselectForwardState.Stage.SomeFailed) } }
)
)
}

Wyświetl plik

@ -214,7 +214,12 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
if (activity instanceof Callback && !skipCallbacks) {
callback = (Callback) activity;
} else {
callback = null;
Fragment parent = getParentFragment();
if (parent instanceof Callback && !skipCallbacks) {
callback = (Callback) parent;
} else {
callback = null;
}
}
LiveData<TrustAndVerifyResult> trustOrVerifyResultLiveData = viewModel.trustOrVerifyChangedRecipients();
@ -244,6 +249,8 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
private void handleCancel(@NonNull DialogInterface dialogInterface, int which) {
if (getActivity() instanceof Callback) {
((Callback) getActivity()).onCanceled();
} else if (getParentFragment() instanceof Callback) {
((Callback) getParentFragment()).onCanceled();
}
}

Wyświetl plik

@ -12,6 +12,7 @@ public class TooltipValues extends SignalStoreValues {
private static final String BLUR_HUD_ICON = "tooltip.blur_hud_icon";
private static final String GROUP_CALL_SPEAKER_VIEW = "tooltip.group_call_speaker_view";
private static final String GROUP_CALL_TOOLTIP_DISPLAY_COUNT = "tooltip.group_call_tooltip_display_count";
private static final String MULTI_FORWARD_DIALOG = "tooltip.multi.forward.dialog";
TooltipValues(@NonNull KeyValueStore store) {
@ -20,6 +21,7 @@ public class TooltipValues extends SignalStoreValues {
@Override
public void onFirstEverAppLaunch() {
markMultiForwardDialogSeen();
}
@Override
@ -54,4 +56,12 @@ public class TooltipValues extends SignalStoreValues {
public void markGroupCallingLobbyEntered() {
putInteger(GROUP_CALL_TOOLTIP_DISPLAY_COUNT, Integer.MAX_VALUE);
}
public boolean showMultiForwardDialog() {
return getBoolean(MULTI_FORWARD_DIALOG, true);
}
public void markMultiForwardDialogSeen() {
putBoolean(MULTI_FORWARD_DIALOG, false);
}
}

Wyświetl plik

@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.stickers.StickerLocator;
@ -35,6 +36,8 @@ public final class MultiShareArgs implements Parcelable {
private final boolean viewOnce;
private final LinkPreview linkPreview;
private final List<Mention> mentions;
private final long timestamp;
private final long expiresAt;
private MultiShareArgs(@NonNull Builder builder) {
shareContactAndThreads = builder.shareContactAndThreads;
@ -47,6 +50,8 @@ public final class MultiShareArgs implements Parcelable {
viewOnce = builder.viewOnce;
linkPreview = builder.linkPreview;
mentions = builder.mentions == null ? new ArrayList<>() : new ArrayList<>(builder.mentions);
timestamp = builder.timestamp;
expiresAt = builder.expiresAt;
}
protected MultiShareArgs(Parcel in) {
@ -59,6 +64,8 @@ public final class MultiShareArgs implements Parcelable {
dataType = in.readString();
viewOnce = in.readByte() != 0;
mentions = in.createTypedArrayList(Mention.CREATOR);
timestamp = in.readLong();
expiresAt = in.readLong();
String linkedPreviewString = in.readString();
LinkPreview preview;
@ -111,6 +118,14 @@ public final class MultiShareArgs implements Parcelable {
return mentions;
}
public long getTimestamp() {
return timestamp;
}
public long getExpiresAt() {
return expiresAt;
}
public @NonNull InterstitialContentType getInterstitialContentType() {
if (!requiresInterstitial()) {
return InterstitialContentType.NONE;
@ -154,6 +169,8 @@ public final class MultiShareArgs implements Parcelable {
dest.writeString(dataType);
dest.writeByte((byte) (viewOnce ? 1 : 0));
dest.writeTypedList(mentions);
dest.writeLong(timestamp);
dest.writeLong(expiresAt);
if (linkPreview != null) {
try {
@ -179,7 +196,9 @@ public final class MultiShareArgs implements Parcelable {
.withLinkPreview(linkPreview)
.withMedia(media)
.withStickerLocator(stickerLocator)
.withMentions(mentions);
.withMentions(mentions)
.withTimestamp(timestamp)
.withExpiration(expiresAt);
}
private boolean requiresInterstitial() {
@ -200,6 +219,8 @@ public final class MultiShareArgs implements Parcelable {
private LinkPreview linkPreview;
private boolean viewOnce;
private List<Mention> mentions;
private long timestamp;
private long expiresAt;
public Builder(@NonNull Set<ShareContactAndThread> shareContactAndThreads) {
this.shareContactAndThreads = shareContactAndThreads;
@ -250,6 +271,16 @@ public final class MultiShareArgs implements Parcelable {
return this;
}
public @NonNull Builder withTimestamp(long timestamp) {
this.timestamp = timestamp;
return this;
}
public @NonNull Builder withExpiration(long expiresAt) {
this.expiresAt = expiresAt;
return this;
}
public @NonNull MultiShareArgs build() {
return new MultiShareArgs(this);
}

Wyświetl plik

@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.SlideFactory;
import org.thoughtcrime.securesms.mms.StickerSlide;
@ -58,12 +59,23 @@ public final class MultiShareSender {
@WorkerThread
public static MultiShareSendResultCollection sendSync(@NonNull MultiShareArgs multiShareArgs) {
Context context = ApplicationDependencies.getApplication();
boolean isMmsEnabled = Util.isMmsCapable(context);
String message = multiShareArgs.getDraftText();
SlideDeck slideDeck = buildSlideDeck(context, multiShareArgs);
List<MultiShareSendResult> results = new ArrayList<>(multiShareArgs.getShareContactAndThreads().size());
Context context = ApplicationDependencies.getApplication();
boolean isMmsEnabled = Util.isMmsCapable(context);
String message = multiShareArgs.getDraftText();
SlideDeck slideDeck;
try {
slideDeck = buildSlideDeck(context, multiShareArgs);
} catch (SlideNotFoundException e) {
Log.w(TAG, "Could not create slide for media message");
for (ShareContactAndThread shareContactAndThread : multiShareArgs.getShareContactAndThreads()) {
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.GENERIC_ERROR));
}
return new MultiShareSendResultCollection(results);
}
List<MultiShareSendResult> results = new ArrayList<>(multiShareArgs.getShareContactAndThreads().size());
for (ShareContactAndThread shareContactAndThread : multiShareArgs.getShareContactAndThreads()) {
Recipient recipient = Recipient.resolved(shareContactAndThread.getRecipientId());
@ -91,7 +103,7 @@ public final class MultiShareSender {
sendMediaMessage(context, multiShareArgs, recipient, slideDeck, transport, shareContactAndThread.getThreadId(), forceSms, expiresIn, multiShareArgs.isViewOnce(), subscriptionId, mentions);
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.SUCCESS));
} else {
sendTextMessage(context, multiShareArgs, recipient, shareContactAndThread.getThreadId() ,forceSms, expiresIn, subscriptionId);
sendTextMessage(context, multiShareArgs, recipient, shareContactAndThread.getThreadId(), forceSms, expiresIn, subscriptionId);
results.add(new MultiShareSendResult(shareContactAndThread, MultiShareSendResult.Type.SUCCESS));
}
@ -180,16 +192,26 @@ public final class MultiShareSender {
MessageSender.send(context, outgoingTextMessage, threadId, forceSms, null);
}
private static @NonNull SlideDeck buildSlideDeck(@NonNull Context context, @NonNull MultiShareArgs multiShareArgs) {
private static @NonNull SlideDeck buildSlideDeck(@NonNull Context context, @NonNull MultiShareArgs multiShareArgs) throws SlideNotFoundException {
SlideDeck slideDeck = new SlideDeck();
if (multiShareArgs.getStickerLocator() != null) {
slideDeck.addSlide(new StickerSlide(context, multiShareArgs.getDataUri(), 0, multiShareArgs.getStickerLocator(), multiShareArgs.getDataType()));
} else if (!multiShareArgs.getMedia().isEmpty()) {
for (Media media : multiShareArgs.getMedia()) {
slideDeck.addSlide(SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight()));
Slide slide = SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight());
if (slide != null) {
slideDeck.addSlide(slide);
} else {
throw new SlideNotFoundException();
}
}
} else if (multiShareArgs.getDataUri() != null) {
slideDeck.addSlide(SlideFactory.getSlide(context, multiShareArgs.getDataType(), multiShareArgs.getDataUri(), 0, 0));
Slide slide = SlideFactory.getSlide(context, multiShareArgs.getDataType(), multiShareArgs.getDataUri(), 0, 0);
if (slide != null) {
slideDeck.addSlide(slide);
} else {
throw new SlideNotFoundException();
}
}
return slideDeck;
@ -244,8 +266,12 @@ public final class MultiShareSender {
}
private enum Type {
GENERIC_ERROR,
MMS_NOT_ENABLED,
SUCCESS
}
}
private static final class SlideNotFoundException extends Exception {
}
}

Wyświetl plik

@ -47,6 +47,7 @@
android:layout_marginBottom="8dp"
android:background="@drawable/rounded_rectangle_secondary"
android:hint="@string/MultiselectForwardFragment__add_a_message"
android:inputType="textCapSentences"
android:minHeight="44dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"

Wyświetl plik

@ -3691,6 +3691,12 @@
<string name="DSLSettingsToolbar__navigate_up">Navigate up</string>
<string name="MultiselectForwardFragment__forward_to">Forward to</string>
<string name="MultiselectForwardFragment__add_a_message">Add a message</string>
<string name="MultiselectForwardFragment__faster_forwards">Faster forwards</string>
<string name="MultiselectForwardFragment__forwarded_messages_are_now">Forwarded messages are now sent immediately.</string>
<plurals name="MultiselectForwardFragment_send_d_messages">
<item quantity="one">Send %1$d message</item>
<item quantity="other">Send %1$d messages</item>
</plurals>
<plurals name="MultiselectForwardFragment_messages_sent">
<item quantity="one">Message sent</item>
<item quantity="other">Messages sent</item>
@ -3699,6 +3705,10 @@
<item quantity="one">Message failed to send</item>
<item quantity="other">Messages failed to send</item>
</plurals>
<plurals name="MultiselectForwardFragment__couldnt_forward_messages">
<item quantity="one">Couldn\'t forward message because it\'s no longer available.</item>
<item quantity="other">Couldn\'t forward messages because they\'re no longer available.</item>
</plurals>
<string name="MultiselectForwardFragment__limit_reached">Limit reached</string>
<!-- EOF -->