mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Handle offline state when pinning messages.
This commit is contained in:
committed by
jeffrey-signal
parent
0f5b790461
commit
7297f7a894
@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.polls.PollRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
@@ -120,8 +121,12 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment()
|
||||
.setTitle(R.string.PinnedMessage__unpin_title)
|
||||
.setMessage(getString(R.string.PinnedMessage__unpin_body))
|
||||
.setPositiveButton(R.string.PinnedMessage__unpin) { dialog, which ->
|
||||
viewModel.unpinMessage()
|
||||
dismissAllowingStateLoss()
|
||||
if (NetworkUtil.isConnected(requireContext())) {
|
||||
viewModel.unpinMessage()
|
||||
dismissAllowingStateLoss()
|
||||
} else {
|
||||
showNetworkErrorDialog()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, which -> dialog.dismiss() }
|
||||
.show()
|
||||
@@ -129,6 +134,14 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment()
|
||||
unpinAll.visible = requireArguments().getBoolean(KEY_CAN_UNPIN)
|
||||
}
|
||||
|
||||
private fun showNetworkErrorDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinnedMessage__couldnt_unpin_all)
|
||||
.setMessage(getString(R.string.PinnedMessage__check_connection))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun initializeGiphyMp4(videoContainer: ViewGroup, list: RecyclerView): GiphyMp4ProjectionRecycler {
|
||||
val maxPlayback = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInConversation()
|
||||
val holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(
|
||||
|
||||
@@ -407,6 +407,7 @@ class ConversationFragment :
|
||||
companion object {
|
||||
private val TAG = Log.tag(ConversationFragment::class.java)
|
||||
private val POLL_SPINNER_DELAY = 500.milliseconds
|
||||
private val PIN_SPINNER_DELAY = 500.milliseconds
|
||||
|
||||
private const val ACTION_PINNED_SHORTCUT = "action_pinned_shortcut"
|
||||
private const val SAVED_STATE_IS_SEARCH_REQUESTED = "is_search_requested"
|
||||
@@ -1728,7 +1729,23 @@ class ConversationFragment :
|
||||
duration = if (values[selection] == -1) kotlin.time.Duration.INFINITE else values[selection].days,
|
||||
threadRecipient = conversationMessage.threadRecipient
|
||||
)
|
||||
.subscribe()
|
||||
.doOnSubscribe {
|
||||
handler.postDelayed({ showSpinner() }, PIN_SPINNER_DELAY.inWholeMilliseconds)
|
||||
}
|
||||
.doFinally {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
hideSpinner()
|
||||
}
|
||||
.subscribeBy(
|
||||
onError = {
|
||||
Log.w(TAG, "Error received during pin message!", it)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinnedMessage__couldnt_pin)
|
||||
.setMessage(getString(R.string.PinnedMessage__check_connection))
|
||||
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> dialog!!.dismiss() }
|
||||
.show()
|
||||
}
|
||||
)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
@@ -1738,7 +1755,25 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
private fun handleUnpinMessage(messageId: Long) {
|
||||
viewModel.unpinMessage(messageId)
|
||||
disposables += viewModel
|
||||
.unpinMessage(messageId)
|
||||
.doOnSubscribe {
|
||||
handler.postDelayed({ showSpinner() }, PIN_SPINNER_DELAY.inWholeMilliseconds)
|
||||
}
|
||||
.doFinally {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
hideSpinner()
|
||||
}
|
||||
.subscribeBy(
|
||||
onError = {
|
||||
Log.w(TAG, "Error received during unpin message!", it)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.PinnedMessage__couldnt_unpin)
|
||||
.setMessage(getString(R.string.PinnedMessage__check_connection))
|
||||
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> dialog!!.dismiss() }
|
||||
.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleVideoCall() {
|
||||
|
||||
@@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies.expiringMessageMa
|
||||
import org.thoughtcrime.securesms.groups.GroupNotAMemberException
|
||||
import org.thoughtcrime.securesms.jobs.GroupSendJobHelper
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob
|
||||
import org.thoughtcrime.securesms.jobs.UnpinMessageJob
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardUtil
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore.Companion.settings
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
@@ -307,6 +308,11 @@ class ConversationRepository(
|
||||
|
||||
fun pinMessage(messageRecord: MessageRecord, duration: Duration, threadRecipient: Recipient): Completable {
|
||||
return Completable.create { emitter ->
|
||||
val isGroup = threadRecipient.isPushV2Group
|
||||
if (isGroup && threadRecipient.groupId.getOrNull()?.isV2 != true) {
|
||||
emitter.tryOnError(Exception("Pin message failed - missing group id"))
|
||||
}
|
||||
|
||||
val message = OutgoingMessage.pinMessage(
|
||||
threadRecipient = threadRecipient,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
@@ -323,13 +329,89 @@ class ConversationRepository(
|
||||
|
||||
Log.i(TAG, "Sending pin create to ${message.threadRecipient.id}, thread: ${messageRecord.threadId}")
|
||||
|
||||
MessageSender.send(
|
||||
AppDependencies.application,
|
||||
message,
|
||||
messageRecord.threadId,
|
||||
MessageSender.SendType.SIGNAL,
|
||||
null
|
||||
) { emitter.onComplete() }
|
||||
val possibleTargets: List<Recipient> = if (isGroup) {
|
||||
SignalDatabase.groups.getGroupMembers(threadRecipient.requireGroupId().requireV2(), GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF).map { it.resolve() }.distinctBy { it.id }
|
||||
} else {
|
||||
listOf(threadRecipient)
|
||||
}
|
||||
|
||||
val eligibleTargets = RecipientUtil.getEligibleForSending(possibleTargets)
|
||||
val results = PinSendUtil.sendPinMessage(applicationContext, threadRecipient, message, eligibleTargets)
|
||||
|
||||
val sendResults = GroupSendJobHelper.getCompletedSends(eligibleTargets, results)
|
||||
|
||||
if (sendResults.completed.isNotEmpty() || possibleTargets.isEmpty()) {
|
||||
val allocatedThreadId = SignalDatabase.threads.getOrCreateValidThreadId(threadRecipient, messageRecord.threadId, message.distributionType)
|
||||
val outgoingMessage = applyUniversalExpireTimerIfNecessary(applicationContext, threadRecipient, message, allocatedThreadId)
|
||||
val insertResult = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, allocatedThreadId, false, null)
|
||||
|
||||
SignalDatabase.threads.update(threadId = allocatedThreadId, unarchive = true, syncThreadDelete = true)
|
||||
databaseObserver.notifyConversationListeners(messageRecord.threadId)
|
||||
if (outgoingMessage.expiresIn > 0) {
|
||||
SignalDatabase.messages.markExpireStarted(insertResult.messageId)
|
||||
expiringMessageManager.scheduleDeletion(insertResult.messageId, true, message.expiresIn)
|
||||
}
|
||||
|
||||
if (sendResults.skipped.isNotEmpty()) {
|
||||
val messageRecord = SignalDatabase.messages.getMessageRecord(insertResult.messageId)
|
||||
val filterRecipientIds = (sendResults.skipped - sendResults.completed.map { it.id }).toSet()
|
||||
Log.i(TAG, "Some recipients skipped when sending pin message. Resending to $filterRecipientIds")
|
||||
MessageSender.resendGroupMessage(applicationContext, messageRecord, filterRecipientIds)
|
||||
} else {
|
||||
SignalDatabase.messages.markAsSent(insertResult.messageId, true)
|
||||
}
|
||||
emitter.onComplete()
|
||||
} else {
|
||||
emitter.tryOnError(Exception("Pin message failed"))
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun unpinMessage(messageId: Long): Completable {
|
||||
return Completable.create { emitter ->
|
||||
val message = SignalDatabase.messages.getMessageRecordOrNull(messageId)
|
||||
if (message == null) {
|
||||
emitter.tryOnError(Exception("Unpin message failed - missing message"))
|
||||
}
|
||||
|
||||
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(message!!.threadId)
|
||||
if (threadRecipient == null) {
|
||||
emitter.tryOnError(Exception("Unpin message failed - missing thread recipient"))
|
||||
}
|
||||
|
||||
val isGroup = threadRecipient!!.isPushV2Group
|
||||
if (isGroup && threadRecipient.groupId.getOrNull()?.isV2 != true) {
|
||||
emitter.tryOnError(Exception("Unpin message failed - missing group id"))
|
||||
}
|
||||
|
||||
Log.i(TAG, "Sending unpin message to ${threadRecipient.id}")
|
||||
|
||||
val possibleTargets: List<Recipient> = if (isGroup) {
|
||||
SignalDatabase.groups.getGroupMembers(threadRecipient.requireGroupId().requireV2(), GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF).map { it.resolve() }.distinctBy { it.id }
|
||||
} else {
|
||||
listOf(threadRecipient)
|
||||
}
|
||||
|
||||
val eligibleTargets: List<Recipient> = RecipientUtil.getEligibleForSending(possibleTargets)
|
||||
val results = PinSendUtil.sendUnpinMessage(applicationContext, threadRecipient, message.fromRecipient.requireServiceId(), message.dateSent, eligibleTargets)
|
||||
val sendResults = GroupSendJobHelper.getCompletedSends(eligibleTargets, results)
|
||||
|
||||
if (sendResults.completed.isNotEmpty() || possibleTargets.isEmpty()) {
|
||||
SignalDatabase.messages.unpinMessage(messageId = messageId, threadId = message.threadId)
|
||||
databaseObserver.notifyConversationListeners(message.threadId)
|
||||
|
||||
if (sendResults.skipped.isNotEmpty()) {
|
||||
val filterRecipientIds = (sendResults.skipped - sendResults.completed.map { it.id }).toSet()
|
||||
Log.i(TAG, "Some recipients skipped when sending unpin message. Resending to $filterRecipientIds")
|
||||
val unpinJob = UnpinMessageJob.create(messageId = messageId, initialRecipientIds = filterRecipientIds)
|
||||
if (unpinJob != null) {
|
||||
AppDependencies.jobManager.add(unpinJob)
|
||||
}
|
||||
}
|
||||
emitter.onComplete()
|
||||
} else {
|
||||
emitter.tryOnError(Exception("Unpin message failed"))
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.PollVoteJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
|
||||
import org.thoughtcrime.securesms.jobs.UnpinMessageJob
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardUtil
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
@@ -356,19 +355,22 @@ class ConversationViewModel(
|
||||
}
|
||||
|
||||
fun pinMessage(messageRecord: MessageRecord, duration: Duration, threadRecipient: Recipient): Completable {
|
||||
return repository
|
||||
.pinMessage(messageRecord, duration, threadRecipient)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
return if (!NetworkUtil.isConnected(AppDependencies.application)) {
|
||||
Completable.error(Exception("Connection required to pin message"))
|
||||
} else {
|
||||
repository
|
||||
.pinMessage(messageRecord, duration, threadRecipient)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
||||
fun unpinMessage(messageId: Long) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val unpinJob = UnpinMessageJob.create(messageId = messageId)
|
||||
if (unpinJob != null) {
|
||||
AppDependencies.jobManager.add(unpinJob)
|
||||
} else {
|
||||
Log.w(TAG, "Unable to create unpin job, ignoring.")
|
||||
}
|
||||
fun unpinMessage(messageId: Long): Completable {
|
||||
return if (!NetworkUtil.isConnected(AppDependencies.application)) {
|
||||
Completable.error(Exception("Connection required to unpin message"))
|
||||
} else {
|
||||
repository
|
||||
.unpinMessage(messageId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.models.ServiceId
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl
|
||||
import org.thoughtcrime.securesms.groups.GroupNotAMemberException
|
||||
import org.thoughtcrime.securesms.messages.GroupSendUtil
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException
|
||||
import org.thoughtcrime.securesms.util.GroupUtil
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Companion.newBuilder
|
||||
import java.io.IOException
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Functions used when pinning/unpinning messages
|
||||
*/
|
||||
object PinSendUtil {
|
||||
|
||||
private val PIN_TERMINATE_TIMEOUT = 7000.milliseconds
|
||||
|
||||
@Throws(IOException::class, GroupNotAMemberException::class, UndeliverableMessageException::class)
|
||||
fun sendPinMessage(applicationContext: Context, threadRecipient: Recipient, message: OutgoingMessage, destinations: List<Recipient>): List<SendMessageResult?> {
|
||||
val builder = newBuilder()
|
||||
val groupId = if (threadRecipient.isPushV2Group) threadRecipient.requireGroupId().requireV2() else null
|
||||
|
||||
if (groupId != null) {
|
||||
val groupRecord: GroupRecord? = SignalDatabase.groups.getGroup(groupId).getOrNull()
|
||||
if (groupRecord != null && groupRecord.attributesAccessControl == GroupAccessControl.ONLY_ADMINS && !groupRecord.isAdmin(Recipient.self())) {
|
||||
throw UndeliverableMessageException("Non-admins cannot pin messages!")
|
||||
}
|
||||
GroupUtil.setDataMessageGroupContext(AppDependencies.application, builder, groupId)
|
||||
}
|
||||
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val message = builder
|
||||
.withTimestamp(sentTime)
|
||||
.withExpiration((message.expiresIn / 1000).toInt())
|
||||
.withProfileKey(ProfileKeyUtil.getSelfProfileKey().serialize())
|
||||
.withPinnedMessage(
|
||||
SignalServiceDataMessage.PinnedMessage(
|
||||
targetAuthor = ServiceId.parseOrThrow(message.messageExtras!!.pinnedMessage!!.targetAuthorAci),
|
||||
targetSentTimestamp = message.messageExtras.pinnedMessage.targetTimestamp,
|
||||
pinDurationInSeconds = message.messageExtras.pinnedMessage.pinDurationInSeconds.takeIf { it != MessageTable.PIN_FOREVER }?.toInt(),
|
||||
forever = (message.messageExtras.pinnedMessage.pinDurationInSeconds == MessageTable.PIN_FOREVER).takeIf { it }
|
||||
)
|
||||
)
|
||||
.build()
|
||||
|
||||
return GroupSendUtil.sendUnresendableDataMessage(
|
||||
applicationContext,
|
||||
groupId,
|
||||
destinations,
|
||||
false,
|
||||
ContentHint.DEFAULT,
|
||||
message,
|
||||
false
|
||||
) { System.currentTimeMillis() - sentTime > PIN_TERMINATE_TIMEOUT.inWholeMilliseconds }
|
||||
}
|
||||
|
||||
@Throws(IOException::class, GroupNotAMemberException::class, UndeliverableMessageException::class)
|
||||
fun sendUnpinMessage(applicationContext: Context, threadRecipient: Recipient, targetAuthor: ServiceId, targetSentTimestamp: Long, destinations: List<Recipient>): List<SendMessageResult?> {
|
||||
val builder = newBuilder()
|
||||
val groupId = if (threadRecipient.isPushV2Group) threadRecipient.requireGroupId().requireV2() else null
|
||||
if (groupId != null) {
|
||||
val groupRecord: GroupRecord? = SignalDatabase.groups.getGroup(groupId).getOrNull()
|
||||
if (groupRecord != null && groupRecord.attributesAccessControl == GroupAccessControl.ONLY_ADMINS && !groupRecord.isAdmin(Recipient.self())) {
|
||||
throw UndeliverableMessageException("Non-admins cannot pin messages!")
|
||||
}
|
||||
|
||||
GroupUtil.setDataMessageGroupContext(AppDependencies.application, builder, groupId)
|
||||
}
|
||||
|
||||
val sentTime = System.currentTimeMillis()
|
||||
val message = builder
|
||||
.withTimestamp(sentTime)
|
||||
.withProfileKey(ProfileKeyUtil.getSelfProfileKey().serialize())
|
||||
.withUnpinnedMessage(
|
||||
SignalServiceDataMessage.UnpinnedMessage(
|
||||
targetAuthor = targetAuthor,
|
||||
targetSentTimestamp = targetSentTimestamp
|
||||
)
|
||||
)
|
||||
.build()
|
||||
|
||||
return GroupSendUtil.sendUnresendableDataMessage(
|
||||
applicationContext,
|
||||
groupId,
|
||||
destinations,
|
||||
false,
|
||||
ContentHint.DEFAULT,
|
||||
message,
|
||||
false
|
||||
) { System.currentTimeMillis() - sentTime > PIN_TERMINATE_TIMEOUT.inWholeMilliseconds }
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,10 @@ class UnpinMessageJob(
|
||||
const val KEY: String = "UnpinMessageJob"
|
||||
private val TAG = Log.tag(UnpinMessageJob::class.java)
|
||||
|
||||
fun create(messageId: Long): UnpinMessageJob? {
|
||||
/**
|
||||
* If [initialRecipientIds] is set, the message will only be sent to those recipients. Otherwise, it is sent to everyone who is eligible.
|
||||
*/
|
||||
fun create(messageId: Long, initialRecipientIds: Set<RecipientId> = emptySet()): UnpinMessageJob? {
|
||||
val message = SignalDatabase.messages.getMessageRecordOrNull(messageId)
|
||||
if (message == null) {
|
||||
Log.w(TAG, "Unable to find corresponding message")
|
||||
@@ -47,7 +50,9 @@ class UnpinMessageJob(
|
||||
return null
|
||||
}
|
||||
|
||||
val recipients = if (conversationRecipient.isGroup) {
|
||||
val recipients = if (initialRecipientIds.isNotEmpty()) {
|
||||
initialRecipientIds.map { it.toLong() }
|
||||
} else if (conversationRecipient.isGroup) {
|
||||
conversationRecipient.participantIds.filter { it != Recipient.self().id }.map { it.toLong() }
|
||||
} else {
|
||||
listOf(conversationRecipient.id.toLong())
|
||||
|
||||
@@ -54,7 +54,9 @@ object SignalServiceProtoUtil {
|
||||
hasRemoteDelete ||
|
||||
pollCreate != null ||
|
||||
pollVote != null ||
|
||||
pollTerminate != null
|
||||
pollTerminate != null ||
|
||||
pinMessage != null ||
|
||||
unpinMessage != null
|
||||
}
|
||||
|
||||
val DataMessage.hasDisallowedAnnouncementOnlyContent: Boolean
|
||||
|
||||
@@ -9070,6 +9070,14 @@
|
||||
<string name="PinnedMessage__unpin_title">Unpin all messages?</string>
|
||||
<!-- Body of dialog to unpin all messages -->
|
||||
<string name="PinnedMessage__unpin_body">Messages will be unpinned for all members.</string>
|
||||
<!-- Dialog title when failing to pin a message -->
|
||||
<string name="PinnedMessage__couldnt_pin">Couldn\'t pin message</string>
|
||||
<!-- Dialog title when failing to unpin a message -->
|
||||
<string name="PinnedMessage__couldnt_unpin">Couldn\'t unpin message</string>
|
||||
<!-- Dialog title when failing to unpin all messages -->
|
||||
<string name="PinnedMessage__couldnt_unpin_all">Couldn\'t unpin messages</string>
|
||||
<!-- Dialog body when failing to pin a message -->
|
||||
<string name="PinnedMessage__check_connection">Check your connection and try again.</string>
|
||||
|
||||
<!-- EOF -->
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user