Fixed a few bugs and some optimisations

Updated a number of nested layout components to be included instead of inflated
Added a couple of optimisations to the EmojiTextView
Fixed an issue where long conversation titles could squish the unread count
Fixed an issue where the typing indicator wasn't working on the home screen
This commit is contained in:
Morgan Pretty 2023-01-13 13:22:18 +11:00
parent 693c3a9656
commit 70f0dad36e
39 changed files with 625 additions and 1417 deletions

View File

@ -114,7 +114,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment()); Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
if (slide != null) { if (slide != null) {
thumbnailView.setImageResource(glideRequests, slide, false, false); thumbnailView.setImageResource(glideRequests, slide, false, null);
} }
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord)); thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));

View File

@ -1,158 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview;
import network.loki.messenger.R;
import okhttp3.HttpUrl;
public class LinkPreviewView extends FrameLayout {
private static final int TYPE_CONVERSATION = 0;
private static final int TYPE_COMPOSE = 1;
private ViewGroup container;
private OutlinedThumbnailView thumbnail;
private TextView title;
private TextView site;
private View divider;
private View closeButton;
private View spinner;
private int type;
private int defaultRadius;
private CornerMask cornerMask;
private Outliner outliner;
private CloseClickedListener closeClickedListener;
public LinkPreviewView(Context context) {
super(context);
init(null);
}
public LinkPreviewView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
private void init(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.link_preview, this);
container = findViewById(R.id.linkpreview_container);
thumbnail = findViewById(R.id.linkpreview_thumbnail);
title = findViewById(R.id.linkpreview_title);
site = findViewById(R.id.linkpreview_site);
divider = findViewById(R.id.linkpreview_divider);
spinner = findViewById(R.id.linkpreview_progress_wheel);
closeButton = findViewById(R.id.linkpreview_close);
defaultRadius = getResources().getDimensionPixelSize(R.dimen.thumbnail_default_radius);
cornerMask = new CornerMask(this);
outliner = new Outliner();
outliner.setColor(getResources().getColor(R.color.transparent));
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.LinkPreviewView, 0, 0);
type = typedArray.getInt(R.styleable.LinkPreviewView_linkpreview_type, 0);
typedArray.recycle();
}
if (type == TYPE_COMPOSE) {
container.setBackgroundColor(Color.TRANSPARENT);
container.setPadding(0, 0, 0, 0);
divider.setVisibility(VISIBLE);
closeButton.setOnClickListener(v -> {
if (closeClickedListener != null) {
closeClickedListener.onCloseClicked();
}
});
}
setWillNotDraw(false);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (type == TYPE_COMPOSE) return;
cornerMask.mask(canvas);
outliner.draw(canvas);
}
public void setLoading() {
title.setVisibility(GONE);
site.setVisibility(GONE);
thumbnail.setVisibility(GONE);
spinner.setVisibility(VISIBLE);
closeButton.setVisibility(GONE);
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showCloseButton) {
setLinkPreview(glideRequests, linkPreview, showThumbnail);
if (showCloseButton) {
closeButton.setVisibility(VISIBLE);
} else {
closeButton.setVisibility(GONE);
}
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
title.setVisibility(VISIBLE);
site.setVisibility(VISIBLE);
thumbnail.setVisibility(VISIBLE);
spinner.setVisibility(GONE);
closeButton.setVisibility(VISIBLE);
title.setText(linkPreview.getTitle());
HttpUrl url = HttpUrl.parse(linkPreview.getUrl());
if (url != null) {
site.setText(url.topPrivateDomain());
}
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
thumbnail.setVisibility(VISIBLE);
thumbnail.setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
thumbnail.showDownloadText(false);
} else {
thumbnail.setVisibility(GONE);
}
}
public void setCorners(int topLeft, int topRight) {
cornerMask.setRadii(topLeft, topRight, 0, 0);
outliner.setRadii(topLeft, topRight, 0, 0);
thumbnail.setCorners(topLeft, defaultRadius, defaultRadius, defaultRadius);
postInvalidate();
}
public void setCloseClickedListener(@Nullable CloseClickedListener closeClickedListener) {
this.closeClickedListener = closeClickedListener;
}
public void setDownloadClickedListener(SlidesClickedListener listener) {
thumbnail.setDownloadClickListener(listener);
}
public interface CloseClickedListener {
void onCloseClicked();
}
}

View File

@ -1,48 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import org.session.libsession.utilities.ThemeUtil;
import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView;
import network.loki.messenger.R;
public class OutlinedThumbnailView extends ThumbnailView {
private CornerMask cornerMask;
private Outliner outliner;
public OutlinedThumbnailView(Context context) {
super(context);
init();
}
public OutlinedThumbnailView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
cornerMask = new CornerMask(this);
outliner = new Outliner();
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
setWillNotDraw(false);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
cornerMask.mask(canvas);
outliner.draw(canvas);
}
public void setCorners(int topLeft, int topRight, int bottomRight, int bottomLeft) {
cornerMask.setRadii(topLeft, topRight, bottomRight, bottomLeft);
outliner.setRadii(topLeft, topRight, bottomRight, bottomLeft);
postInvalidate();
}
}

View File

@ -52,19 +52,4 @@ public class StickerView extends FrameLayout {
public void setOnLongClickListener(@Nullable OnLongClickListener l) { public void setOnLongClickListener(@Nullable OnLongClickListener l) {
image.setOnLongClickListener(l); image.setOnLongClickListener(l);
} }
public void setSticker(@NonNull GlideRequests glideRequests, @NonNull Slide stickerSlide) {
boolean showControls = stickerSlide.asAttachment().getDataUri() == null;
image.setImageResource(glideRequests, stickerSlide, showControls, false);
missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);
}
public void setThumbnailClickListener(@NonNull SlideClickListener listener) {
image.setThumbnailClickListener(listener);
}
public void setDownloadClickListener(@NonNull SlidesClickedListener listener) {
image.setDownloadClickListener(listener);
}
} }

View File

@ -24,7 +24,7 @@ public class EmojiTextView extends AppCompatTextView {
private static final char ELLIPSIS = '…'; private static final char ELLIPSIS = '…';
private CharSequence previousText; private CharSequence previousText;
private BufferType previousBufferType; private BufferType previousBufferType = BufferType.NORMAL;
private float originalFontSize; private float originalFontSize;
private boolean useSystemEmoji; private boolean useSystemEmoji;
private boolean sizeChangeInProgress; private boolean sizeChangeInProgress;
@ -49,6 +49,12 @@ public class EmojiTextView extends AppCompatTextView {
} }
@Override public void setText(@Nullable CharSequence text, BufferType type) { @Override public void setText(@Nullable CharSequence text, BufferType type) {
// No need to do anything special if the text is null or empty
if (text == null || text.length() == 0) {
super.setText(text, type);
return;
}
EmojiParser.CandidateList candidates = EmojiProvider.getCandidates(text); EmojiParser.CandidateList candidates = EmojiProvider.getCandidates(text);
if (scaleEmojis && candidates != null && candidates.allEmojis) { if (scaleEmojis && candidates != null && candidates.allEmojis) {
@ -149,8 +155,13 @@ public class EmojiTextView extends AppCompatTextView {
} }
private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) { private boolean unchanged(CharSequence text, CharSequence overflowText, BufferType bufferType) {
return Util.equals(previousText, text) && CharSequence finalPrevText = (previousText == null || previousText.length() == 0 ? "" : previousText);
Util.equals(previousOverflowText, overflowText) && CharSequence finalText = (text == null || text.length() == 0 ? "" : text);
CharSequence finalPrevOverflowText = (previousOverflowText == null || previousOverflowText.length() == 0 ? "" : previousOverflowText);
CharSequence finalOverflowText = (overflowText == null || overflowText.length() == 0 ? "" : overflowText);
return Util.equals(finalPrevText, finalText) &&
Util.equals(finalPrevOverflowText, finalOverflowText) &&
Util.equals(previousBufferType, bufferType) && Util.equals(previousBufferType, bufferType) &&
useSystemEmoji == useSystemEmoji() && useSystemEmoji == useSystemEmoji() &&
!sizeChangeInProgress; !sizeChangeInProgress;

View File

@ -8,6 +8,7 @@ import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -18,41 +19,28 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.MediaPreviewActivity import org.thoughtcrime.securesms.MediaPreviewActivity
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
class AlbumThumbnailView : FrameLayout { class AlbumThumbnailView : RelativeLayout {
private lateinit var binding: AlbumThumbnailViewBinding
companion object { companion object {
const val MAX_ALBUM_DISPLAY_SIZE = 3 const val MAX_ALBUM_DISPLAY_SIZE = 3
} }
private val binding: AlbumThumbnailViewBinding by lazy { AlbumThumbnailViewBinding.bind(this) }
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { constructor(context: Context) : super(context)
initialize() constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
} constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
initialize()
}
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initialize()
}
private val cornerMask by lazy { CornerMask(this) } private val cornerMask by lazy { CornerMask(this) }
private var slides: List<Slide> = listOf() private var slides: List<Slide> = listOf()
private var slideSize: Int = 0 private var slideSize: Int = 0
private fun initialize() {
binding = AlbumThumbnailViewBinding.inflate(LayoutInflater.from(context), this, true)
}
override fun dispatchDraw(canvas: Canvas?) { override fun dispatchDraw(canvas: Canvas?) {
super.dispatchDraw(canvas) super.dispatchDraw(canvas)
cornerMask.mask(canvas) cornerMask.mask(canvas)
@ -67,11 +55,11 @@ class AlbumThumbnailView : FrameLayout {
val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt) val eventRect = Rect(rawXInt, rawYInt, rawXInt, rawYInt)
val testRect = Rect() val testRect = Rect()
// test each album child // test each album child
binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed forEach@{ index, child ->
child.getGlobalVisibleRect(testRect) child.getGlobalVisibleRect(testRect)
if (testRect.contains(eventRect)) { if (testRect.contains(eventRect)) {
// hit intersects with this particular child // hit intersects with this particular child
val slide = slides.getOrNull(index) ?: return val slide = slides.getOrNull(index) ?: return@forEach
// only open to downloaded images // only open to downloaded images
if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) { if (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED) {
// Restart download here (on IO thread) // Restart download here (on IO thread)
@ -79,7 +67,7 @@ class AlbumThumbnailView : FrameLayout {
onAttachmentNeedsDownload(attachment.attachmentId.rowId, mms.getId()) onAttachmentNeedsDownload(attachment.attachmentId.rowId, mms.getId())
} }
} }
if (slide.isInProgress) return if (slide.isInProgress) return@forEach
ActivityDispatcher.get(context)?.dispatchIntent { context -> ActivityDispatcher.get(context)?.dispatchIntent { context ->
MediaPreviewActivity.getPreviewIntent(context, slide, mms, threadRecipient) MediaPreviewActivity.getPreviewIntent(context, slide, mms, threadRecipient)
@ -130,7 +118,7 @@ class AlbumThumbnailView : FrameLayout {
else -> R.layout.album_thumbnail_3 // three stacked with additional text else -> R.layout.album_thumbnail_3 // three stacked with additional text
} }
fun getThumbnailView(position: Int): KThumbnailView = when (position) { fun getThumbnailView(position: Int): ThumbnailView = when (position) {
0 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1) 0 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_1)
1 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2) 1 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_2)
2 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3) 2 -> binding.albumCellContainer.findViewById<ViewGroup>(R.id.albumCellContainer).findViewById(R.id.album_cell_3)

View File

@ -23,7 +23,7 @@ class LinkPreviewDraftView : LinearLayout {
// Start out with the loader showing and the content view hidden // Start out with the loader showing and the content view hidden
binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true) binding = ViewLinkPreviewDraftBinding.inflate(LayoutInflater.from(context), this, true)
binding.linkPreviewDraftContainer.isVisible = false binding.linkPreviewDraftContainer.isVisible = false
binding.thumbnailImageView.clipToOutline = true binding.thumbnailImageView.root.clipToOutline = true
binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() } binding.linkPreviewDraftCancelButton.setOnClickListener { cancel() }
} }
@ -31,10 +31,10 @@ class LinkPreviewDraftView : LinearLayout {
// Hide the loader and show the content view // Hide the loader and show the content view
binding.linkPreviewDraftContainer.isVisible = true binding.linkPreviewDraftContainer.isVisible = true
binding.linkPreviewDraftLoader.isVisible = false binding.linkPreviewDraftLoader.isVisible = false
binding.thumbnailImageView.radius = toPx(4, resources) binding.thumbnailImageView.root.radius = toPx(4, resources)
if (linkPreview.getThumbnail().isPresent) { if (linkPreview.getThumbnail().isPresent) {
// This internally fetches the thumbnail // This internally fetches the thumbnail
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, false) binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, null)
} }
binding.linkPreviewDraftTitleTextView.text = linkPreview.title binding.linkPreviewDraftTitleTextView.text = linkPreview.title
} }

