diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 3f8f03fa51..def84b57f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -64,10 +64,12 @@ import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.components.MediaView; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader; +import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel; import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.DateUtils; @@ -116,6 +118,22 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im private int restartItem = -1; + public static Intent getPreviewIntent(Context context, Slide slide, MmsMessageRecord mms) { + Intent previewIntent = null; + if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) { + previewIntent = new Intent(context, MediaPreviewActivity.class); + previewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setDataAndType(slide.getUri(), slide.getContentType()) + .putExtra(ADDRESS_EXTRA, mms.getRecipient().getAddress()) + .putExtra(OUTGOING_EXTRA, mms.isOutgoing()) + .putExtra(DATE_EXTRA, mms.getTimestamp()) + .putExtra(SIZE_EXTRA, slide.asAttachment().getSize()) + .putExtra(CAPTION_EXTRA, slide.getCaption().orNull()) + .putExtra(LEFT_IS_RECENT_EXTRA, false); + } + return previewIntent; + } + @SuppressWarnings("ConstantConditions") @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AlbumThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/AlbumThumbnailView.java index 1586ca6c52..bda4159c9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/AlbumThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/AlbumThumbnailView.java @@ -1,27 +1,27 @@ package org.thoughtcrime.securesms.components; import android.content.Context; -import androidx.annotation.ColorInt; -import androidx.annotation.IdRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.TextView; -import network.loki.messenger.R; +import androidx.annotation.ColorInt; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; -import org.thoughtcrime.securesms.conversation.v2.utilities.ThumbnailView; +import org.session.libsession.utilities.Stub; +import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView; 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 org.session.libsession.utilities.Stub; import java.util.List; +import network.loki.messenger.R; + public class AlbumThumbnailView extends FrameLayout { private @Nullable SlideClickListener thumbnailClickListener; @@ -53,8 +53,8 @@ public class AlbumThumbnailView extends FrameLayout { private void initialize() { inflate(getContext(), R.layout.album_thumbnail_view, this); - albumCellContainer = findViewById(R.id.album_cell_container); - transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub)); + albumCellContainer = findViewById(R.id.albumCellContainer); + transferControls = new Stub<>(findViewById(R.id.albumTransferControlsStub)); } public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List slides, boolean showControls) { @@ -149,9 +149,8 @@ public class AlbumThumbnailView extends FrameLayout { } private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) { - ThumbnailView cell = findViewById(id); - cell.setImageResource(glideRequests, slide, false, false); - cell.setLoadIndicatorVisibile(slide.isInProgress()); + KThumbnailView cell = findViewById(id); + cell.setImageResource(glideRequests, slide, false); cell.setThumbnailClickListener(defaultThumbnailClickListener); cell.setOnLongClickListener(defaultLongClickListener); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java index e41f641024..af9e766416 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java @@ -65,15 +65,10 @@ public class ConversationItemThumbnail extends FrameLayout { if (attrs != null) { TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0); - thumbnail.setBounds(typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0), - typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0), - typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0), - typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0)); typedArray.recycle(); } } - @SuppressWarnings("SuspiciousNameCombination") @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java index df28984ab0..71bf8a2804 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java @@ -29,7 +29,6 @@ public class OutlinedThumbnailView extends ThumbnailView { outliner = new Outliner(); outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color)); - setRadius(0); setWillNotDraw(false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AlbumThumbnailView.kt new file mode 100644 index 0000000000..5384be14e2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/AlbumThumbnailView.kt @@ -0,0 +1,169 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.view.children +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.album_thumbnail_view.view.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.MediaPreviewActivity +import org.thoughtcrime.securesms.components.CornerMask +import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView +import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher +import org.thoughtcrime.securesms.longmessage.LongMessageActivity +import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.mms.Slide + +class AlbumThumbnailView : FrameLayout { + + companion object { + const val MAX_ALBUM_DISPLAY_SIZE = 5 + } + + // region Lifecycle + constructor(context: Context) : super(context) { + initialize() + } + + 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 var slides: List = listOf() + private var slideSize: Int = 0 + + private fun initialize() { + LayoutInflater.from(context).inflate(R.layout.album_thumbnail_view, this) + } + + override fun dispatchDraw(canvas: Canvas?) { + super.dispatchDraw(canvas) + cornerMask.mask(canvas) + } + // endregion + + // region Interaction + + fun calculateHitObject(rawRect: Rect, mms: MmsMessageRecord) { + // Z-check in specific order + val testRect = Rect() + // test "Read More" + albumCellBodyTextReadMore.getGlobalVisibleRect(testRect) + if (Rect.intersects(rawRect, testRect)) { + // dispatch to activity view + ActivityDispatcher.get(context)?.dispatchIntent { context -> + LongMessageActivity.getIntent(context, mms.recipient.address, mms.getId(), true) + } + return + } + // test each album child + albumCellContainer.findViewById(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child -> + child.getGlobalVisibleRect(testRect) + if (Rect.intersects(rawRect, testRect)) { + // hit intersects with this particular child + val slide = slides.getOrNull(index) ?: return + // only open to downloaded images + if (slide.isInProgress) return + + ActivityDispatcher.get(context)?.dispatchIntent { context -> + MediaPreviewActivity.getPreviewIntent(context, slide, mms) + } + } + } + } + + fun bind(glideRequests: GlideRequests, message: MmsMessageRecord, + isStart: Boolean, isEnd: Boolean) { + slides = message.slideDeck.thumbnailSlides + if (slides.isEmpty()) { + // this should never be encountered because it's checked by parent + return + } + calculateRadius(isStart, isEnd, message.isOutgoing) + + // recreate cell views if different size to what we have already (for recycling) + if (slides.size != this.slideSize) { + albumCellContainer.removeAllViews() + LayoutInflater.from(context).inflate(layoutRes(slides.size), albumCellContainer) + val overflowed = slides.size > MAX_ALBUM_DISPLAY_SIZE + albumCellContainer.findViewById(R.id.album_cell_overflow_text)?.let { overflowText -> + // overflowText will be null if !overflowed + overflowText.isVisible = overflowed // more than max album size + overflowText.text = context.getString(R.string.AlbumThumbnailView_plus, slides.size - MAX_ALBUM_DISPLAY_SIZE) + } + this.slideSize = slides.size + } + // iterate binding + slides.take(5).forEachIndexed { position, slide -> + val thumbnailView = getThumbnailView(position) + thumbnailView.setImageResource(glideRequests, slide, isPreview = false) + } + albumCellBodyParent.isVisible = message.body.isNotEmpty() + albumCellBodyText.text = message.body + post { + // post to await layout of text + albumCellBodyText.layout?.let { layout -> + val maxEllipsis = (0 until layout.lineCount).maxByOrNull { lineNum -> layout.getEllipsisCount(lineNum) } + ?: 0 + // show read more text if at least one line is ellipsized + albumCellBodyTextReadMore.isVisible = maxEllipsis > 0 + } + } + } + + // endregion + + + fun layoutRes(slideCount: Int) = when (slideCount) { + 1 -> R.layout.album_thumbnail_1 // single + 2 -> R.layout.album_thumbnail_2// two sidebyside + 3 -> R.layout.album_thumbnail_3// three stacked + 4 -> R.layout.album_thumbnail_4// four square + 5 -> R.layout.album_thumbnail_5// + else -> R.layout.album_thumbnail_many// five or more + } + + fun getThumbnailView(position: Int): KThumbnailView = when (position) { + 0 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_1) + 1 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_2) + 2 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_3) + 3 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_4) + 4 -> albumCellContainer.findViewById(R.id.albumCellContainer).findViewById(R.id.album_cell_5) + else -> throw Exception("Can't get thumbnail view for non-existent thumbnail at position: $position") + } + + fun calculateRadius(isStart: Boolean, isEnd: Boolean, outgoing: Boolean) { + val roundedDimen = context.resources.getDimension(R.dimen.message_corner_radius).toInt() + val collapsedDimen = context.resources.getDimension(R.dimen.message_corner_collapse_radius).toInt() + val (startTop, endTop, startBottom, endBottom) = when { + // single message, consistent dimen + isStart && isEnd -> intArrayOf(roundedDimen, roundedDimen, roundedDimen, roundedDimen) + // start of message cluster, collapsed BL + isStart -> intArrayOf(roundedDimen, roundedDimen, collapsedDimen, roundedDimen) + // end of message cluster, collapsed TL + isEnd -> intArrayOf(collapsedDimen, roundedDimen, roundedDimen, roundedDimen) + // else in the middle, no rounding left side + else -> intArrayOf(collapsedDimen, roundedDimen, collapsedDimen, roundedDimen) + } + // TL, TR, BR, BL (CW direction) + cornerMask.setRadii( + if (!outgoing) startTop else endTop, // TL + if (!outgoing) endTop else startTop, // TR + if (!outgoing) endBottom else startBottom, // BR + if (!outgoing) startBottom else endBottom // BL + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index dcd9e88602..86853b55ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.Manifest import android.animation.FloatEvaluator import android.animation.ValueAnimator +import android.content.Context import android.content.ClipData import android.content.ClipboardManager import android.content.Intent @@ -10,6 +11,7 @@ import android.content.res.Resources import android.database.Cursor import android.graphics.Rect import android.graphics.Typeface +import android.os.Bundle import android.net.Uri import android.os.* import android.text.TextUtils @@ -81,6 +83,8 @@ import org.thoughtcrime.securesms.giph.ui.GiphyActivity import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState +import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher +import org.thoughtcrime.securesms.loki.utilities.push import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity.Companion.selectedContactsKey import org.thoughtcrime.securesms.loki.utilities.MentionUtilities @@ -102,7 +106,8 @@ import kotlin.math.* // price we pay is a bit of back and forth between the input bar and the conversation activity. class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, - InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ConversationActionModeCallbackDelegate { + InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher, + ConversationActionModeCallbackDelegate { private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private var linkPreviewViewModel: LinkPreviewViewModel? = null private var threadID: Long = -1 @@ -130,8 +135,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val adapter = ConversationAdapter( this, cursor, - onItemPress = { message, position, view -> - handlePress(message, position, view) + onItemPress = { message, position, view, rawRect -> + handlePress(message, position, view, rawRect) }, onItemSwipeToReply = { message, position -> handleSwipeToReply(message, position) @@ -188,6 +193,28 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe markAllAsRead() } + override fun onResume() { + super.onResume() + ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadID) + } + + override fun onPause() { + super.onPause() + ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(-1) + } + + override fun getSystemService(name: String): Any? { + if (name == ActivityDispatcher.SERVICE) { + return this + } + return super.getSystemService(name) + } + + override fun dispatchIntent(body: (Context) -> Intent?) { + val intent = body(this) ?: return + push(intent, false) + } + private fun setUpRecyclerView() { conversationRecyclerView.adapter = adapter val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true) @@ -563,7 +590,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } // `position` is the adapter position; not the visual position - private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView) { + private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView, rawRect: Rect) { val actionMode = this.actionMode if (actionMode != null) { adapter.toggleSelection(message, position) @@ -579,7 +606,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // We have to use onContentClick (rather than a click listener directly on // the view) so as to not interfere with all the other gestures. Do not add // onClickListeners directly to message content views. - view.onContentClick() + view.onContentClick(rawRect) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index 63306b0c4f..c5c0d1e028 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Context import android.database.Cursor +import android.graphics.Rect import android.view.ViewGroup import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView.ViewHolder @@ -13,7 +14,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.mms.GlideRequests -class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView) -> Unit, +class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, Rect) -> Unit, private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit, private val glide: GlideRequests) : CursorRecyclerViewAdapter(context, cursor) { @@ -68,7 +69,7 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr view.messageTimestampTextView.isVisible = isSelected val position = viewHolder.adapterPosition view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide) - view.onPress = { onItemPress(message, viewHolder.adapterPosition, view) } + view.onPress = { rawX, rawY -> onItemPress(message, viewHolder.adapterPosition, view, Rect(rawX, rawY, rawX, rawY)) } view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) } view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 548ebb31f4..9cb5051031 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -1,6 +1,9 @@ package org.thoughtcrime.securesms.conversation.v2.messages import android.content.Context +import android.graphics.Rect +import android.content.res.ColorStateList +import android.graphics.Color import android.graphics.drawable.Drawable import android.text.util.Linkify import android.util.AttributeSet @@ -19,6 +22,7 @@ import network.loki.messenger.R import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.AlbumThumbnailView import org.thoughtcrime.securesms.components.emoji.EmojiTextView import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord @@ -27,7 +31,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests import kotlin.math.roundToInt class VisibleMessageContentView : LinearLayout { - var onContentClick: (() -> Unit)? = null + var onContentClick: ((rawRect: Rect) -> Unit)? = null var onContentDoubleTap: (() -> Unit)? = null // region Lifecycle @@ -85,9 +89,17 @@ class VisibleMessageContentView : LinearLayout { documentView.bind(message, VisibleMessageContentView.getTextColor(context, message)) mainContainer.addView(documentView) } else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) { - val dummyTextView = TextView(context) - dummyTextView.text = "asifuygaihsfo" - mainContainer.addView(dummyTextView) + val albumThumbnailView = AlbumThumbnailView(context) + mainContainer.addView(albumThumbnailView) + // 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 + albumThumbnailView.bind( + glideRequests = glide, + message = message, + isStart = isStartOfMessageCluster, + isEnd = isEndOfMessageCluster + ) + onContentClick = { albumThumbnailView.calculateHitObject(it, message) } } else if (message.isOpenGroupInvitation) { val openGroupInvitationView = OpenGroupInvitationView(context) openGroupInvitationView.bind(message, VisibleMessageContentView.getTextColor(context, message)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index c8d91496d2..88e4ee9ced 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -9,12 +9,10 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.util.AttributeSet -import android.util.Log import android.view.* import android.widget.LinearLayout import androidx.core.content.ContextCompat import androidx.core.view.isVisible -import kotlinx.android.synthetic.main.view_conversation.view.* import kotlinx.android.synthetic.main.view_visible_message.view.* import kotlinx.android.synthetic.main.view_visible_message.view.profilePictureView import network.loki.messenger.R @@ -47,7 +45,7 @@ class VisibleMessageView : LinearLayout { private var onDoubleTap: (() -> Unit)? = null var snIsSelected = false set(value) { field = value; handleIsSelectedChanged()} - var onPress: (() -> Unit)? = null + var onPress: ((rawX: Int, rawY: Int) -> Unit)? = null var onSwipeToReply: (() -> Unit)? = null var onLongPress: (() -> Unit)? = null @@ -279,7 +277,7 @@ class VisibleMessageView : LinearLayout { this.pressCallback = null onDoubleTap?.invoke() } else { - val newPressCallback = Runnable { onPress() } + val newPressCallback = Runnable { onPress(event.rawX.toInt(), event.rawY.toInt()) } this.pressCallback = newPressCallback gestureHandler.postDelayed(newPressCallback, VisibleMessageView.maxDoubleTapInterval) } @@ -307,13 +305,13 @@ class VisibleMessageView : LinearLayout { onLongPress?.invoke() } - private fun onPress() { - onPress?.invoke() - pressCallback = null + fun onContentClick(rawRect: Rect) { + messageContentView.onContentClick?.invoke(rawRect) } - fun onContentClick() { - messageContentView.onContentClick?.invoke() + private fun onPress(rawX: Int, rawY: Int) { + onPress?.invoke(rawX, rawY) + pressCallback = null } // endregion } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt new file mode 100644 index 0000000000..b070264944 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/KThumbnailView.kt @@ -0,0 +1,197 @@ +package org.thoughtcrime.securesms.conversation.v2.utilities + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.net.Uri +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import androidx.core.view.isVisible +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions +import com.bumptech.glide.request.RequestOptions +import kotlinx.android.synthetic.main.thumbnail_view.view.* +import network.loki.messenger.R +import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress +import org.session.libsession.utilities.Util.equals +import org.session.libsignal.utilities.ListenableFuture +import org.session.libsignal.utilities.SettableFuture +import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget +import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget +import org.thoughtcrime.securesms.mms.* +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri + +open class KThumbnailView: FrameLayout { + + companion object { + private const val WIDTH = 0 + private const val HEIGHT = 1 + } + + // region Lifecycle + 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 val image by lazy { thumbnail_image } + private val playOverlay by lazy { play_overlay } + private val captionIcon by lazy { thumbnail_caption_icon } + val loadIndicator: View by lazy { thumbnail_load_indicator } + + private val dimensDelegate = ThumbnailDimensDelegate() + + var thumbnailClickListener: SlideClickListener? = null + + private var slide: Slide? = null + private var radius: Int = 0 + + private fun initialize(attrs: AttributeSet?) { + inflate(context, R.layout.thumbnail_view, this) + if (attrs != null) { + val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0) + + dimensDelegate.setBounds(typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0), + typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0), + typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0), + typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0)) + + radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0) + + typedArray.recycle() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val adjustedDimens = dimensDelegate.resourceSize() + if (adjustedDimens[WIDTH] == 0 && adjustedDimens[HEIGHT] == 0) { + return super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + + val finalWidth: Int = adjustedDimens[WIDTH] + paddingLeft + paddingRight + val finalHeight: Int = adjustedDimens[HEIGHT] + paddingTop + paddingBottom + + super.onMeasure( + MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY) + ) + } + + private fun getDefaultWidth() = maxOf(layoutParams?.width ?: 0, 0) + private fun getDefaultHeight() = maxOf(layoutParams?.height ?: 0, 0) + // endregion + + // region Interaction + fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean): ListenableFuture { + return setImageResource(glide, slide, isPreview, 0, 0) + } + + fun setImageResource(glide: GlideRequests, slide: Slide, + isPreview: Boolean, naturalWidth: Int, + naturalHeight: Int): ListenableFuture { + + val currentSlide = this.slide + + playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() && + (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview)) + + if (equals(currentSlide, slide)) { + // don't re-load slide + return SettableFuture(false) + } + + + if (currentSlide != null && currentSlide.fastPreflightId != null && currentSlide.fastPreflightId == slide.fastPreflightId) { + // not reloading slide for fast preflight + this.slide = slide + } + + this.slide = slide + + captionIcon.isVisible = slide.caption.isPresent + loadIndicator.isVisible = slide.isInProgress + + dimensDelegate.setDimens(naturalWidth, naturalHeight) + invalidate() + + val result = SettableFuture() + + when { + slide.thumbnailUri != null -> { + buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(image, result)) + } + slide.hasPlaceholder() -> { + buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(image, result)) + } + else -> { + glide.clear(image) + result.set(false) + } + } + return result + } + + fun buildThumbnailGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest { + + val dimens = dimensDelegate.resourceSize() + + val request = glide.load(DecryptableUri(slide.thumbnailUri!!)) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .let { request -> + if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) { + request.override(getDefaultWidth(), getDefaultHeight()) + } else { + request.override(dimens[WIDTH], dimens[HEIGHT]) + } + } + .transition(DrawableTransitionOptions.withCrossFade()) + .centerCrop() + + return if (slide.isInProgress) request else request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture)) + } + + fun buildPlaceholderGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest { + + val dimens = dimensDelegate.resourceSize() + + return glide.asBitmap() + .load(slide.getPlaceholderRes(context.theme)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .let { request -> + if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) { + request.override(getDefaultWidth(), getDefaultHeight()) + } else { + request.override(dimens[WIDTH], dimens[HEIGHT]) + } + } + .fitCenter() + } + + open fun clear(glideRequests: GlideRequests) { + glideRequests.clear(image) + slide = null + } + + fun setImageResource(glideRequests: GlideRequests, uri: Uri): ListenableFuture { + val future = SettableFuture() + + var request: GlideRequest = glideRequests.load(DecryptableUri(uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .transition(DrawableTransitionOptions.withCrossFade()) + + request = if (radius > 0) { + request.transforms(CenterCrop(), RoundedCorners(radius)) + } else { + request.transforms(CenterCrop()) + } + + request.into(GlideDrawableListeningTarget(image, future)) + + return future + } + + // endregion + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailDimensDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailDimensDelegate.kt new file mode 100644 index 0000000000..fb50d8de62 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailDimensDelegate.kt @@ -0,0 +1,91 @@ +package org.thoughtcrime.securesms.conversation.v2.utilities + +class ThumbnailDimensDelegate { + + companion object { + // dimens array constants + private const val WIDTH = 0 + private const val HEIGHT = 1 + private const val DIMENS_ARRAY_SIZE = 2 + + // bounds array constants + private const val MIN_WIDTH = 0 + private const val MIN_HEIGHT = 1 + private const val MAX_WIDTH = 2 + private const val MAX_HEIGHT = 3 + private const val BOUNDS_ARRAY_SIZE = 4 + + // const zero int array + private val EMPTY_DIMENS = intArrayOf(0,0) + + } + + private val measured: IntArray = IntArray(DIMENS_ARRAY_SIZE) + private val dimens: IntArray = IntArray(DIMENS_ARRAY_SIZE) + private val bounds: IntArray = IntArray(BOUNDS_ARRAY_SIZE) + + fun resourceSize(): IntArray { + if (dimens.all { it == 0 }) { + // dimens are (0, 0), don't go any further + return EMPTY_DIMENS + } + + val naturalWidth = dimens[WIDTH].toDouble() + val naturalHeight = dimens[HEIGHT].toDouble() + val minWidth = dimens[MIN_WIDTH] + val maxWidth = dimens[MAX_WIDTH] + val minHeight = dimens[MIN_HEIGHT] + val maxHeight = dimens[MAX_HEIGHT] + + // calculate actual measured + var measuredWidth: Double = naturalWidth + var measuredHeight: Double = naturalHeight + + val widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth + val heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight + + if (!widthInBounds || !heightInBounds) { + val minWidthRatio: Double = naturalWidth / minWidth + val maxWidthRatio: Double = naturalWidth / maxWidth + val minHeightRatio: Double = naturalHeight / minHeight + val maxHeightRatio: Double = naturalHeight / maxHeight + if (maxWidthRatio > 1 || maxHeightRatio > 1) { + if (maxWidthRatio >= maxHeightRatio) { + measuredWidth /= maxWidthRatio + measuredHeight /= maxWidthRatio + } else { + measuredWidth /= maxHeightRatio + measuredHeight /= maxHeightRatio + } + measuredWidth = Math.max(measuredWidth, minWidth.toDouble()) + measuredHeight = Math.max(measuredHeight, minHeight.toDouble()) + } else if (minWidthRatio < 1 || minHeightRatio < 1) { + if (minWidthRatio <= minHeightRatio) { + measuredWidth /= minWidthRatio + measuredHeight /= minWidthRatio + } else { + measuredWidth /= minHeightRatio + measuredHeight /= minHeightRatio + } + measuredWidth = Math.min(measuredWidth, maxWidth.toDouble()) + measuredHeight = Math.min(measuredHeight, maxHeight.toDouble()) + } + } + measured[WIDTH] = measuredWidth.toInt() + measured[HEIGHT] = measuredHeight.toInt() + return measured + } + + fun setBounds(minWidth: Int, minHeight: Int, maxWidth: Int, maxHeight: Int) { + bounds[MIN_WIDTH] = minWidth + bounds[MIN_HEIGHT] = minHeight + bounds[MAX_WIDTH] = maxWidth + bounds[MAX_HEIGHT] = maxHeight + } + + fun setDimens(width: Int, height: Int) { + dimens[WIDTH] = width + dimens[HEIGHT] = height + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt new file mode 100644 index 0000000000..60ef8116d2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt @@ -0,0 +1,64 @@ +package org.thoughtcrime.securesms.conversation.v2.utilities + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Interpolator +import android.graphics.Paint +import android.graphics.Rect +import android.os.SystemClock +import android.util.AttributeSet +import android.util.Log +import android.view.View +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.Animation +import android.view.animation.AnimationSet +import android.view.animation.AnimationUtils +import androidx.core.content.res.ResourcesCompat +import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView +import network.loki.messenger.R +import kotlin.math.sin + +class ThumbnailProgressBar: View { + + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + private val firstX: Double + get() = sin(SystemClock.elapsedRealtime() / 300.0) * 1.5 + + private val secondX: Double + get() = sin(SystemClock.elapsedRealtime() / 300.0 + (Math.PI/4)) * 1.5 + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = ResourcesCompat.getColor(resources, R.color.accent, null) + } + + private val objectRect = Rect() + private val drawingRect = Rect() + + override fun dispatchDraw(canvas: Canvas?) { + if (canvas == null) return + + getDrawingRect(objectRect) + drawingRect.set(objectRect) + + val coercedFX = firstX + val coercedSX = secondX + + val firstMeasuredX = objectRect.left + (objectRect.width() * coercedFX) + val secondMeasuredX = objectRect.left + (objectRect.width() * coercedSX) + + drawingRect.set( + (if (firstMeasuredX < secondMeasuredX) firstMeasuredX else secondMeasuredX).toInt(), + objectRect.top, + (if (firstMeasuredX < secondMeasuredX) secondMeasuredX else firstMeasuredX).toInt(), + objectRect.bottom + ) + + canvas.drawRect(drawingRect, paint) + invalidate() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java index d0d7c8a605..79d0099998 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailView.java @@ -97,6 +97,7 @@ public class ThumbnailView extends FrameLayout { 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ActivityUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ActivityUtilities.kt index 6fd9250e0d..4986a1ce36 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ActivityUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/utilities/ActivityUtilities.kt @@ -1,6 +1,10 @@ package org.thoughtcrime.securesms.loki.utilities +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context import android.content.Intent +import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar @@ -52,4 +56,13 @@ fun AppCompatActivity.show(intent: Intent, isForResult: Boolean = false) { startActivity(intent) } overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) +} + +interface ActivityDispatcher { + companion object { + const val SERVICE = "ActivityDispatcher_SERVICE" + @SuppressLint("WrongConstant") + fun get(context: Context) = context.getSystemService(SERVICE) as? ActivityDispatcher + } + fun dispatchIntent(body: (Context)->Intent?) } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java b/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java index d3c683dd46..adb8a921b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java @@ -2,36 +2,20 @@ package org.thoughtcrime.securesms.longmessage; import android.content.Context; import android.content.Intent; -import android.graphics.PorterDuff; import android.os.Bundle; -import android.text.SpannableString; import android.text.method.LinkMovementMethod; -import android.text.style.URLSpan; -import android.text.util.Linkify; -import android.util.TypedValue; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.lifecycle.ViewModelProvider; -import com.annimon.stream.Stream; - -import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; -import org.thoughtcrime.securesms.components.ConversationItemFooter; -import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; - import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.TextSecurePreferences; -import org.session.libsession.utilities.ThemeUtil; import org.session.libsession.utilities.Util; -import org.session.libsession.utilities.Stub; - -import java.util.Locale; +import org.session.libsession.utilities.recipients.Recipient; +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; +import org.thoughtcrime.securesms.loki.utilities.MentionUtilities; import network.loki.messenger.R; @@ -43,8 +27,7 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity { private static final int MAX_DISPLAY_LENGTH = 64 * 1024; - private Stub sentBubble; - private Stub receivedBubble; + private TextView textBody; private LongMessageViewModel viewModel; @@ -60,9 +43,7 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity { protected void onCreate(Bundle savedInstanceState, boolean ready) { super.onCreate(savedInstanceState, ready); setContentView(R.layout.longmessage_activity); - - sentBubble = new Stub<>(findViewById(R.id.longmessage_sent_stub)); - receivedBubble = new Stub<>(findViewById(R.id.longmessage_received_stub)); + textBody = findViewById(R.id.longmessage_text); initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), getIntent().getBooleanExtra(KEY_IS_MMS, false)); } @@ -93,36 +74,19 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity { return; } - if (message.get().getMessageRecord().isOutgoing()) { getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_your_message)); } else { Recipient recipient = message.get().getMessageRecord().getRecipient(); - String name = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize()) ; + String name = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize()); getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_message_from_s, name)); } - ViewGroup bubble; + String trimmedBody = getTrimmedBody(message.get().getFullBody()); + String mentionBody = MentionUtilities.highlightMentions(trimmedBody, message.get().getMessageRecord().getThreadId(), this); - if (message.get().getMessageRecord().isOutgoing()) { - bubble = sentBubble.get(); - bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.message_sent_background_color), PorterDuff.Mode.MULTIPLY); - } else { - bubble = receivedBubble.get(); - bubble.getBackground().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.message_received_background_color), PorterDuff.Mode.MULTIPLY); - } - - TextView text = bubble.findViewById(R.id.longmessage_text); - ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer); - - String trimmedBody = getTrimmedBody(message.get().getFullBody()); - SpannableString styledBody = linkifyMessageBody(new SpannableString(trimmedBody)); - - bubble.setVisibility(View.VISIBLE); - text.setText(styledBody); - text.setMovementMethod(LinkMovementMethod.getInstance()); - text.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(this)); - footer.setMessageRecord(message.get().getMessageRecord(), Locale.getDefault()); + textBody.setText(mentionBody); + textBody.setMovementMethod(LinkMovementMethod.getInstance()); }); } @@ -131,15 +95,4 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity { : text.substring(0, MAX_DISPLAY_LENGTH); } - private SpannableString linkifyMessageBody(SpannableString messageBody) { - int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS; - boolean hasLinks = Linkify.addLinks(messageBody, linkPattern); - - if (hasLinks) { - Stream.of(messageBody.getSpans(0, messageBody.length(), URLSpan.class)) - .filterNot(url -> LinkPreviewUtil.isLegalUrl(url.getURL())) - .forEach(messageBody::removeSpan); - } - return messageBody; - } } diff --git a/app/src/main/res/layout/album_thumbnail_1.xml b/app/src/main/res/layout/album_thumbnail_1.xml new file mode 100644 index 0000000000..cf0f5d4892 --- /dev/null +++ b/app/src/main/res/layout/album_thumbnail_1.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/album_thumbnail_2.xml b/app/src/main/res/layout/album_thumbnail_2.xml index 6ce179ea6a..9ac5866113 100644 --- a/app/src/main/res/layout/album_thumbnail_2.xml +++ b/app/src/main/res/layout/album_thumbnail_2.xml @@ -7,13 +7,13 @@ android:layout_width="@dimen/album_total_width" android:layout_height="@dimen/album_2_total_height"> - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/album_thumbnail_view.xml b/app/src/main/res/layout/album_thumbnail_view.xml index 11af8b53fb..d9b566976d 100644 --- a/app/src/main/res/layout/album_thumbnail_view.xml +++ b/app/src/main/res/layout/album_thumbnail_view.xml @@ -1,21 +1,80 @@ - + + /> - \ No newline at end of file + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/longmessage_activity.xml b/app/src/main/res/layout/longmessage_activity.xml index 49f0641ab5..ed933b2355 100644 --- a/app/src/main/res/layout/longmessage_activity.xml +++ b/app/src/main/res/layout/longmessage_activity.xml @@ -4,23 +4,18 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - + android:layout_height="wrap_content"/> - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/thumbnail_view.xml b/app/src/main/res/layout/thumbnail_view.xml index a22164f376..b20b58356d 100644 --- a/app/src/main/res/layout/thumbnail_view.xml +++ b/app/src/main/res/layout/thumbnail_view.xml @@ -22,15 +22,12 @@ android:src="@drawable/ic_caption_28" android:visibility="gone" /> -