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 c439862bac..b536e36ea8 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 @@ -234,7 +234,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe setUpLinkPreviewObserver() restoreDraftIfNeeded() addOpenGroupGuidelinesIfNeeded() - scrollToBottomButton.setOnClickListener { conversationRecyclerView.smoothScrollToPosition(0) } + scrollToBottomButton.setOnClickListener { + val layoutManager = conversationRecyclerView.layoutManager ?: return@setOnClickListener + if (layoutManager.isSmoothScrolling) { + conversationRecyclerView.scrollToPosition(0) + } else { + conversationRecyclerView.smoothScrollToPosition(0) + } + } unreadCount = DatabaseFactory.getMmsSmsDatabase(this).getUnreadCount(threadID) updateUnreadCountIndicator() setUpTypingObserver() @@ -1338,12 +1345,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun copyMessages(messages: Set) { val sortedMessages = messages.sortedBy { it.dateSent } + val messageSize = sortedMessages.size val builder = StringBuilder() - for (message in sortedMessages) { + val messageIterator = sortedMessages.iterator() + while (messageIterator.hasNext()) { + val message = messageIterator.next() val body = MentionUtilities.highlightMentions(message.body, threadID, this) if (TextUtils.isEmpty(body)) { continue } - val formattedTimestamp = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), message.timestamp) - builder.append("$formattedTimestamp: $body").append('\n') + if (messageSize > 1) { + val formattedTimestamp = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), message.timestamp) + builder.append("$formattedTimestamp: ") + } + builder.append(body) + if (messageIterator.hasNext()) { + builder.append('\n') + } } if (builder.isNotEmpty() && builder[builder.length - 1] == '\n') { builder.deleteCharAt(builder.length - 1) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt new file mode 100644 index 0000000000..859f208a52 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ModalUrlBottomSheet.kt @@ -0,0 +1,72 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context.CLIPBOARD_SERVICE +import android.content.Intent +import android.graphics.Typeface +import android.net.Uri +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.style.StyleSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.android.synthetic.main.fragment_modal_url_bottom_sheet.* +import network.loki.messenger.R +import org.thoughtcrime.securesms.util.UiModeUtilities + +class ModalUrlBottomSheet(private val url: String): BottomSheetDialogFragment(), View.OnClickListener { + + override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_modal_url_bottom_sheet, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val explanation = resources.getString(R.string.dialog_open_url_explanation, url) + val spannable = SpannableStringBuilder(explanation) + val startIndex = explanation.indexOf(url) + spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + openURLExplanationTextView.text = spannable + cancelButton.setOnClickListener(this) + copyButton.setOnClickListener(this) + openURLButton.setOnClickListener(this) + } + + private fun open() { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + requireContext().startActivity(intent) + } catch (e: Exception) { + Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_SHORT).show() + } + dismiss() + } + + private fun copy() { + val clip = ClipData.newPlainText("URL", url) + val manager = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + manager.setPrimaryClip(clip) + Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + dismiss() + } + + override fun onStart() { + super.onStart() + val window = dialog?.window ?: return + val isLightMode = UiModeUtilities.isDayUiMode(requireContext()) + window.setDimAmount(if (isLightMode) 0.1f else 0.75f) + } + + override fun onClick(v: View?) { + when (v) { + openURLButton -> open() + copyButton -> copy() + cancelButton -> dismiss() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java index 5a04e77ac2..6765232c77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/ExpirationTimerView.java @@ -1,16 +1,18 @@ package org.thoughtcrime.securesms.conversation.v2.components; import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; -import network.loki.messenger.R; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.session.libsession.utilities.Util; import java.lang.ref.WeakReference; import java.util.concurrent.TimeUnit; +import network.loki.messenger.R; + public class ExpirationTimerView extends androidx.appcompat.widget.AppCompatImageView { private long startedAt; @@ -86,10 +88,12 @@ public class ExpirationTimerView extends androidx.appcompat.widget.AppCompatImag long progressed = System.currentTimeMillis() - startedAt; long remaining = expiresIn - progressed; - if (remaining < TimeUnit.SECONDS.toMillis(30)) { - return 50; - } else { + if (remaining <= 0) { + return 0; + } else if (remaining < TimeUnit.SECONDS.toMillis(30)) { return 1000; + } else { + return 5000; } } @@ -106,16 +110,20 @@ public class ExpirationTimerView extends androidx.appcompat.widget.AppCompatImag ExpirationTimerView timerView = expirationTimerViewReference.get(); if (timerView == null) return; - timerView.setExpirationTime(timerView.startedAt, timerView.expiresIn); - + long nextUpdate = timerView.calculateAnimationDelay(timerView.startedAt, timerView.expiresIn); synchronized (timerView) { - if (!timerView.visible) { + if (timerView.visible) { + timerView.setExpirationTime(timerView.startedAt, timerView.expiresIn); + } else { + timerView.stopped = true; + return; + } + if (nextUpdate <= 0) { timerView.stopped = true; return; } } - - Util.runOnMainDelayed(this, timerView.calculateAnimationDelay(timerView.startedAt, timerView.expiresIn)); + Util.runOnMainDelayed(this, nextUpdate); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/OpenURLDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/OpenURLDialog.kt deleted file mode 100644 index ea0230f578..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/OpenURLDialog.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.dialogs - -import android.content.Intent -import android.graphics.Typeface -import android.net.Uri -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.style.StyleSpan -import android.view.LayoutInflater -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import kotlinx.android.synthetic.main.dialog_open_url.view.* -import network.loki.messenger.R -import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog - -/** Shown upon tapping a URL. */ -class OpenURLDialog(private val url: String) : BaseDialog() { - - override fun setContentView(builder: AlertDialog.Builder) { - val contentView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_open_url, null) - val explanation = resources.getString(R.string.dialog_open_url_explanation, url) - val spannable = SpannableStringBuilder(explanation) - val startIndex = explanation.indexOf(url) - spannable.setSpan(StyleSpan(Typeface.BOLD), startIndex, startIndex + url.count(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - contentView.openURLExplanationTextView.text = spannable - contentView.cancelButton.setOnClickListener { dismiss() } - contentView.openURLButton.setOnClickListener { open() } - builder.setView(contentView) - } - - private fun open() { - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - requireContext().startActivity(intent) - } catch (e: Exception) { - Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_SHORT).show() - } - dismiss() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt index 0457f82702..45921122f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation.v2.messages import android.content.Context import android.graphics.Canvas import android.graphics.Rect -import android.text.method.LinkMovementMethod import android.util.AttributeSet import android.view.LayoutInflater import android.view.MotionEvent @@ -11,20 +10,17 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.core.content.res.ResourcesCompat -import androidx.core.text.getSpans -import androidx.core.text.toSpannable import androidx.core.view.isVisible import kotlinx.android.synthetic.main.view_link_preview.view.* import network.loki.messenger.R import org.thoughtcrime.securesms.components.CornerMask -import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog +import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities -import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans import org.thoughtcrime.securesms.database.model.MmsMessageRecord -import org.thoughtcrime.securesms.util.UiModeUtilities import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.ImageSlide +import org.thoughtcrime.securesms.util.UiModeUtilities class LinkPreviewView : LinearLayout { private val cornerMask by lazy { CornerMask(this) } @@ -97,7 +93,7 @@ class LinkPreviewView : LinearLayout { fun openURL() { val url = this.url ?: return val activity = context as AppCompatActivity - OpenURLDialog(url).show(activity.supportFragmentManager, "Open URL Dialog") + ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog") } // endregion } \ No newline at end of file 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 38831ed5ab..f98b3a23dd 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 @@ -31,8 +31,8 @@ import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.components.emoji.EmojiTextView import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.conversation.v2.ModalUrlBottomSheet import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView -import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans @@ -237,7 +237,7 @@ class VisibleMessageContentView : LinearLayout { val updatedUrl = urlSpan.url.let { HttpUrl.parse(it).toString() } val replacementSpan = ModalURLSpan(updatedUrl) { url -> val activity = context as AppCompatActivity - OpenURLDialog(url).show(activity.supportFragmentManager, "Open URL Dialog") + ModalUrlBottomSheet(url).show(activity.supportFragmentManager, "Open URL Dialog") } val start = body.getSpanStart(urlSpan) val end = body.getSpanEnd(urlSpan) 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 e57ea8035b..4579281e17 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 @@ -218,7 +218,9 @@ class VisibleMessageView : LinearLayout { if (message.expireStarted + message.expiresIn <= System.currentTimeMillis()) { ApplicationContext.getInstance(context).expiringMessageManager.checkSchedule() } - } else if (!message.isOutgoing && !message.isMediaPending) { + } else if (!message.isMediaPending) { + expirationTimerView.setPercentComplete(0.0f) + expirationTimerView.stopAnimation() ThreadUtils.queue { val expirationManager = ApplicationContext.getInstance(context).expiringMessageManager val id = message.getId() @@ -226,6 +228,9 @@ class VisibleMessageView : LinearLayout { if (mms) DatabaseFactory.getMmsDatabase(context).markExpireStarted(id) else DatabaseFactory.getSmsDatabase(context).markExpireStarted(id) expirationManager.scheduleDeletion(id, mms, message.expiresIn) } + } else { + expirationTimerView.stopAnimation() + expirationTimerView.setPercentComplete(0.0f) } } else { expirationTimerView.isVisible = false diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index 28531ff4cc..dbc5710ade 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -46,14 +46,20 @@ class ConversationView : LinearLayout { accentView.visibility = View.VISIBLE } else { accentView.setBackgroundResource(R.color.accent) - accentView.visibility = if (unreadCount > 0) View.VISIBLE else View.INVISIBLE + // Using thread.isRead we can determine if the last message was our own, and display it as 'read' even though previous messages may not be + // This would also not trigger the disappearing message timer which may or may not be desirable + accentView.visibility = if (unreadCount > 0 && !thread.isRead) View.VISIBLE else View.INVISIBLE + } + val formattedUnreadCount = if (thread.isRead) { + null + } else { + if (unreadCount < 100) unreadCount.toString() else "99+" } - val formattedUnreadCount = if (unreadCount < 100) unreadCount.toString() else "99+" unreadCountTextView.text = formattedUnreadCount val textSize = if (unreadCount < 100) 12.0f else 9.0f unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize) unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL) - unreadCountIndicator.isVisible = (unreadCount != 0) + unreadCountIndicator.isVisible = (unreadCount != 0 && !thread.isRead) val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString() conversationViewDisplayNameTextView.text = senderDisplayName @@ -69,7 +75,7 @@ class ConversationView : LinearLayout { val rawSnippet = thread.getDisplayBody(context) val snippet = highlightMentions(rawSnippet, thread.threadId, context) snippetTextView.text = snippet - snippetTextView.typeface = if (unreadCount > 0) Typeface.DEFAULT_BOLD else Typeface.DEFAULT + snippetTextView.typeface = if (unreadCount > 0 && !thread.isRead) Typeface.DEFAULT_BOLD else Typeface.DEFAULT snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE if (isTyping) { typingIndicatorView.startAnimation() diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index bc0c48eaf5..de059b217b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -33,15 +33,11 @@ import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities -import org.thoughtcrime.securesms.util.UiModeUtilities -import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints -import org.thoughtcrime.securesms.util.BitmapDecodingException -import org.thoughtcrime.securesms.util.BitmapUtil +import org.thoughtcrime.securesms.util.* import java.io.File import java.security.SecureRandom import java.util.* @@ -85,6 +81,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { chatsButton.setOnClickListener { showChatSettings() } sendInvitationButton.setOnClickListener { sendInvitation() } faqButton.setOnClickListener { showFAQ() } + surveyButton.setOnClickListener { showSurvey() } helpTranslateButton.setOnClickListener { helpTranslate() } seedButton.setOnClickListener { showSeed() } clearAllDataButton.setOnClickListener { clearAllData() } @@ -296,6 +293,16 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } } + private fun showSurvey() { + try { + val url = "https://getsession.org/survey" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(intent) + } catch (e: Exception) { + Toast.makeText(this, "Can't open URL", Toast.LENGTH_LONG).show() + } + } + private fun helpTranslate() { try { val url = "https://crowdin.com/project/session-android" diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 0e8bf779ff..39c229df9c 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -193,6 +193,7 @@ android:background="?android:dividerHorizontal" /> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aaa000eb8f..0494478661 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -873,6 +873,7 @@ Open URL? Are you sure you want to open %s? Open + Copy URL Enable Link Previews? Enabling link previews will show previews for URLs you send and receive. This can be useful, but Session will need to contact linked websites to generate previews. You can always disable link previews in Session\'s settings. @@ -898,5 +899,6 @@ Delete just for me Delete for everyone Delete for me and %s + Feedback/Survey diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index f844b0ae35..97513d9948 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -287,7 +287,8 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli val userPublicKey = TextSecurePreferences.getLocalNumber(context) // Create the group val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) - if (storage.getGroup(groupID) != null) { + val groupExists = storage.getGroup(groupID) != null + if (groupExists) { // Update the group if (!storage.isGroupActive(groupPublicKey)) { // Clear zombie list if the group wasn't active @@ -311,10 +312,10 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli // Notify the PN server PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!) // Notify the user - if (userPublicKey == sender) { + if (userPublicKey == sender && !groupExists) { val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp) - } else { + } else if (userPublicKey != sender) { storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, sentTimestamp) } // Start polling