View File

@ -1,118 +0,0 @@
package org.thoughtcrime.securesms.conversation.v2.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import network.loki.messenger.R;
public class TypingIndicatorView extends LinearLayout {
private boolean isActive;
private long startTime;
private static final long CYCLE_DURATION = 1500;
private static final long DOT_DURATION = 600;
private static final float MIN_ALPHA = 0.4f;
private static final float MIN_SCALE = 0.75f;
private View dot1;
private View dot2;
private View dot3;
public TypingIndicatorView(Context context) {
super(context);
initialize(null);
}
public TypingIndicatorView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize(attrs);
}
private void initialize(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.view_typing_indicator, this);
setWillNotDraw(false);
dot1 = findViewById(R.id.typing_dot1);
dot2 = findViewById(R.id.typing_dot2);
dot3 = findViewById(R.id.typing_dot3);
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.TypingIndicatorView, 0, 0);
int tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE);
typedArray.recycle();
dot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
dot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
dot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (!isActive) {
super.onDraw(canvas);
return;
}
long timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION;
render(dot1, timeInCycle, 0);
render(dot2, timeInCycle, 150);
render(dot3, timeInCycle, 300);
super.onDraw(canvas);
postInvalidate();
}
private void render(View dot, long timeInCycle, long start) {
long end = start + DOT_DURATION;
long peak = start + (DOT_DURATION / 2);
if (timeInCycle < start || timeInCycle > end) {
renderDefault(dot);
} else if (timeInCycle < peak) {
renderFadeIn(dot, timeInCycle, start);
} else {
renderFadeOut(dot, timeInCycle, peak);
}
}
private void renderDefault(View dot) {
dot.setAlpha(MIN_ALPHA);
dot.setScaleX(MIN_SCALE);
dot.setScaleY(MIN_SCALE);
}
private void renderFadeIn(View dot, long timeInCycle, long fadeInStart) {
float percent = (float) (timeInCycle - fadeInStart) / 300;
dot.setAlpha(MIN_ALPHA + (1 - MIN_ALPHA) * percent);
dot.setScaleX(MIN_SCALE + (1 - MIN_SCALE) * percent);
dot.setScaleY(MIN_SCALE + (1 - MIN_SCALE) * percent);
}
private void renderFadeOut(View dot, long timeInCycle, long fadeOutStart) {
float percent = (float) (timeInCycle - fadeOutStart) / 300;
dot.setAlpha(1 - (1 - MIN_ALPHA) * percent);
dot.setScaleX(1 - (1 - MIN_SCALE) * percent);
dot.setScaleY(1 - (1 - MIN_SCALE) * percent);
}
public void startAnimation() {
isActive = true;
startTime = System.currentTimeMillis();
postInvalidate();
}
public void stopAnimation() {
isActive = false;
}
}

View File

@ -0,0 +1,105 @@
package org.thoughtcrime.securesms.conversation.v2.components
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.PorterDuff
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewTypingIndicatorBinding
class TypingIndicatorView : LinearLayout {
companion object {
private const val CYCLE_DURATION: Long = 1500
private const val DOT_DURATION: Long = 600
private const val MIN_ALPHA = 0.4f
private const val MIN_SCALE = 0.75f
}
private val binding: ViewTypingIndicatorBinding by lazy {
val binding = ViewTypingIndicatorBinding.bind(this)
if (tint != -1) {
binding.typingDot1.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
binding.typingDot2.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
binding.typingDot3.getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY)
}
return@lazy binding
}
private var isActive = false
private var startTime: Long = 0
private var tint: Int = -1
constructor(context: Context) : super(context) { initialize(null) }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
private fun initialize(attrs: AttributeSet?) {
setWillNotDraw(false)
if (attrs != null) {
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.TypingIndicatorView, 0, 0)
this.tint = typedArray.getColor(R.styleable.TypingIndicatorView_typingIndicator_tint, Color.WHITE)
typedArray.recycle()
}
}
override fun onDraw(canvas: Canvas) {
if (!isActive) {
super.onDraw(canvas)
return
}
val timeInCycle = (System.currentTimeMillis() - startTime) % CYCLE_DURATION
render(binding.typingDot1, timeInCycle, 0)
render(binding.typingDot2, timeInCycle, 150)
render(binding.typingDot3, timeInCycle, 300)
super.onDraw(canvas)
postInvalidate()
}
private fun render(dot: View?, timeInCycle: Long, start: Long) {
val end = start + DOT_DURATION
val peak = start + DOT_DURATION / 2
if (timeInCycle < start || timeInCycle > end) {
renderDefault(dot)
} else if (timeInCycle < peak) {
renderFadeIn(dot, timeInCycle, start)
} else {
renderFadeOut(dot, timeInCycle, peak)
}
}
private fun renderDefault(dot: View?) {
dot!!.alpha = MIN_ALPHA
dot.scaleX = MIN_SCALE
dot.scaleY = MIN_SCALE
}
private fun renderFadeIn(dot: View?, timeInCycle: Long, fadeInStart: Long) {
val percent = (timeInCycle - fadeInStart).toFloat() / 300
dot!!.alpha = MIN_ALPHA + (1 - MIN_ALPHA) * percent
dot.scaleX = MIN_SCALE + (1 - MIN_SCALE) * percent
dot.scaleY = MIN_SCALE + (1 - MIN_SCALE) * percent
}
private fun renderFadeOut(dot: View?, timeInCycle: Long, fadeOutStart: Long) {
val percent = (timeInCycle - fadeOutStart).toFloat() / 300
dot!!.alpha = 1 - (1 - MIN_ALPHA) * percent
dot.scaleX = 1 - (1 - MIN_SCALE) * percent
dot.scaleY = 1 - (1 - MIN_SCALE) * percent
}
fun startAnimation() {
isActive = true
startTime = System.currentTimeMillis()
postInvalidate()
}
fun stopAnimation() {
isActive = false
}
}

View File

@ -19,7 +19,7 @@ class TypingIndicatorViewContainer : LinearLayout {
} }
fun setTypists(typists: List<Recipient>) { fun setTypists(typists: List<Recipient>) {
if (typists.isEmpty()) { binding.typingIndicator.stopAnimation(); return } if (typists.isEmpty()) { binding.typingIndicator.root.stopAnimation(); return }
binding.typingIndicator.startAnimation() binding.typingIndicator.root.startAnimation()
} }
} }

View File

@ -1,346 +0,0 @@
package org.thoughtcrime.securesms.conversation.v2.messages;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Group;
import androidx.core.content.ContextCompat;
import com.google.android.flexbox.FlexboxLayout;
import com.google.android.flexbox.JustifyContent;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.ThemeUtil;
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.conversation.v2.ViewUtil;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.util.NumberUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import network.loki.messenger.R;
public class EmojiReactionsView extends LinearLayout implements View.OnTouchListener {
// Normally 6dp, but we have 1dp left+right margin on the pills themselves
private final int OUTER_MARGIN = ViewUtil.dpToPx(2);
private static final int DEFAULT_THRESHOLD = 5;
private List<ReactionRecord> records;
private long messageId;
private ViewGroup container;
private Group showLess;
private VisibleMessageViewDelegate delegate;
private Handler gestureHandler = new Handler(Looper.getMainLooper());
private Runnable pressCallback;
private Runnable longPressCallback;
private long onDownTimestamp = 0;
private static long longPressDurationThreshold = 250;
private static long maxDoubleTapInterval = 200;
private boolean extended = false;
public EmojiReactionsView(Context context) {
super(context);
init(null);
}
public EmojiReactionsView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
private void init(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.view_emoji_reactions, this);
this.container = findViewById(R.id.layout_emoji_container);
this.showLess = findViewById(R.id.group_show_less);
records = new ArrayList<>();
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiReactionsView, 0, 0);
typedArray.recycle();
}
}
public void clear() {
this.records.clear();
container.removeAllViews();
}
public void setReactions(long messageId, @NonNull List<ReactionRecord> records, boolean outgoing, VisibleMessageViewDelegate delegate) {
this.delegate = delegate;
if (records.equals(this.records)) {
return;
}
FlexboxLayout containerLayout = (FlexboxLayout) this.container;
containerLayout.setJustifyContent(outgoing ? JustifyContent.FLEX_END : JustifyContent.FLEX_START);
this.records.clear();
this.records.addAll(records);
if (this.messageId != messageId) {
extended = false;
}
this.messageId = messageId;
displayReactions(extended ? Integer.MAX_VALUE : DEFAULT_THRESHOLD);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v.getTag() == null) return false;
Reaction reaction = (Reaction) v.getTag();
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) onDown(new MessageId(reaction.messageId, reaction.isMms));
else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback();
else if (action == MotionEvent.ACTION_UP) onUp(reaction);
return true;
}
private void displayReactions(int threshold) {
String userPublicKey = TextSecurePreferences.getLocalNumber(getContext());
List<Reaction> reactions = buildSortedReactionsList(records, userPublicKey, threshold);
container.removeAllViews();
LinearLayout overflowContainer = new LinearLayout(getContext());
overflowContainer.setOrientation(LinearLayout.HORIZONTAL);
int innerPadding = ViewUtil.dpToPx(4);
overflowContainer.setPaddingRelative(innerPadding,innerPadding,innerPadding,innerPadding);
int pixelSize = ViewUtil.dpToPx(1);
for (Reaction reaction : reactions) {
if (container.getChildCount() + 1 >= DEFAULT_THRESHOLD && threshold != Integer.MAX_VALUE && reactions.size() > threshold) {
if (overflowContainer.getParent() == null) {
container.addView(overflowContainer);
MarginLayoutParams overflowParams = (MarginLayoutParams) overflowContainer.getLayoutParams();
overflowParams.height = ViewUtil.dpToPx(26);
overflowParams.setMargins(pixelSize, pixelSize, pixelSize, pixelSize);
overflowContainer.setLayoutParams(overflowParams);
overflowContainer.setBackground(ContextCompat.getDrawable(getContext(), R.drawable.reaction_pill_background));
}
View pill = buildPill(getContext(), this, reaction, true);
pill.setOnClickListener(v -> {
extended = true;
displayReactions(Integer.MAX_VALUE);
});
pill.findViewById(R.id.reactions_pill_count).setVisibility(View.GONE);
pill.findViewById(R.id.reactions_pill_spacer).setVisibility(View.GONE);
overflowContainer.addView(pill);
} else {
View pill = buildPill(getContext(), this, reaction, false);
pill.setTag(reaction);
pill.setOnTouchListener(this);
MarginLayoutParams params = (MarginLayoutParams) pill.getLayoutParams();
params.setMargins(pixelSize, pixelSize, pixelSize, pixelSize);
pill.setLayoutParams(params);
container.addView(pill);
}
}
int overflowChildren = overflowContainer.getChildCount();
int negativeMargin = ViewUtil.dpToPx(-8);
for (int i = 0; i < overflowChildren; i++) {
View child = overflowContainer.getChildAt(i);
MarginLayoutParams childParams = (MarginLayoutParams) child.getLayoutParams();
if ((i == 0 && overflowChildren > 1) || i + 1 < overflowChildren) {
// if first and there is more than one child, or we are not the last child then set negative right margin
childParams.setMargins(0,0, negativeMargin, 0);
child.setLayoutParams(childParams);
}
}
if (threshold == Integer.MAX_VALUE) {
showLess.setVisibility(VISIBLE);
for (int id : showLess.getReferencedIds()) {
findViewById(id).setOnClickListener(view -> {
extended = false;
displayReactions(DEFAULT_THRESHOLD);
});
}
} else {
showLess.setVisibility(GONE);
}
}
private void onReactionClicked(Reaction reaction) {
if (reaction.messageId != 0) {
MessageId messageId = new MessageId(reaction.messageId, reaction.isMms);
delegate.onReactionClicked(reaction.emoji, messageId, reaction.userWasSender);
}
}
private static @NonNull List<Reaction> buildSortedReactionsList(@NonNull List<ReactionRecord> records, String userPublicKey, int threshold) {
Map<String, Reaction> counters = new LinkedHashMap<>();
for (ReactionRecord record : records) {
String baseEmoji = EmojiUtil.getCanonicalRepresentation(record.getEmoji());
Reaction info = counters.get(baseEmoji);
if (info == null) {
info = new Reaction(record.getMessageId(), record.isMms(), record.getEmoji(), record.getCount(), record.getSortId(), record.getDateReceived(), userPublicKey.equals(record.getAuthor()));
} else {
info.update(record.getEmoji(), record.getCount(), record.getDateReceived(), userPublicKey.equals(record.getAuthor()));
}
counters.put(baseEmoji, info);
}
List<Reaction> reactions = new ArrayList<>(counters.values());
Collections.sort(reactions, Collections.reverseOrder());
if (reactions.size() >= threshold + 2 && threshold != Integer.MAX_VALUE) {
List<Reaction> shortened = new ArrayList<>(threshold + 2);
shortened.addAll(reactions.subList(0, threshold + 2));
return shortened;
} else {
return reactions;
}
}
private static View buildPill(@NonNull Context context, @NonNull ViewGroup parent, @NonNull Reaction reaction, boolean isCompact) {
View root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false);
EmojiImageView emojiView = root.findViewById(R.id.reactions_pill_emoji);
TextView countView = root.findViewById(R.id.reactions_pill_count);
View spacer = root.findViewById(R.id.reactions_pill_spacer);
if (isCompact) {
root.setPaddingRelative(1,1,1,1);
ViewGroup.LayoutParams layoutParams = root.getLayoutParams();
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
root.setLayoutParams(layoutParams);
}
if (reaction.emoji != null) {
emojiView.setImageEmoji(reaction.emoji);
if (reaction.count >= 1) {
countView.setText(NumberUtil.getFormattedNumber(reaction.count));
} else {
countView.setVisibility(GONE);
spacer.setVisibility(GONE);
}
} else {
emojiView.setVisibility(GONE);
spacer.setVisibility(GONE);
countView.setText(context.getString(R.string.ReactionsConversationView_plus, reaction.count));
}
if (reaction.userWasSender && !isCompact) {
root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected));
countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactionsPillSelectedTextColor));
} else {
if (!isCompact) {
root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background));
}
}
return root;
}
private void onDown(MessageId messageId) {
removeLongPressCallback();
Runnable newLongPressCallback = () -> {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if (delegate != null) {
delegate.onReactionLongClicked(messageId);
}
};
this.longPressCallback = newLongPressCallback;
gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold);
onDownTimestamp = new Date().getTime();
}
private void removeLongPressCallback() {
if (longPressCallback != null) {
gestureHandler.removeCallbacks(longPressCallback);
}
}
private void onUp(Reaction reaction) {
if ((new Date().getTime() - onDownTimestamp) < longPressDurationThreshold) {
removeLongPressCallback();
if (pressCallback != null) {
gestureHandler.removeCallbacks(pressCallback);
this.pressCallback = null;
} else {
Runnable newPressCallback = () -> {
onReactionClicked(reaction);
pressCallback = null;
};
this.pressCallback = newPressCallback;
gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval);
}
}
}
private static class Reaction implements Comparable<Reaction> {
private final long messageId;
private final boolean isMms;
private String emoji;
private long count;
private long sortIndex;
private long lastSeen;
private boolean userWasSender;
Reaction(long messageId, boolean isMms, @Nullable String emoji, long count, long sortIndex, long lastSeen, boolean userWasSender) {
this.messageId = messageId;
this.isMms = isMms;
this.emoji = emoji;
this.count = count;
this.sortIndex = sortIndex;
this.lastSeen = lastSeen;
this.userWasSender = userWasSender;
}
void update(@NonNull String emoji, long count, long lastSeen, boolean userWasSender) {
if (!this.userWasSender) {
if (userWasSender || lastSeen > this.lastSeen) {
this.emoji = emoji;
}
}
this.count = this.count + count;
this.lastSeen = Math.max(this.lastSeen, lastSeen);
this.userWasSender = this.userWasSender || userWasSender;
}
@NonNull Reaction merge(@NonNull Reaction other) {
this.count = this.count + other.count;
this.lastSeen = Math.max(this.lastSeen, other.lastSeen);
this.userWasSender = this.userWasSender || other.userWasSender;
return this;
}
@Override
public int compareTo(Reaction rhs) {
Reaction lhs = this;
if (lhs.count == rhs.count ) {
return Long.compare(lhs.sortIndex, rhs.sortIndex);
} else {
return Long.compare(lhs.count, rhs.count);
}
}
}
}

View File

@ -0,0 +1,291 @@
package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.*
import android.view.View.OnTouchListener
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import com.google.android.flexbox.JustifyContent
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewEmojiReactionsBinding
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.ThemeUtil
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
import org.thoughtcrime.securesms.components.emoji.EmojiUtil
import org.thoughtcrime.securesms.conversation.v2.ViewUtil
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.util.NumberUtil.getFormattedNumber
import java.util.*
class EmojiReactionsView : ConstraintLayout, OnTouchListener {
companion object {
private const val DEFAULT_THRESHOLD = 5
private const val longPressDurationThreshold: Long = 250
private const val maxDoubleTapInterval: Long = 200
}
private val binding: ViewEmojiReactionsBinding by lazy { ViewEmojiReactionsBinding.bind(this) }
// Normally 6dp, but we have 1dp left+right margin on the pills themselves
private val OUTER_MARGIN = ViewUtil.dpToPx(2)
private var records: MutableList<ReactionRecord>? = null
private var messageId: Long = 0
private var delegate: VisibleMessageViewDelegate? = null
private val gestureHandler = Handler(Looper.getMainLooper())
private var pressCallback: Runnable? = null
private var longPressCallback: Runnable? = null
private var onDownTimestamp: Long = 0
private var extended = false
constructor(context: Context) : super(context) { init(null) }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { init(attrs) }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { init(attrs) }
private fun init(attrs: AttributeSet?) {
records = ArrayList()
if (attrs != null) {
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.EmojiReactionsView, 0, 0)
typedArray.recycle()
}
}
fun clear() {
records!!.clear()
binding.layoutEmojiContainer.removeAllViews()
}
fun setReactions(messageId: Long, records: List<ReactionRecord>, outgoing: Boolean, delegate: VisibleMessageViewDelegate?) {
this.delegate = delegate
if (records == this.records) {
return
}
binding.layoutEmojiContainer.justifyContent = if (outgoing) JustifyContent.FLEX_END else JustifyContent.FLEX_START
this.records!!.clear()
this.records!!.addAll(records)
if (this.messageId != messageId) {
extended = false
}
this.messageId = messageId
displayReactions(if (extended) Int.MAX_VALUE else DEFAULT_THRESHOLD)
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (v.tag == null) return false
val reaction = v.tag as Reaction
val action = event.action
if (action == MotionEvent.ACTION_DOWN) onDown(MessageId(reaction.messageId, reaction.isMms)) else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback() else if (action == MotionEvent.ACTION_UP) onUp(reaction)
return true
}
private fun displayReactions(threshold: Int) {
val userPublicKey = getLocalNumber(context)
val reactions = buildSortedReactionsList(records!!, userPublicKey, threshold)
binding.layoutEmojiContainer.removeAllViews()
val overflowContainer = LinearLayout(context)
overflowContainer.orientation = LinearLayout.HORIZONTAL
val innerPadding = ViewUtil.dpToPx(4)
overflowContainer.setPaddingRelative(innerPadding, innerPadding, innerPadding, innerPadding)
val pixelSize = ViewUtil.dpToPx(1)
for (reaction in reactions) {
if (binding.layoutEmojiContainer.childCount + 1 >= DEFAULT_THRESHOLD && threshold != Int.MAX_VALUE && reactions.size > threshold) {
if (overflowContainer.parent == null) {
binding.layoutEmojiContainer.addView(overflowContainer)
val overflowParams = overflowContainer.layoutParams as MarginLayoutParams
overflowParams.height = ViewUtil.dpToPx(26)
overflowParams.setMargins(pixelSize, pixelSize, pixelSize, pixelSize)
overflowContainer.layoutParams = overflowParams
overflowContainer.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)
}
val pill = buildPill(context, this, reaction, true)
pill.setOnClickListener { v: View? ->
extended = true
displayReactions(Int.MAX_VALUE)
}
pill.findViewById<View>(R.id.reactions_pill_count).visibility = GONE
pill.findViewById<View>(R.id.reactions_pill_spacer).visibility = GONE
overflowContainer.addView(pill)
} else {
val pill = buildPill(context, this, reaction, false)
pill.tag = reaction
pill.setOnTouchListener(this)
val params = pill.layoutParams as MarginLayoutParams
params.setMargins(pixelSize, pixelSize, pixelSize, pixelSize)
pill.layoutParams = params
binding.layoutEmojiContainer.addView(pill)
}
}
val overflowChildren = overflowContainer.childCount
val negativeMargin = ViewUtil.dpToPx(-8)
for (i in 0 until overflowChildren) {
val child = overflowContainer.getChildAt(i)
val childParams = child.layoutParams as MarginLayoutParams
if (i == 0 && overflowChildren > 1 || i + 1 < overflowChildren) {
// if first and there is more than one child, or we are not the last child then set negative right margin
childParams.setMargins(0, 0, negativeMargin, 0)
child.layoutParams = childParams
}
}
if (threshold == Int.MAX_VALUE) {
binding.groupShowLess.visibility = VISIBLE
for (id in binding.groupShowLess.referencedIds) {
findViewById<View>(id).setOnClickListener { view: View? ->
extended = false
displayReactions(DEFAULT_THRESHOLD)
}
}
} else {
binding.groupShowLess.visibility = GONE
}
}
private fun buildSortedReactionsList(records: List<ReactionRecord>, userPublicKey: String?, threshold: Int): List<Reaction> {
val counters: MutableMap<String, Reaction> = LinkedHashMap()
records.forEach {
val baseEmoji = EmojiUtil.getCanonicalRepresentation(it.emoji)
val info = counters[baseEmoji]
if (info == null) {
counters[baseEmoji] = Reaction(messageId, it.isMms, it.emoji, it.count, it.sortId, it.dateReceived, userPublicKey == it.author)
}
else {
info.update(it.emoji, it.count, it.dateReceived, userPublicKey == it.author)
}
}
val reactions: List<Reaction> = ArrayList(counters.values)
Collections.sort(reactions, Collections.reverseOrder())
return if (reactions.size >= threshold + 2 && threshold != Int.MAX_VALUE) {
val shortened: MutableList<Reaction> = ArrayList(threshold + 2)
shortened.addAll(reactions.subList(0, threshold + 2))
shortened
} else {
reactions
}
}
private fun buildPill(context: Context, parent: ViewGroup, reaction: Reaction, isCompact: Boolean): View {
val root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false)
val emojiView = root.findViewById<EmojiImageView>(R.id.reactions_pill_emoji)
val countView = root.findViewById<TextView>(R.id.reactions_pill_count)
val spacer = root.findViewById<View>(R.id.reactions_pill_spacer)
if (isCompact) {
root.setPaddingRelative(1, 1, 1, 1)
val layoutParams = root.layoutParams
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
root.layoutParams = layoutParams
}
if (reaction.emoji != null) {
emojiView.setImageEmoji(reaction.emoji)
if (reaction.count >= 1) {
countView.text = getFormattedNumber(reaction.count)
} else {
countView.visibility = GONE
spacer.visibility = GONE
}
} else {
emojiView.visibility = GONE
spacer.visibility = GONE
countView.text = context.getString(R.string.ReactionsConversationView_plus, reaction.count)
}
if (reaction.userWasSender && !isCompact) {
root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)
countView.setTextColor(ThemeUtil.getThemedColor(context, R.attr.reactionsPillSelectedTextColor))
} else {
if (!isCompact) {
root.background = ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)
}
}
return root
}
private fun onReactionClicked(reaction: Reaction) {
if (reaction.messageId != 0L) {
val messageId = MessageId(reaction.messageId, reaction.isMms)
delegate!!.onReactionClicked(reaction.emoji!!, messageId, reaction.userWasSender)
}
}
private fun onDown(messageId: MessageId) {
removeLongPressCallback()
val newLongPressCallback = Runnable {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
if (delegate != null) {
delegate!!.onReactionLongClicked(messageId)
}
}
longPressCallback = newLongPressCallback
gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold)
onDownTimestamp = Date().time
}
private fun removeLongPressCallback() {
if (longPressCallback != null) {
gestureHandler.removeCallbacks(longPressCallback!!)
}
}
private fun onUp(reaction: Reaction) {
if (Date().time - onDownTimestamp < longPressDurationThreshold) {
removeLongPressCallback()
if (pressCallback != null) {
gestureHandler.removeCallbacks(pressCallback!!)
pressCallback = null
} else {
val newPressCallback = Runnable {
onReactionClicked(reaction)
pressCallback = null
}
pressCallback = newPressCallback
gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval)
}
}
}
internal class Reaction(
internal val messageId: Long,
internal val isMms: Boolean,
internal var emoji: String?,
internal var count: Long,
internal val sortIndex: Long,
internal var lastSeen: Long,
internal var userWasSender: Boolean
) : Comparable<Reaction?> {
fun update(emoji: String, count: Long, lastSeen: Long, userWasSender: Boolean) {
if (!this.userWasSender) {
if (userWasSender || lastSeen > this.lastSeen) {
this.emoji = emoji
}
}
this.count = this.count + count
this.lastSeen = Math.max(this.lastSeen, lastSeen)
this.userWasSender = this.userWasSender || userWasSender
}
fun merge(other: Reaction): Reaction {
count = count + other.count
lastSeen = Math.max(lastSeen, other.lastSeen)
userWasSender = userWasSender || other.userWasSender
return this
}
override fun compareTo(other: Reaction?): Int {
if (other == null) { return -1 }
if (this.count == other.count) {
return this.sortIndex.compareTo(other.sortIndex)
}
return this.count.compareTo(other.count)
}
}
}

View File

@ -4,11 +4,9 @@ import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewLinkPreviewBinding import network.loki.messenger.databinding.ViewLinkPreviewBinding
@ -19,21 +17,16 @@ import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtiliti
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.ImageSlide import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.util.UiModeUtilities
class LinkPreviewView : LinearLayout { class LinkPreviewView : LinearLayout {
private lateinit var binding: ViewLinkPreviewBinding private val binding: ViewLinkPreviewBinding by lazy { ViewLinkPreviewBinding.bind(this) }
private val cornerMask by lazy { CornerMask(this) } private val cornerMask by lazy { CornerMask(this) }
private var url: String? = null private var url: String? = null
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private fun initialize() {
binding = ViewLinkPreviewBinding.inflate(LayoutInflater.from(context), this, true)
}
// endregion // endregion
// region Updating // region Updating
@ -48,8 +41,8 @@ class LinkPreviewView : LinearLayout {
// Thumbnail // Thumbnail
if (linkPreview.getThumbnail().isPresent) { if (linkPreview.getThumbnail().isPresent) {
// This internally fetches the thumbnail // This internally fetches the thumbnail
binding.thumbnailImageView.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message) binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message)
binding.thumbnailImageView.loadIndicator.isVisible = false binding.thumbnailImageView.root.loadIndicator.isVisible = false
} }
// Title // Title
binding.titleTextView.text = linkPreview.title binding.titleTextView.text = linkPreview.title

View File

@ -93,7 +93,7 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
val backgroundColor = context.getAccentColor() val backgroundColor = context.getAccentColor()
binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor) binding.quoteViewAttachmentPreviewContainer.backgroundTintList = ColorStateList.valueOf(backgroundColor)
binding.quoteViewAttachmentPreviewImageView.isVisible = false binding.quoteViewAttachmentPreviewImageView.isVisible = false
binding.quoteViewAttachmentThumbnailImageView.isVisible = false binding.quoteViewAttachmentThumbnailImageView.root.isVisible = false
when { when {
attachments.audioSlide != null -> { attachments.audioSlide != null -> {
binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone) binding.quoteViewAttachmentPreviewImageView.setImageResource(R.drawable.ic_microphone)
@ -108,9 +108,9 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
attachments.thumbnailSlide != null -> { attachments.thumbnailSlide != null -> {
val slide = attachments.thumbnailSlide!! val slide = attachments.thumbnailSlide!!
// This internally fetches the thumbnail // This internally fetches the thumbnail
binding.quoteViewAttachmentThumbnailImageView.radius = toPx(4, resources) binding.quoteViewAttachmentThumbnailImageView.root.radius = toPx(4, resources)
binding.quoteViewAttachmentThumbnailImageView.setImageResource(glide, slide, false, false) binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false, null)
binding.quoteViewAttachmentThumbnailImageView.isVisible = true binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image) binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
} }
} }

View File

@ -45,21 +45,17 @@ import org.thoughtcrime.securesms.util.getAccentColor
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout { class VisibleMessageContentView : ConstraintLayout {
private lateinit var binding: ViewVisibleMessageContentBinding private val binding: ViewVisibleMessageContentBinding by lazy { ViewVisibleMessageContentBinding.bind(this) }
var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf() var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf()
var onContentDoubleTap: (() -> Unit)? = null var onContentDoubleTap: (() -> Unit)? = null
var delegate: VisibleMessageViewDelegate? = null var delegate: VisibleMessageViewDelegate? = null
var indexInAdapter: Int = -1 var indexInAdapter: Int = -1
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() } constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
private fun initialize() {
binding = ViewVisibleMessageContentBinding.inflate(LayoutInflater.from(context), this, true)
}
// endregion // endregion
// region Updating // region Updating
@ -86,7 +82,7 @@ class VisibleMessageContentView : LinearLayout {
// reset visibilities / containers // reset visibilities / containers
onContentClick.clear() onContentClick.clear()
binding.albumThumbnailView.clearViews() binding.albumThumbnailView.root.clearViews()
onContentDoubleTap = null onContentDoubleTap = null
if (message.isDeleted) { if (message.isDeleted) {
@ -94,11 +90,11 @@ class VisibleMessageContentView : LinearLayout {
binding.deletedMessageView.root.bind(message, getTextColor(context, message)) binding.deletedMessageView.root.bind(message, getTextColor(context, message))
binding.bodyTextView.isVisible = false binding.bodyTextView.isVisible = false
binding.quoteView.root.isVisible = false binding.quoteView.root.isVisible = false
binding.linkPreviewView.isVisible = false binding.linkPreviewView.root.isVisible = false
binding.untrustedView.root.isVisible = false binding.untrustedView.root.isVisible = false
binding.voiceMessageView.root.isVisible = false binding.voiceMessageView.root.isVisible = false
binding.documentView.root.isVisible = false binding.documentView.root.isVisible = false
binding.albumThumbnailView.isVisible = false binding.albumThumbnailView.root.isVisible = false
binding.openGroupInvitationView.root.isVisible = false binding.openGroupInvitationView.root.isVisible = false
return return
} else { } else {
@ -110,12 +106,12 @@ class VisibleMessageContentView : LinearLayout {
binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null
binding.linkPreviewView.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty() binding.linkPreviewView.root.isVisible = message is MmsMessageRecord && message.linkPreviews.isNotEmpty()
binding.untrustedView.root.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty() binding.untrustedView.root.isVisible = !contactIsTrusted && message is MmsMessageRecord && message.quote == null && message.linkPreviews.isEmpty()
binding.voiceMessageView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null binding.voiceMessageView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.audioSlide != null
binding.documentView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null binding.documentView.root.isVisible = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.documentSlide != null
binding.albumThumbnailView.isVisible = mediaThumbnailMessage binding.albumThumbnailView.root.isVisible = mediaThumbnailMessage
binding.openGroupInvitationView.root.isVisible = message.isOpenGroupInvitation binding.openGroupInvitationView.root.isVisible = message.isOpenGroupInvitation
var hideBody = false var hideBody = false
@ -162,8 +158,8 @@ class VisibleMessageContentView : LinearLayout {
when { when {
message is MmsMessageRecord && message.linkPreviews.isNotEmpty() -> { message is MmsMessageRecord && message.linkPreviews.isNotEmpty() -> {
binding.linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster) binding.linkPreviewView.root.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
onContentClick.add { event -> binding.linkPreviewView.calculateHit(event) } onContentClick.add { event -> binding.linkPreviewView.root.calculateHit(event) }
// Body text view is inside the link preview for layout convenience // Body text view is inside the link preview for layout convenience
} }
message is MmsMessageRecord && message.slideDeck.audioSlide != null -> { message is MmsMessageRecord && message.slideDeck.audioSlide != null -> {
@ -200,21 +196,21 @@ class VisibleMessageContentView : LinearLayout {
if (contactIsTrusted || message.isOutgoing) { if (contactIsTrusted || message.isOutgoing) {
// isStart and isEnd of cluster needed for calculating the mask for full bubble image groups // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups
// bind after add view because views are inflated and calculated during bind // bind after add view because views are inflated and calculated during bind
binding.albumThumbnailView.bind( binding.albumThumbnailView.root.bind(
glideRequests = glide, glideRequests = glide,
message = message, message = message,
isStart = isStartOfMessageCluster, isStart = isStartOfMessageCluster,
isEnd = isEndOfMessageCluster isEnd = isEndOfMessageCluster
) )
val layoutParams = binding.albumThumbnailView.layoutParams as ConstraintLayout.LayoutParams val layoutParams = binding.albumThumbnailView.root.layoutParams as ConstraintLayout.LayoutParams
layoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f layoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
binding.albumThumbnailView.layoutParams = layoutParams binding.albumThumbnailView.root.layoutParams = layoutParams
onContentClick.add { event -> onContentClick.add { event ->
binding.albumThumbnailView.calculateHitObject(event, message, thread, onAttachmentNeedsDownload) binding.albumThumbnailView.root.calculateHitObject(event, message, thread, onAttachmentNeedsDownload)
} }
} else { } else {
hideBody = true hideBody = true
binding.albumThumbnailView.clearViews() binding.albumThumbnailView.root.clearViews()
binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message)) binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message))
onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) } onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) }
} }
@ -246,7 +242,7 @@ class VisibleMessageContentView : LinearLayout {
} }
private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean = private fun ViewVisibleMessageContentBinding.barrierViewsGone(): Boolean =
listOf<View>(albumThumbnailView, linkPreviewView, voiceMessageView.root, quoteView.root).none { it.isVisible } listOf<View>(albumThumbnailView.root, linkPreviewView.root, voiceMessageView.root, quoteView.root).none { it.isVisible }
private fun getBackground(isOutgoing: Boolean): Drawable { private fun getBackground(isOutgoing: Boolean): Drawable {
val backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone val backgroundID = if (isOutgoing) R.drawable.message_bubble_background_sent_alone else R.drawable.message_bubble_background_received_alone
@ -261,8 +257,8 @@ class VisibleMessageContentView : LinearLayout {
binding.openGroupInvitationView.root, binding.openGroupInvitationView.root,
binding.documentView.root, binding.documentView.root,
binding.quoteView.root, binding.quoteView.root,
binding.linkPreviewView, binding.linkPreviewView.root,
binding.albumThumbnailView, binding.albumThumbnailView.root,
binding.bodyTextView binding.bodyTextView
).forEach { view: View -> view.isVisible = false } ).forEach { view: View -> view.isVisible = false }
} }

View File

@ -85,7 +85,7 @@ class VisibleMessageView : LinearLayout {
var onPress: ((event: MotionEvent) -> Unit)? = null var onPress: ((event: MotionEvent) -> Unit)? = null
var onSwipeToReply: (() -> Unit)? = null var onSwipeToReply: (() -> Unit)? = null
var onLongPress: (() -> Unit)? = null var onLongPress: (() -> Unit)? = null
val messageContentView: VisibleMessageContentView by lazy { binding.messageContentView } val messageContentView: VisibleMessageContentView by lazy { binding.messageContentView.root }
companion object { companion object {
const val swipeToReplyThreshold = 64.0f // dp const val swipeToReplyThreshold = 64.0f // dp
@ -108,7 +108,7 @@ class VisibleMessageView : LinearLayout {
isHapticFeedbackEnabled = true isHapticFeedbackEnabled = true
setWillNotDraw(false) setWillNotDraw(false)
binding.messageInnerContainer.disableClipping() binding.messageInnerContainer.disableClipping()
binding.messageContentView.disableClipping() binding.messageContentView.root.disableClipping()
} }
// endregion // endregion
@ -210,26 +210,26 @@ class VisibleMessageView : LinearLayout {
// Expiration timer // Expiration timer
updateExpirationTimer(message) updateExpirationTimer(message)
// Emoji Reactions // Emoji Reactions
val emojiLayoutParams = binding.emojiReactionsView.layoutParams as ConstraintLayout.LayoutParams val emojiLayoutParams = binding.emojiReactionsView.root.layoutParams as ConstraintLayout.LayoutParams
emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f
binding.emojiReactionsView.layoutParams = emojiLayoutParams binding.emojiReactionsView.root.layoutParams = emojiLayoutParams
if (message.reactions.isNotEmpty()) { if (message.reactions.isNotEmpty()) {
val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) } val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) }
if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) { if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) {
binding.emojiReactionsView.setReactions(message.id, message.reactions, message.isOutgoing, delegate) binding.emojiReactionsView.root.setReactions(message.id, message.reactions, message.isOutgoing, delegate)
binding.emojiReactionsView.isVisible = true binding.emojiReactionsView.root.isVisible = true
} else { } else {
binding.emojiReactionsView.isVisible = false binding.emojiReactionsView.root.isVisible = false
} }
} }
else { else {
binding.emojiReactionsView.isVisible = false binding.emojiReactionsView.root.isVisible = false
} }
// Populate content view // Populate content view
binding.messageContentView.indexInAdapter = indexInAdapter binding.messageContentView.root.indexInAdapter = indexInAdapter
binding.messageContentView.bind( binding.messageContentView.root.bind(
message, message,
isStartOfMessageCluster, isStartOfMessageCluster,
isEndOfMessageCluster, isEndOfMessageCluster,
@ -239,8 +239,8 @@ class VisibleMessageView : LinearLayout {
message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false), message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false),
onAttachmentNeedsDownload onAttachmentNeedsDownload
) )
binding.messageContentView.delegate = delegate binding.messageContentView.root.delegate = delegate
onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() } onDoubleTap = { binding.messageContentView.root.onContentDoubleTap?.invoke() }
} }
private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean { private fun isStartOfMessageCluster(current: MessageRecord, previous: MessageRecord?, isGroupThread: Boolean): Boolean {
@ -275,7 +275,7 @@ class VisibleMessageView : LinearLayout {
private fun updateExpirationTimer(message: MessageRecord) { private fun updateExpirationTimer(message: MessageRecord) {
val container = binding.messageInnerContainer val container = binding.messageInnerContainer
val content = binding.messageContentView val content = binding.messageContentView.root
val expiration = binding.expirationTimerView val expiration = binding.expirationTimerView
val spacing = binding.messageContentSpacing val spacing = binding.messageContentSpacing
container.removeAllViewsInLayout() container.removeAllViewsInLayout()
@ -326,7 +326,7 @@ class VisibleMessageView : LinearLayout {
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
val iconSize = toPx(24, context.resources) val iconSize = toPx(24, context.resources)
val left = binding.messageInnerContainer.left + binding.messageContentView.right + spacing val left = binding.messageInnerContainer.left + binding.messageContentView.root.right + spacing
val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2) val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2)
val right = left + iconSize val right = left + iconSize
val bottom = top + iconSize val bottom = top + iconSize
@ -348,7 +348,7 @@ class VisibleMessageView : LinearLayout {
fun recycle() { fun recycle() {
binding.profilePictureView.root.recycle() binding.profilePictureView.root.recycle()
binding.messageContentView.recycle() binding.messageContentView.root.recycle()
} }
// endregion // endregion
@ -444,7 +444,7 @@ class VisibleMessageView : LinearLayout {
} }
fun onContentClick(event: MotionEvent) { fun onContentClick(event: MotionEvent) {
binding.messageContentView.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) } binding.messageContentView.root.onContentClick.iterator().forEach { clickHandler -> clickHandler.invoke(event) }
} }
private fun onPress(event: MotionEvent) { private fun onPress(event: MotionEvent) {
@ -464,7 +464,7 @@ class VisibleMessageView : LinearLayout {
} }
fun playVoiceMessage() { fun playVoiceMessage() {
binding.messageContentView.playVoiceMessage() binding.messageContentView.root.playVoiceMessage()
} }
// endregion // endregion
} }

View File

@ -1,425 +0,0 @@
package org.thoughtcrime.securesms.conversation.v2.utilities;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
import android.content.Context;
import android.content.res.TypedArray;
import android.net.Uri;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.FitCenter;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.ViewUtil;
import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.SettableFuture;
import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget;
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget;
import org.thoughtcrime.securesms.components.TransferControlView;
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.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import java.util.Collections;
import java.util.Locale;
import network.loki.messenger.R;
public class ThumbnailView extends FrameLayout {
private static final String TAG = ThumbnailView.class.getSimpleName();
private static final int WIDTH = 0;
private static final int HEIGHT = 1;
private static final int MIN_WIDTH = 0;
private static final int MAX_WIDTH = 1;
private static final int MIN_HEIGHT = 2;
private static final int MAX_HEIGHT = 3;
private ImageView image;
private View playOverlay;
private View loadIndicator;
private OnClickListener parentClickListener;
private final int[] dimens = new int[2];
private final int[] bounds = new int[4];
private final int[] measureDimens = new int[2];
private Optional<TransferControlView> transferControls = Optional.absent();
private SlideClickListener thumbnailClickListener = null;
private SlidesClickedListener downloadClickListener = null;
private Slide slide = null;
public int radius;
public ThumbnailView(Context context) {
this(context, null);
}
public ThumbnailView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ThumbnailView(final Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.thumbnail_view, this);
this.image = findViewById(R.id.thumbnail_image);
this.playOverlay = findViewById(R.id.play_overlay);
this.loadIndicator = findViewById(R.id.thumbnail_load_indicator);
super.setOnClickListener(new ThumbnailClickDispatcher());
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0);
bounds[MIN_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0);
bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0);
bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0);
bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0);
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0);
typedArray.recycle();
} else {
radius = 0;
}
}
@Override
protected void onMeasure(int originalWidthMeasureSpec, int originalHeightMeasureSpec) {
fillTargetDimensions(measureDimens, dimens, bounds);
if (measureDimens[WIDTH] == 0 && measureDimens[HEIGHT] == 0) {
super.onMeasure(originalWidthMeasureSpec, originalHeightMeasureSpec);
return;
}
int finalWidth = measureDimens[WIDTH] + getPaddingLeft() + getPaddingRight();
int finalHeight = measureDimens[HEIGHT] + getPaddingTop() + getPaddingBottom();
super.onMeasure(MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY));
}
@SuppressWarnings("SuspiciousNameCombination")
private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
int dimensFilledCount = getNonZeroCount(dimens);
int boundsFilledCount = getNonZeroCount(bounds);
if (dimensFilledCount == 0 || boundsFilledCount == 0) {
targetDimens[WIDTH] = 0;
targetDimens[HEIGHT] = 0;
return;
}
double naturalWidth = dimens[WIDTH];
double naturalHeight = dimens[HEIGHT];
int minWidth = bounds[MIN_WIDTH];
int maxWidth = bounds[MAX_WIDTH];
int minHeight = bounds[MIN_HEIGHT];
int maxHeight = bounds[MAX_HEIGHT];
if (dimensFilledCount > 0 && dimensFilledCount < dimens.length) {
throw new IllegalStateException(String.format(Locale.ENGLISH, "Width or height has been specified, but not both. Dimens: %f x %f",
naturalWidth, naturalHeight));
}
if (boundsFilledCount > 0 && boundsFilledCount < bounds.length) {
throw new IllegalStateException(String.format(Locale.ENGLISH, "One or more min/max dimensions have been specified, but not all. Bounds: [%d, %d, %d, %d]",
minWidth, maxWidth, minHeight, maxHeight));
}
double measuredWidth = naturalWidth;
double measuredHeight = naturalHeight;
boolean widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth;
boolean heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight;
if (!widthInBounds || !heightInBounds) {
double minWidthRatio = naturalWidth / minWidth;
double maxWidthRatio = naturalWidth / maxWidth;
double minHeightRatio = naturalHeight / minHeight;
double maxHeightRatio = naturalHeight / maxHeight;
if (maxWidthRatio > 1 || maxHeightRatio > 1) {
if (maxWidthRatio >= maxHeightRatio) {
measuredWidth /= maxWidthRatio;
measuredHeight /= maxWidthRatio;
} else {
measuredWidth /= maxHeightRatio;
measuredHeight /= maxHeightRatio;
}
measuredWidth = Math.max(measuredWidth, minWidth);
measuredHeight = Math.max(measuredHeight, minHeight);
} else if (minWidthRatio < 1 || minHeightRatio < 1) {
if (minWidthRatio <= minHeightRatio) {
measuredWidth /= minWidthRatio;
measuredHeight /= minWidthRatio;
} else {
measuredWidth /= minHeightRatio;
measuredHeight /= minHeightRatio;
}
measuredWidth = Math.min(measuredWidth, maxWidth);
measuredHeight = Math.min(measuredHeight, maxHeight);
}
}
targetDimens[WIDTH] = (int) measuredWidth;
targetDimens[HEIGHT] = (int) measuredHeight;
}
private int getNonZeroCount(int[] vals) {
int count = 0;
for (int val : vals) {
if (val > 0) {
count++;
}
}
return count;
}
@Override
public void setOnClickListener(OnClickListener l) {
parentClickListener = l;
}
@Override
public void setFocusable(boolean focusable) {
super.setFocusable(focusable);
if (transferControls.isPresent()) transferControls.get().setFocusable(focusable);
}
@Override
public void setClickable(boolean clickable) {
super.setClickable(clickable);
if (transferControls.isPresent()) transferControls.get().setClickable(clickable);
}
private TransferControlView getTransferControls() {
if (!transferControls.isPresent()) {
transferControls = Optional.of(ViewUtil.inflateStub(this, R.id.transfer_controls_stub));
}
return transferControls.get();
}
public void setBounds(int minWidth, int maxWidth, int minHeight, int maxHeight) {
bounds[MIN_WIDTH] = minWidth;
bounds[MAX_WIDTH] = maxWidth;
bounds[MIN_HEIGHT] = minHeight;
bounds[MAX_HEIGHT] = maxHeight;
forceLayout();
}
@UiThread
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
boolean showControls, boolean isPreview)
{
return setImageResource(glideRequests, slide, showControls, isPreview, 0, 0);
}
@UiThread
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
boolean showControls, boolean isPreview,
int naturalWidth, int naturalHeight)
{
if (showControls) {
getTransferControls().setSlide(slide);
getTransferControls().setDownloadClickListener(new DownloadClickDispatcher());
} else if (transferControls.isPresent()) {
getTransferControls().setVisibility(View.GONE);
}
if (slide.getThumbnailUri() != null && slide.hasPlayOverlay() &&
(slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
{
this.playOverlay.setVisibility(View.VISIBLE);
} else {
this.playOverlay.setVisibility(View.GONE);
}
if (Util.equals(slide, this.slide)) {
Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getDataUri());
return new SettableFuture<>(false);
}
if (this.slide != null && this.slide.getFastPreflightId() != null &&
this.slide.getFastPreflightId().equals(slide.getFastPreflightId()))
{
Log.i(TAG, "Not re-loading slide for fast preflight: " + slide.getFastPreflightId());
this.slide = slide;
return new SettableFuture<>(false);
}
Log.i(TAG, "loading part with id " + slide.asAttachment().getDataUri()
+ ", progress " + slide.getTransferState() + ", fast preflight id: " +
slide.asAttachment().getFastPreflightId());
this.slide = slide;
dimens[WIDTH] = naturalWidth;
dimens[HEIGHT] = naturalHeight;
invalidate();
SettableFuture<Boolean> result = new SettableFuture<>();
if (slide.getThumbnailUri() != null) {
buildThumbnailGlideRequest(glideRequests, slide).into(new GlideDrawableListeningTarget(image, result));
} else if (slide.hasPlaceholder()) {
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(image, result));
} else {
glideRequests.load(R.drawable.ic_image_white_24dp).centerInside().into(image);
result.set(false);
}
return result;
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
SettableFuture<Boolean> future = new SettableFuture<>();
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
GlideRequest request = glideRequests.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(withCrossFade());
if (radius > 0) {
request = request.transforms(new CenterCrop(), new RoundedCorners(radius));
} else {
request = request.transforms(new CenterCrop());
}
request.into(new GlideDrawableListeningTarget(image, future));
return future;
}
public void setThumbnailClickListener(SlideClickListener listener) {
this.thumbnailClickListener = listener;
}
public void setDownloadClickListener(SlidesClickedListener listener) {
this.downloadClickListener = listener;
}
public void clear(GlideRequests glideRequests) {
glideRequests.clear(image);
if (transferControls.isPresent()) {
getTransferControls().clear();
}
slide = null;
}
public void showDownloadText(boolean showDownloadText) {
getTransferControls().setShowDownloadText(showDownloadText);
}
public void showProgressSpinner() {
getTransferControls().showProgressSpinner();
}
public void setLoadIndicatorVisibile(boolean visible) {
this.loadIndicator.setVisibility(visible ? VISIBLE : GONE);
}
protected void setRadius(int radius) {
this.radius = radius;
}
private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri()))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(withCrossFade()), new CenterCrop());
if (slide.isInProgress()) return request;
else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture));
}
private RequestBuilder buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
return applySizing(glideRequests.asBitmap()
.load(slide.getPlaceholderRes(getContext().getTheme()))
.diskCacheStrategy(DiskCacheStrategy.NONE), new FitCenter());
}
private GlideRequest applySizing(@NonNull GlideRequest request, @NonNull BitmapTransformation fitting) {
int[] size = new int[2];
fillTargetDimensions(size, dimens, bounds);
if (size[WIDTH] == 0 && size[HEIGHT] == 0) {
size[WIDTH] = getDefaultWidth();
size[HEIGHT] = getDefaultHeight();
}
request = request.override(size[WIDTH], size[HEIGHT]);
if (radius > 0) {
return request.transforms(fitting, new RoundedCorners(radius));
} else {
return request.transforms(fitting);
}
}
private int getDefaultWidth() {
ViewGroup.LayoutParams params = getLayoutParams();
if (params != null) {
return Math.max(params.width, 0);
}
return 0;
}
private int getDefaultHeight() {
ViewGroup.LayoutParams params = getLayoutParams();
if (params != null) {
return Math.max(params.height, 0);
}
return 0;
}
private class ThumbnailClickDispatcher implements View.OnClickListener {
@Override
public void onClick(View view) {
if (thumbnailClickListener != null &&
slide != null &&
slide.asAttachment().getDataUri() != null &&
slide.getTransferState() == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE)
{
thumbnailClickListener.onClick(view, slide);
} else if (parentClickListener != null) {
parentClickListener.onClick(view);
}
}
}
private class DownloadClickDispatcher implements View.OnClickListener {
@Override
public void onClick(View view) {
if (downloadClickListener != null && slide != null) {
downloadClickListener.onClick(view, Collections.singletonList(slide));
} else {
Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + String.valueOf(slide) + " downloadClickListener: " + String.valueOf(downloadClickListener));
}
}
}
}

View File

@ -2,14 +2,11 @@ package org.thoughtcrime.securesms.conversation.v2.utilities
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.CenterCrop
@ -29,31 +26,33 @@ import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.mms.GlideRequest import org.thoughtcrime.securesms.mms.GlideRequest
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
import kotlin.Boolean
import kotlin.Int
import kotlin.getValue
import kotlin.lazy
import kotlin.let
open class KThumbnailView: FrameLayout { open class ThumbnailView: FrameLayout {
private lateinit var binding: ThumbnailViewBinding
companion object { companion object {
private const val WIDTH = 0 private const val WIDTH = 0
private const val HEIGHT = 1 private const val HEIGHT = 1
} }
private val binding: ThumbnailViewBinding by lazy { ThumbnailViewBinding.bind(this) }
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize(null) } constructor(context: Context) : super(context) { initialize(null) }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) } constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) } constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
private val image by lazy { binding.thumbnailImage }
private val playOverlay by lazy { binding.playOverlay }
val loadIndicator: View by lazy { binding.thumbnailLoadIndicator } val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
val downloadIndicator: View by lazy { binding.thumbnailDownloadIcon }
private val dimensDelegate = ThumbnailDimensDelegate() private val dimensDelegate = ThumbnailDimensDelegate()
private var slide: Slide? = null private var slide: Slide? = null
private var radius: Int = 0 var radius: Int = 0
private fun initialize(attrs: AttributeSet?) { private fun initialize(attrs: AttributeSet?) {
binding = ThumbnailViewBinding.inflate(LayoutInflater.from(context), this)
if (attrs != null) { if (attrs != null) {
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0) val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
@ -66,8 +65,6 @@ open class KThumbnailView: FrameLayout {
typedArray.recycle() typedArray.recycle()
} }
val background = ContextCompat.getColor(context, R.color.transparent_black_6)
binding.root.background = ColorDrawable(background)
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@ -90,17 +87,17 @@ open class KThumbnailView: FrameLayout {
// endregion // endregion
// region Interaction // region Interaction
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord): ListenableFuture<Boolean> { fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord?): ListenableFuture<Boolean> {
return setImageResource(glide, slide, isPreview, 0, 0, mms) return setImageResource(glide, slide, isPreview, 0, 0, mms)
} }
fun setImageResource(glide: GlideRequests, slide: Slide, fun setImageResource(glide: GlideRequests, slide: Slide,
isPreview: Boolean, naturalWidth: Int, isPreview: Boolean, naturalWidth: Int,
naturalHeight: Int, mms: MmsMessageRecord): ListenableFuture<Boolean> { naturalHeight: Int, mms: MmsMessageRecord?): ListenableFuture<Boolean> {
val currentSlide = this.slide val currentSlide = this.slide
playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() && binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview)) (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
if (equals(currentSlide, slide)) { if (equals(currentSlide, slide)) {
@ -116,8 +113,8 @@ open class KThumbnailView: FrameLayout {
this.slide = slide this.slide = slide
loadIndicator.isVisible = slide.isInProgress binding.thumbnailLoadIndicator.isVisible = slide.isInProgress
downloadIndicator.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED binding.thumbnailDownloadIcon.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED
dimensDelegate.setDimens(naturalWidth, naturalHeight) dimensDelegate.setDimens(naturalWidth, naturalHeight)
invalidate() invalidate()
@ -126,13 +123,13 @@ open class KThumbnailView: FrameLayout {
when { when {
slide.thumbnailUri != null -> { slide.thumbnailUri != null -> {
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(image, result)) buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, result))
} }
slide.hasPlaceholder() -> { slide.hasPlaceholder() -> {
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(image, result)) buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, result))
} }
else -> { else -> {
glide.clear(image) glide.clear(binding.thumbnailImage)
result.set(false) result.set(false)
} }
} }
@ -176,7 +173,7 @@ open class KThumbnailView: FrameLayout {
} }
open fun clear(glideRequests: GlideRequests) { open fun clear(glideRequests: GlideRequests) {
glideRequests.clear(image) glideRequests.clear(binding.thumbnailImage)
slide = null slide = null
} }
@ -193,11 +190,35 @@ open class KThumbnailView: FrameLayout {
request.transforms(CenterCrop()) request.transforms(CenterCrop())
} }
request.into(GlideDrawableListeningTarget(image, future)) request.into(GlideDrawableListeningTarget(binding.thumbnailImage, future))
return future return future
} }
// fun showDownloadText(showDownloadText: Boolean) {
// getTransferControls()?.setShowDownloadText(showDownloadText);
// }
//
// fun setDownloadClickListener(listener: SlidesClickedListener) {
// this.downloadClickListener = listener;
// }
//
// private fun getTransferControls(): TransferControlView? {
// if (transferControls == null) {
// transferControls = ViewUtil.inflateStub(this, R.id.transfer_controls_stub);
// }
//
// return transferControls
// }
// endregion // endregion
// private class DownloadClickDispatcher : OnClickListener {
// override fun onClick(view: View?) {
// if (downloadClickListener != null && slide != null) {
// downloadClickListener.onClick(view, Collections.singletonList(slide))
// } else {
// Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + String.valueOf(slide) + " downloadClickListener: " + String.valueOf(downloadClickListener))
// }
// }
// }
} }

View File

@ -99,11 +99,11 @@ class ConversationView : LinearLayout {
binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT binding.snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE binding.snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
if (isTyping) { if (isTyping) {
binding.typingIndicatorView.startAnimation() binding.typingIndicatorView.root.startAnimation()
} else { } else {
binding.typingIndicatorView.stopAnimation() binding.typingIndicatorView.root.stopAnimation()
} }
binding.typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE binding.typingIndicatorView.root.visibility = if (isTyping) View.VISIBLE else View.GONE
binding.statusIndicatorImageView.visibility = View.VISIBLE binding.statusIndicatorImageView.visibility = View.VISIBLE
when { when {
!thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE !thread.isOutgoing -> binding.statusIndicatorImageView.visibility = View.GONE

View File

@ -202,7 +202,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
OpenGroupManager.startPolling() OpenGroupManager.startPolling()
JobQueue.shared.resumePendingJobs() JobQueue.shared.resumePendingJobs()
} }
// Set up typing observer
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
updateProfileButton() updateProfileButton()
TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect { TextSecurePreferences.events.filter { it == TextSecurePreferences.PROFILE_NAME_PREF }.collect {
@ -365,6 +365,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
setupMessageRequestsBanner() setupMessageRequestsBanner()
updateEmptyState() updateEmptyState()
} }
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
}
} }
private fun updateEmptyState() { private fun updateEmptyState() {

View File

@ -63,6 +63,8 @@ class HomeAdapter(
lateinit var glide: GlideRequests lateinit var glide: GlideRequests
var typingThreadIDs = setOf<Long>() var typingThreadIDs = setOf<Long>()
set(value) { set(value) {
if (field == value) { return }
field = value field = value
// TODO: replace this with a diffed update or a partial change set with payloads // TODO: replace this with a diffed update or a partial change set with payloads
notifyDataSetChanged() notifyDataSetChanged()

View File

@ -6,7 +6,7 @@
android:layout_width="@dimen/media_bubble_default_dimens" android:layout_width="@dimen/media_bubble_default_dimens"
android:layout_height="@dimen/media_bubble_default_dimens"> android:layout_height="@dimen/media_bubble_default_dimens">
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -7,13 +7,13 @@
android:layout_width="@dimen/album_total_width" android:layout_width="@dimen/album_total_width"
android:layout_height="@dimen/album_2_total_height"> android:layout_height="@dimen/album_2_total_height">
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_2_cell_width" android:layout_width="@dimen/album_2_cell_width"
android:layout_height="@dimen/album_2_total_height" android:layout_height="@dimen/album_2_total_height"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_2_cell_width" android:layout_width="@dimen/album_2_cell_width"
android:layout_height="@dimen/album_2_total_height" android:layout_height="@dimen/album_2_total_height"

View File

@ -6,13 +6,13 @@
android:layout_width="@dimen/album_total_width" android:layout_width="@dimen/album_total_width"
android:layout_height="@dimen/album_3_total_height"> android:layout_height="@dimen/album_3_total_height">
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_3_cell_width_big" android:layout_width="@dimen/album_3_cell_width_big"
android:layout_height="@dimen/album_3_total_height" android:layout_height="@dimen/album_3_total_height"
app:thumbnail_radius="0dp"/> app:thumbnail_radius="0dp"/>
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_3_cell_size_small" android:layout_width="@dimen/album_3_cell_size_small"
android:layout_height="@dimen/album_3_cell_size_small" android:layout_height="@dimen/album_3_cell_size_small"
@ -25,7 +25,7 @@
android:layout_height="@dimen/album_5_cell_size_small" android:layout_height="@dimen/album_5_cell_size_small"
android:layout_gravity="end|bottom"> android:layout_gravity="end|bottom">
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_3" android:id="@+id/album_cell_3"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -20,4 +21,4 @@
android:layout_gravity="center" android:layout_gravity="center"
android:layout="@layout/transfer_controls_stub" /> android:layout="@layout/transfer_controls_stub" />
</RelativeLayout> </org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView>

View File

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/linkpreview_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/small_spacing"
android:background="?linkpreview_background_color">
<org.thoughtcrime.securesms.components.OutlinedThumbnailView
android:id="@+id/linkpreview_thumbnail"
android:layout_width="72dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/linkpreview_divider"
app:layout_constraintHeight_min="72dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/linkpreview_title"
tools:src="@drawable/ic_contact_picture"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/linkpreview_title"
style="@style/Signal.Text.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:maxLines="1"
android:textSize="@dimen/medium_font_size"
android:textColor="?linkpreview_primary_text_color"
app:layout_constraintEnd_toStartOf="@+id/linkpreview_close"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
app:layout_constraintTop_toTopOf="parent"
tools:text="Wall Crawler Strikes Again!" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/linkpreview_site"
style="@style/Signal.Text.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:maxLines="1"
android:textSize="@dimen/small_font_size"
android:textColor="?linkpreview_secondary_text_color"
android:alpha="0.6"
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
app:layout_constraintTop_toBottomOf="@+id/linkpreview_title"
tools:text="dailybugle.com" />
<View
android:id="@+id/linkpreview_divider"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="8dp"
android:layout_marginTop="6dp"
android:background="?linkpreview_divider_color"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/linkpreview_thumbnail"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/linkpreview_thumbnail"
app:layout_constraintTop_toBottomOf="@+id/linkpreview_site"
app:layout_constraintVertical_bias="0.0"
tools:visibility="visible" />
<ImageView
android:id="@+id/linkpreview_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_marginTop="4dp"
android:src="@drawable/ic_close_white_18dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/gray70"
tools:visibility="visible" />
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.DoubleBounce"
android:id="@+id/linkpreview_progress_wheel"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="center"
android:padding="@dimen/small_spacing"
app:SpinKit_Color="?android:textColorPrimary"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/linkpreview_divider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>

View File

@ -5,7 +5,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="2dp"> android:padding="2dp">
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/image" android:id="@+id/image"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -8,7 +8,7 @@
android:layout_margin="2dp" android:layout_margin="2dp"
android:animateLayoutChanges="true"> android:animateLayoutChanges="true">
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/rail_item_image" android:id="@+id/rail_item_image"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"

View File

@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge <org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent_black_6">
<ImageView <ImageView
android:id="@+id/thumbnail_image" android:id="@+id/thumbnail_image"
@ -60,4 +63,4 @@
android:layout="@layout/transfer_controls_stub" android:layout="@layout/transfer_controls_stub"
android:visibility="gone" /> android:visibility="gone" />
</merge> </org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView>

View File

@ -34,23 +34,21 @@
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView <TextView
android:id="@+id/conversationViewDisplayNameTextView" android:id="@+id/conversationViewDisplayNameTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/unreadCountIndicator"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constrainedWidth="true"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintHorizontal_bias="0"
android:drawablePadding="4dp" android:drawablePadding="4dp"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" android:ellipsize="end"
@ -65,11 +63,16 @@
<RelativeLayout <RelativeLayout
android:id="@+id/unreadCountIndicator" android:id="@+id/unreadCountIndicator"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginStart="4dp"
app:layout_constraintStart_toEndOf="@id/conversationViewDisplayNameTextView"
app:layout_constraintEnd_toStartOf="@id/timestampTextView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:minWidth="20dp"
android:maxWidth="40dp" android:maxWidth="40dp"
android:paddingLeft="4dp" android:paddingLeft="4dp"
android:paddingRight="4dp" android:paddingRight="4dp"
android:layout_height="20dp"
android:layout_marginStart="4dp"
android:background="@drawable/rounded_rectangle" android:background="@drawable/rounded_rectangle"
android:backgroundTint="?unreadIndicatorBackgroundColor"> android:backgroundTint="?unreadIndicatorBackgroundColor">
@ -78,20 +81,22 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:text="8"
android:textColor="?unreadIndicatorTextColor" android:textColor="?unreadIndicatorTextColor"
android:textSize="@dimen/very_small_font_size" android:textSize="@dimen/very_small_font_size"
android:textStyle="bold" /> android:textStyle="bold"
tools:text="8"
tools:textColor="?android:textColorPrimary" />
</RelativeLayout> </RelativeLayout>
</LinearLayout>
<TextView <TextView
android:id="@+id/timestampTextView" android:id="@+id/timestampTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="@dimen/medium_spacing" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:paddingStart="@dimen/medium_spacing"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" android:ellipsize="end"
android:textSize="@dimen/small_font_size" android:textSize="@dimen/small_font_size"
@ -99,7 +104,7 @@
android:alpha="0.4" android:alpha="0.4"
tools:text="9:41 AM" /> tools:text="9:41 AM" />
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -131,7 +136,7 @@
android:textSize="@dimen/medium_font_size" android:textSize="@dimen/medium_font_size"
tools:text="Sorry, gotta go fight crime again" /> tools:text="Sorry, gotta go fight crime again" />
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView <include layout="@layout/view_typing_indicator"
android:id="@+id/typingIndicatorView" android:id="@+id/typingIndicatorView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -17,7 +17,7 @@
android:background="@drawable/message_bubble_background_received_alone" android:background="@drawable/message_bubble_background_received_alone"
android:backgroundTint="?message_received_background_color"> android:backgroundTint="?message_received_background_color">
<org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView <include layout="@layout/view_typing_indicator"
android:id="@+id/typingIndicator" android:id="@+id/typingIndicator"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <org.thoughtcrime.securesms.conversation.v2.messages.EmojiReactionsView
android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/small_spacing" android:padding="@dimen/small_spacing"
android:gravity="center"> android:gravity="center">
@ -65,4 +66,4 @@
android:visibility="gone" android:visibility="gone"
app:constraint_referenced_ids="image_view_show_less, text_view_show_less"/> app:constraint_referenced_ids="image_view_show_less, text_view_show_less"/>
</androidx.constraintlayout.widget.ConstraintLayout> </org.thoughtcrime.securesms.conversation.v2.messages.EmojiReactionsView>

View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/mainLinkPreviewContainer" <org.thoughtcrime.securesms.conversation.v2.messages.LinkPreviewView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/mainLinkPreviewContainer"
android:background="@color/transparent_black_6" android:background="@color/transparent_black_6"
android:layout_width="300dp" android:layout_width="300dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -20,7 +21,7 @@
android:src="@drawable/ic_link" android:src="@drawable/ic_link"
app:tint="?android:textColorPrimary" /> app:tint="?android:textColorPrimary" />
<org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView <include layout="@layout/thumbnail_view"
android:background="@color/transparent_black_6" android:background="@color/transparent_black_6"
android:id="@+id/thumbnailImageView" android:id="@+id/thumbnailImageView"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -43,4 +44,4 @@
android:ellipsize="end" android:ellipsize="end"
android:textColor="?android:textColorPrimary"/> android:textColor="?android:textColorPrimary"/>
</LinearLayout> </org.thoughtcrime.securesms.conversation.v2.messages.LinkPreviewView>

View File

@ -26,7 +26,7 @@
android:src="@drawable/ic_link" android:src="@drawable/ic_link"
app:tint="?android:textColorPrimary" /> app:tint="?android:textColorPrimary" />
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/thumbnailImageView" android:id="@+id/thumbnailImageView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -44,7 +44,7 @@
android:scaleType="centerInside" android:scaleType="centerInside"
android:src="@drawable/ic_microphone" /> android:src="@drawable/ic_microphone" />
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/quoteViewAttachmentThumbnailImageView" android:id="@+id/quoteViewAttachmentThumbnailImageView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -44,7 +44,7 @@
android:scaleType="centerInside" android:scaleType="centerInside"
android:src="@drawable/ic_microphone" /> android:src="@drawable/ic_microphone" />
<org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView <include layout="@layout/thumbnail_view"
android:id="@+id/quoteViewAttachmentThumbnailImageView" android:id="@+id/quoteViewAttachmentThumbnailImageView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -1,11 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge <org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
tools:context="org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView">
<View <View
android:id="@+id/typing_dot1" android:id="@+id/typing_dot1"
@ -37,4 +36,4 @@
android:alpha="0.5" android:alpha="0.5"
android:background="@drawable/circle_white" /> android:background="@drawable/circle_white" />
</merge> </org.thoughtcrime.securesms.conversation.v2.components.TypingIndicatorView>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView xmlns:android="http://schemas.android.com/apk/res/android" <org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/visibleMessageView" android:id="@+id/visibleMessageView"
@ -81,7 +82,7 @@
app:layout_constraintStart_toEndOf="@+id/profilePictureView" app:layout_constraintStart_toEndOf="@+id/profilePictureView"
app:layout_constraintTop_toBottomOf="@+id/senderNameTextView"> app:layout_constraintTop_toBottomOf="@+id/senderNameTextView">
<org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView <include layout="@layout/view_visible_message_content"
android:id="@+id/messageContentView" android:id="@+id/messageContentView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
@ -104,7 +105,7 @@
</LinearLayout> </LinearLayout>
<org.thoughtcrime.securesms.conversation.v2.messages.EmojiReactionsView <include layout="@layout/view_emoji_reactions"
android:id="@+id/emojiReactionsView" android:id="@+id/emojiReactionsView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -75,7 +75,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<org.thoughtcrime.securesms.conversation.v2.messages.LinkPreviewView <include layout="@layout/view_link_preview"
app:layout_constraintTop_toBottomOf="@+id/quoteView" app:layout_constraintTop_toBottomOf="@+id/quoteView"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:visibility="gone" android:visibility="gone"
@ -112,8 +112,8 @@
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView <include layout="@layout/album_thumbnail_view"
android:visibility="visible" android:visibility="gone"
android:id="@+id/albumThumbnailView" android:id="@+id/albumThumbnailView"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@+id/contentParent" app:layout_constraintTop_toBottomOf="@+id/contentParent"
@ -123,4 +123,4 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout> </org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView>