Add sending and error states for story group replies.

fork-5.53.8
Cody Henthorne 2022-05-24 15:17:29 -04:00 zatwierdzone przez Alex Hart
rodzic a29bc1da8c
commit 2a91c67c51
23 zmienionych plików z 635 dodań i 707 usunięć

Wyświetl plik

@ -1049,7 +1049,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
Runnable deleteForEveryone = () -> {
SignalExecutors.BOUNDED.execute(() -> {
for (MessageRecord message : messageRecords) {
MessageSender.sendRemoteDelete(ApplicationDependencies.getApplication(), message.getId(), message.isMms());
MessageSender.sendRemoteDelete(message.getId(), message.isMms());
}
});
};

Wyświetl plik

@ -10,6 +10,7 @@ import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.protobuf.ByteString;
import org.signal.core.util.SetUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.util.Pair;
import org.thoughtcrime.securesms.attachments.Attachment;
@ -45,7 +46,6 @@ import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.RecipientAccessList;
import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.crypto.ContentHint;
@ -65,7 +65,6 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@ -337,8 +336,6 @@ public final class PushGroupSendJob extends PushSendJob {
groupMessageBuilder.withBody(null);
}
} catch (NoSuchMessageException e) {
// The story has probably expired
// TODO [stories] check what should happen in this case
throw new UndeliverableMessageException(e);
}
} else {

Wyświetl plik

@ -239,8 +239,6 @@ public class PushMediaSendJob extends PushSendJob {
mediaMessageBuilder.withBody(null);
}
} catch (NoSuchMessageException e) {
// The story has probably expired
// TODO [stories] check what should happen in this case
throw new UndeliverableMessageException(e);
}
} else {

Wyświetl plik

@ -469,7 +469,7 @@ public class MessageSender {
}
}
public static void sendRemoteDelete(@NonNull Context context, long messageId, boolean isMms) {
public static void sendRemoteDelete(long messageId, boolean isMms) {
MessageDatabase db = isMms ? SignalDatabase.mms() : SignalDatabase.sms();
db.markAsRemoteDelete(messageId);
db.markAsSending(messageId);

Wyświetl plik

@ -45,7 +45,8 @@ object StoryContextMenu {
val uri: Uri? = mediaMessageRecord?.slideDeck?.firstSlide?.uri
val contentType: String? = mediaMessageRecord?.slideDeck?.firstSlide?.contentType
if (uri == null || contentType == null) {
// TODO [stories] Toast that we can't save this media
Log.w(TAG, "Unable to save story media uri: $uri contentType: $contentType")
Toast.makeText(context, R.string.MyStories__unable_to_save, Toast.LENGTH_SHORT).show()
return
}
@ -176,7 +177,6 @@ object StoryContextMenu {
}
)
} else {
// TODO [stories] -- Final icon
add(
ActionItem(R.drawable.ic_check_circle_24, context.getString(R.string.StoriesLandingItem__unhide_story)) {
callbacks.onUnhide()

Wyświetl plik

@ -56,16 +56,6 @@ class PrivateStorySettingsFragment : DSLSettingsFragment(
}
return configure {
customPref(
PrivateStoryItem.Model(
privateStoryItemData = state.privateStory,
onClick = {
// TODO [stories] -- is this even clickable?
}
)
)
dividerPref()
sectionHeaderPref(R.string.MyStorySettingsFragment__who_can_see_this_story)
customPref(
PrivateStoryItem.AddViewerModel(

Wyświetl plik

@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.stories.viewer.reply.group
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.recipients.Recipient
sealed class ReplyBody(val messageRecord: MessageRecord) {
val key: MessageId = MessageId(messageRecord.id, true)
val sender: Recipient = if (messageRecord.isOutgoing) Recipient.self() else messageRecord.individualRecipient.resolve()
val sentAtMillis: Long = messageRecord.dateSent
open fun hasSameContent(other: ReplyBody): Boolean {
return key == other.key &&
sender.hasSameContent(other.sender) &&
sentAtMillis == other.sentAtMillis
}
class Text(val message: ConversationMessage) : ReplyBody(message.messageRecord) {
override fun hasSameContent(other: ReplyBody): Boolean {
return super.hasSameContent(other) &&
(other as? Text)?.let { messageRecord.body == other.messageRecord.body } ?: false
}
}
class Reaction(messageRecord: MessageRecord) : ReplyBody(messageRecord) {
val emoji: CharSequence = messageRecord.body
override fun hasSameContent(other: ReplyBody): Boolean {
return super.hasSameContent(other) &&
(other as? Reaction)?.let { emoji == other.emoji } ?: false
}
}
class RemoteDelete(messageRecord: MessageRecord) : ReplyBody(messageRecord)
}

Wyświetl plik

@ -5,17 +5,17 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsColumns
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSource<StoryGroupReplyItemData.Key, StoryGroupReplyItemData> {
class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSource<MessageId, ReplyBody> {
override fun size(): Int {
return SignalDatabase.mms.getNumberOfStoryReplies(parentStoryId)
}
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<StoryGroupReplyItemData> {
val results: MutableList<StoryGroupReplyItemData> = ArrayList(length)
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<ReplyBody> {
val results: MutableList<ReplyBody> = ArrayList(length)
SignalDatabase.mms.getStoryReplies(parentStoryId).use { cursor ->
cursor.moveToPosition(start - 1)
val reader = MmsDatabase.Reader(cursor)
@ -27,48 +27,21 @@ class StoryGroupReplyDataSource(private val parentStoryId: Long) : PagedDataSour
return results
}
override fun load(key: StoryGroupReplyItemData.Key?): StoryGroupReplyItemData? {
throw UnsupportedOperationException()
override fun load(key: MessageId): ReplyBody {
return readRowFromRecord(SignalDatabase.mms.getMessageRecord(key.id) as MmsMessageRecord)
}
override fun getKey(data: StoryGroupReplyItemData): StoryGroupReplyItemData.Key {
override fun getKey(data: ReplyBody): MessageId {
return data.key
}
private fun readRowFromRecord(record: MmsMessageRecord): StoryGroupReplyItemData {
private fun readRowFromRecord(record: MmsMessageRecord): ReplyBody {
return when {
record.isRemoteDelete -> readRemoteDeleteFromRecord(record)
MmsSmsColumns.Types.isStoryReaction(record.type) -> readReactionFromRecord(record)
else -> readTextFromRecord(record)
}
}
private fun readRemoteDeleteFromRecord(record: MmsMessageRecord): StoryGroupReplyItemData {
return StoryGroupReplyItemData(
key = StoryGroupReplyItemData.Key.RemoteDelete(record.id),
sender = if (record.isOutgoing) Recipient.self() else record.individualRecipient.resolve(),
sentAtMillis = record.dateSent,
replyBody = StoryGroupReplyItemData.ReplyBody.RemoteDelete(record)
)
}
private fun readReactionFromRecord(record: MmsMessageRecord): StoryGroupReplyItemData {
return StoryGroupReplyItemData(
key = StoryGroupReplyItemData.Key.Reaction(record.id),
sender = if (record.isOutgoing) Recipient.self() else record.individualRecipient.resolve(),
sentAtMillis = record.dateSent,
replyBody = StoryGroupReplyItemData.ReplyBody.Reaction(record.body)
)
}
private fun readTextFromRecord(record: MmsMessageRecord): StoryGroupReplyItemData {
return StoryGroupReplyItemData(
key = StoryGroupReplyItemData.Key.Text(record.id),
sender = if (record.isOutgoing) Recipient.self() else record.individualRecipient.resolve(),
sentAtMillis = record.dateSent,
replyBody = StoryGroupReplyItemData.ReplyBody.Text(
record.isRemoteDelete -> ReplyBody.RemoteDelete(record)
MmsSmsColumns.Types.isStoryReaction(record.type) -> ReplyBody.Reaction(record)
else -> ReplyBody.Text(
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(), record)
)
)
}
}
}

Wyświetl plik

@ -6,12 +6,14 @@ import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehaviorHack
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.SignalExecutors
@ -28,6 +30,8 @@ import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragment
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
import org.thoughtcrime.securesms.keyboard.KeyboardPage
@ -39,6 +43,7 @@ import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDial
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerChild
import org.thoughtcrime.securesms.stories.viewer.reply.StoryViewsAndRepliesPagerParent
import org.thoughtcrime.securesms.stories.viewer.reply.composer.StoryReactionBar
@ -64,6 +69,26 @@ class StoryGroupReplyFragment :
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
SafetyNumberChangeDialog.Callback {
companion object {
private val TAG = Log.tag(StoryGroupReplyFragment::class.java)
private const val ARG_STORY_ID = "arg.story.id"
private const val ARG_GROUP_RECIPIENT_ID = "arg.group.recipient.id"
private const val ARG_IS_FROM_NOTIFICATION = "is_from_notification"
private const val ARG_GROUP_REPLY_START_POSITION = "group_reply_start_position"
fun create(storyId: Long, groupRecipientId: RecipientId, isFromNotification: Boolean, groupReplyStartPosition: Int): Fragment {
return StoryGroupReplyFragment().apply {
arguments = Bundle().apply {
putLong(ARG_STORY_ID, storyId)
putParcelable(ARG_GROUP_RECIPIENT_ID, groupRecipientId)
putBoolean(ARG_IS_FROM_NOTIFICATION, isFromNotification)
putInt(ARG_GROUP_REPLY_START_POSITION, groupReplyStartPosition)
}
}
}
}
private val viewModel: StoryGroupReplyViewModel by viewModels(
factoryProducer = {
StoryGroupReplyViewModel.Factory(storyId, StoryGroupReplyRepository())
@ -107,7 +132,7 @@ class StoryGroupReplyFragment :
get() = requireArguments().getInt(ARG_GROUP_REPLY_START_POSITION, -1)
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: PagingMappingAdapter<StoryGroupReplyItemData.Key>
private lateinit var adapter: PagingMappingAdapter<MessageId>
private lateinit var dataObserver: RecyclerView.AdapterDataObserver
private lateinit var composer: StoryReplyComposer
@ -131,7 +156,10 @@ class StoryGroupReplyFragment :
val emptyNotice: View = requireView().findViewById(R.id.empty_notice)
adapter = PagingMappingAdapter<StoryGroupReplyItemData.Key>()
adapter = PagingMappingAdapter<MessageId>().apply {
setPagingController(viewModel.pagingController)
}
val layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
@ -142,37 +170,34 @@ class StoryGroupReplyFragment :
onPageSelected(findListener<StoryViewsAndRepliesPagerParent>()?.selectedChild ?: StoryViewsAndRepliesPagerParent.Child.REPLIES)
viewModel.state.observe(viewLifecycleOwner) { state ->
if (markReadHelper == null && state.threadId > 0L) {
if (isResumed) {
ApplicationDependencies.getMessageNotifier().setVisibleThread(ConversationId(state.threadId, storyId))
var firstSubmit = true
viewModel.state
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy { state ->
if (markReadHelper == null && state.threadId > 0L) {
if (isResumed) {
ApplicationDependencies.getMessageNotifier().setVisibleThread(ConversationId(state.threadId, storyId))
}
markReadHelper = MarkReadHelper(ConversationId(state.threadId, storyId), requireContext(), viewLifecycleOwner)
if (isFromNotification) {
markReadHelper?.onViewsRevealed(System.currentTimeMillis())
}
}
markReadHelper = MarkReadHelper(ConversationId(state.threadId, storyId), requireContext(), viewLifecycleOwner)
emptyNotice.visible = state.noReplies && state.loadState == StoryGroupReplyState.LoadState.READY
colorizer.onNameColorsChanged(state.nameColors)
if (isFromNotification) {
markReadHelper?.onViewsRevealed(System.currentTimeMillis())
adapter.submitList(getConfiguration(state.replies).toMappingModelList()) {
if (firstSubmit && (groupReplyStartPosition >= 0 && adapter.hasItem(groupReplyStartPosition))) {
firstSubmit = false
recyclerView.post { recyclerView.scrollToPosition(groupReplyStartPosition) }
}
}
}
emptyNotice.visible = state.noReplies && state.loadState == StoryGroupReplyState.LoadState.READY
colorizer.onNameColorsChanged(state.nameColors)
}
viewModel.pagingController.observe(viewLifecycleOwner) { controller ->
adapter.setPagingController(controller)
}
var consumed = false
viewModel.pageData.observe(viewLifecycleOwner) { pageData ->
adapter.submitList(getConfiguration(pageData).toMappingModelList()) {
if (!consumed && (groupReplyStartPosition >= 0 && adapter.hasItem(groupReplyStartPosition))) {
consumed = true
recyclerView.post { recyclerView.scrollToPosition(groupReplyStartPosition) }
}
}
}
dataObserver = GroupDataObserver()
adapter.registerAdapterDataObserver(dataObserver)
@ -212,74 +237,51 @@ class StoryGroupReplyFragment :
val lastVisibleItem = (recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
val adapterItem = adapter.getItem(lastVisibleItem)
if (adapterItem == null || adapterItem !is StoryGroupReplyItem.DataWrapper) {
if (adapterItem == null || adapterItem !is StoryGroupReplyItem.Model) {
return
}
markReadHelper?.onViewsRevealed(adapterItem.storyGroupReplyItemData.sentAtMillis)
markReadHelper?.onViewsRevealed(adapterItem.replyBody.sentAtMillis)
}
private fun getConfiguration(pageData: List<StoryGroupReplyItemData>): DSLConfiguration {
private fun getConfiguration(pageData: List<ReplyBody>): DSLConfiguration {
return configure {
pageData.forEach {
when (it.replyBody) {
is StoryGroupReplyItemData.ReplyBody.Text -> {
when (it) {
is ReplyBody.Text -> {
customPref(
StoryGroupReplyItem.TextModel(
storyGroupReplyItemData = it,
text = it.replyBody,
nameColor = colorizer.getIncomingGroupSenderColor(
requireContext(),
it.sender
),
onCopyClick = { model ->
val clipData = ClipData.newPlainText(requireContext().getString(R.string.app_name), model.text.message.getDisplayBody(requireContext()))
ServiceUtil.getClipboardManager(requireContext()).setPrimaryClip(clipData)
Toast.makeText(requireContext(), R.string.StoryGroupReplyFragment__copied_to_clipboard, Toast.LENGTH_SHORT).show()
},
onDeleteClick = { model ->
lifecycleDisposable += DeleteDialog.show(requireActivity(), setOf(model.text.message.messageRecord)).subscribe { result ->
if (result) {
throw AssertionError("We should never end up deleting a Group Thread like this.")
}
}
},
text = it,
nameColor = it.sender.getStoryGroupReplyColor(),
onCopyClick = { s -> onCopyClick(s) },
onMentionClick = { recipientId ->
RecipientBottomSheetDialogFragment
.create(recipientId, null)
.show(childFragmentManager, null)
}
},
onDeleteClick = { m -> onDeleteClick(m) },
onTapForDetailsClick = { m -> onTapForDetailsClick(m) }
)
)
}
is StoryGroupReplyItemData.ReplyBody.Reaction -> {
is ReplyBody.Reaction -> {
customPref(
StoryGroupReplyItem.ReactionModel(
storyGroupReplyItemData = it,
reaction = it.replyBody,
nameColor = colorizer.getIncomingGroupSenderColor(
requireContext(),
it.sender
)
reaction = it,
nameColor = it.sender.getStoryGroupReplyColor(),
onCopyClick = { s -> onCopyClick(s) },
onDeleteClick = { m -> onDeleteClick(m) },
onTapForDetailsClick = { m -> onTapForDetailsClick(m) }
)
)
}
is StoryGroupReplyItemData.ReplyBody.RemoteDelete -> {
is ReplyBody.RemoteDelete -> {
customPref(
StoryGroupReplyItem.RemoteDeleteModel(
storyGroupReplyItemData = it,
remoteDelete = it.replyBody,
nameColor = colorizer.getIncomingGroupSenderColor(
requireContext(),
it.sender
),
onDeleteClick = { model ->
lifecycleDisposable += DeleteDialog.show(requireActivity(), setOf(model.remoteDelete.messageRecord)).subscribe { didDeleteThread ->
if (didDeleteThread) {
throw AssertionError("We should never end up deleting a Group Thread like this.")
}
}
},
remoteDelete = it,
nameColor = it.sender.getStoryGroupReplyColor(),
onDeleteClick = { m -> onDeleteClick(m) },
onTapForDetailsClick = { m -> onTapForDetailsClick(m) }
)
)
}
@ -288,6 +290,37 @@ class StoryGroupReplyFragment :
}
}
private fun onCopyClick(textToCopy: CharSequence) {
val clipData = ClipData.newPlainText(requireContext().getString(R.string.app_name), textToCopy)
ServiceUtil.getClipboardManager(requireContext()).setPrimaryClip(clipData)
Toast.makeText(requireContext(), R.string.StoryGroupReplyFragment__copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
private fun onDeleteClick(messageRecord: MessageRecord) {
lifecycleDisposable += DeleteDialog.show(requireActivity(), setOf(messageRecord)).subscribe { didDeleteThread ->
if (didDeleteThread) {
throw AssertionError("We should never end up deleting a Group Thread like this.")
}
}
}
private fun onTapForDetailsClick(messageRecord: MessageRecord) {
if (messageRecord.isRemoteDelete) {
// TODO [cody] Android doesn't support resending remote deletes yet
return
}
if (messageRecord.isIdentityMismatchFailure) {
SafetyNumberChangeDialog.show(requireContext(), childFragmentManager, messageRecord)
} else if (messageRecord.hasFailedWithNetworkFailures()) {
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.conversation_activity__message_could_not_be_sent)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.conversation_activity__send) { _, _ -> SignalExecutors.BOUNDED.execute { MessageSender.resend(requireContext(), messageRecord) } }
.show()
}
}
override fun onPageSelected(child: StoryViewsAndRepliesPagerParent.Child) {
currentChild = child
updateNestedScrolling()
@ -376,8 +409,7 @@ class StoryGroupReplyFragment :
composer.closeEmojiSearch()
}
override fun onReactWithAnyEmojiDialogDismissed() {
}
override fun onReactWithAnyEmojiDialogDismissed() = Unit
override fun onReactWithAnyEmojiSelected(emoji: String) {
sendReaction(emoji)
@ -427,52 +459,6 @@ class StoryGroupReplyFragment :
}
}
private inner class GroupReplyScrollObserver : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
postMarkAsReadRequest()
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
postMarkAsReadRequest()
}
}
private inner class GroupDataObserver : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (itemCount == 0) {
return
}
val item = adapter.getItem(positionStart)
if (positionStart == adapter.itemCount - 1 && item is StoryGroupReplyItem.DataWrapper) {
val isOutgoing = item.storyGroupReplyItemData.sender == Recipient.self()
if (isOutgoing || (!isOutgoing && !recyclerView.canScrollVertically(1))) {
recyclerView.post { recyclerView.scrollToPosition(positionStart) }
}
}
}
}
companion object {
private val TAG = Log.tag(StoryGroupReplyFragment::class.java)
private const val ARG_STORY_ID = "arg.story.id"
private const val ARG_GROUP_RECIPIENT_ID = "arg.group.recipient.id"
private const val ARG_IS_FROM_NOTIFICATION = "is_from_notification"
private const val ARG_GROUP_REPLY_START_POSITION = "group_reply_start_position"
fun create(storyId: Long, groupRecipientId: RecipientId, isFromNotification: Boolean, groupReplyStartPosition: Int): Fragment {
return StoryGroupReplyFragment().apply {
arguments = Bundle().apply {
putLong(ARG_STORY_ID, storyId)
putParcelable(ARG_GROUP_RECIPIENT_ID, groupRecipientId)
putBoolean(ARG_IS_FROM_NOTIFICATION, isFromNotification)
putInt(ARG_GROUP_REPLY_START_POSITION, groupReplyStartPosition)
}
}
}
}
private fun performSend(body: CharSequence, mentions: List<Mention>) {
lifecycleDisposable += StoryGroupReplySender.sendReply(requireContext(), storyId, body, mentions)
.observeOn(AndroidSchedulers.mainThread())
@ -505,7 +491,7 @@ class StoryGroupReplyFragment :
}
override fun onMessageResentAfterSafetyNumberChange() {
error("Should never get here.")
Log.i(TAG, "Message resent")
}
override fun onCanceled() {
@ -514,6 +500,37 @@ class StoryGroupReplyFragment :
resendReaction = null
}
@ColorInt
private fun Recipient.getStoryGroupReplyColor(): Int {
return colorizer.getIncomingGroupSenderColor(requireContext(), this)
}
private inner class GroupReplyScrollObserver : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
postMarkAsReadRequest()
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
postMarkAsReadRequest()
}
}
private inner class GroupDataObserver : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (itemCount == 0) {
return
}
val item = adapter.getItem(positionStart)
if (positionStart == adapter.itemCount - 1 && item is StoryGroupReplyItem.Model) {
val isOutgoing = item.replyBody.sender == Recipient.self()
if (isOutgoing || (!isOutgoing && !recyclerView.canScrollVertically(1))) {
recyclerView.post { recyclerView.scrollToPosition(positionStart) }
}
}
}
}
interface Callback {
fun onStartDirectReply(recipientId: RecipientId)
fun requestFullScreen(fullscreen: Boolean)

Wyświetl plik

@ -9,17 +9,23 @@ import android.text.style.ClickableSpan
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.annotation.ColorInt
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.updateLayoutParams
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AlertView
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.DeliveryStatusView
import org.thoughtcrime.securesms.components.FromTextView
import org.thoughtcrime.securesms.components.emoji.EmojiImageView
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.components.menu.ActionItem
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.AvatarUtil
@ -29,138 +35,122 @@ import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
import org.thoughtcrime.securesms.util.changeConstraints
import org.thoughtcrime.securesms.util.padding
import org.thoughtcrime.securesms.util.visible
import java.util.Locale
typealias OnCopyClick = (CharSequence) -> Unit
typealias OnDeleteClick = (MessageRecord) -> Unit
typealias OnTapForDetailsClick = (MessageRecord) -> Unit
object StoryGroupReplyItem {
private const val NAME_COLOR_CHANGED = 1
fun register(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(TextModel::class.java, LayoutFactory(::TextViewHolder, R.layout.stories_group_text_reply_item))
mappingAdapter.registerFactory(ReactionModel::class.java, LayoutFactory(::ReactionViewHolder, R.layout.stories_group_reaction_reply_item))
mappingAdapter.registerFactory(RemoteDeleteModel::class.java, LayoutFactory(::RemoteDeleteViewHolder, R.layout.stories_group_remote_delete_item))
mappingAdapter.registerFactory(ReactionModel::class.java, LayoutFactory(::ReactionViewHolder, R.layout.stories_group_text_reply_item))
mappingAdapter.registerFactory(RemoteDeleteModel::class.java, LayoutFactory(::RemoteDeleteViewHolder, R.layout.stories_group_text_reply_item))
}
sealed class Model<T : Any>(
val replyBody: ReplyBody,
@ColorInt val nameColor: Int,
val onCopyClick: OnCopyClick?,
val onDeleteClick: OnDeleteClick,
val onTapForDetailsClick: OnTapForDetailsClick
) : MappingModel<T> {
val messageRecord: MessageRecord = replyBody.messageRecord
val isPending: Boolean = messageRecord.isPending
val isFailure: Boolean = messageRecord.isFailed
val sentAtMillis: Long = replyBody.sentAtMillis
override fun areItemsTheSame(newItem: T): Boolean {
val other = newItem as Model<*>
return replyBody.sender == other.replyBody.sender &&
replyBody.sentAtMillis == other.replyBody.sentAtMillis
}
override fun areContentsTheSame(newItem: T): Boolean {
val other = newItem as Model<*>
return areNonPayloadPropertiesTheSame(other) &&
nameColor == other.nameColor
}
override fun getChangePayload(newItem: T): Any? {
val other = newItem as Model<*>
return if (nameColor != other.nameColor && areNonPayloadPropertiesTheSame(other)) {
NAME_COLOR_CHANGED
} else {
null
}
}
private fun areNonPayloadPropertiesTheSame(newItem: Model<*>): Boolean {
return replyBody.hasSameContent(newItem.replyBody) &&
isPending == newItem.isPending &&
isFailure == newItem.isFailure &&
sentAtMillis == newItem.sentAtMillis
}
}
class TextModel(
override val storyGroupReplyItemData: StoryGroupReplyItemData,
val text: StoryGroupReplyItemData.ReplyBody.Text,
@ColorInt val nameColor: Int,
val onCopyClick: (TextModel) -> Unit,
val onDeleteClick: (TextModel) -> Unit,
val onMentionClick: (RecipientId) -> Unit
) : PreferenceModel<TextModel>(), DataWrapper {
override fun areItemsTheSame(newItem: TextModel): Boolean {
return storyGroupReplyItemData.sender == newItem.storyGroupReplyItemData.sender &&
storyGroupReplyItemData.sentAtMillis == newItem.storyGroupReplyItemData.sentAtMillis
}
override fun areContentsTheSame(newItem: TextModel): Boolean {
return storyGroupReplyItemData == newItem.storyGroupReplyItemData &&
storyGroupReplyItemData.sender.hasSameContent(newItem.storyGroupReplyItemData.sender) &&
nameColor == newItem.nameColor &&
super.areContentsTheSame(newItem)
}
override fun getChangePayload(newItem: TextModel): Any? {
return if (nameColor != newItem.nameColor &&
storyGroupReplyItemData == newItem.storyGroupReplyItemData &&
storyGroupReplyItemData.sender.hasSameContent(newItem.storyGroupReplyItemData.sender) &&
super.areContentsTheSame(newItem)
) {
NAME_COLOR_CHANGED
} else {
null
}
}
}
class RemoteDeleteModel(
override val storyGroupReplyItemData: StoryGroupReplyItemData,
val remoteDelete: StoryGroupReplyItemData.ReplyBody.RemoteDelete,
val onDeleteClick: (RemoteDeleteModel) -> Unit,
@ColorInt val nameColor: Int
) : MappingModel<RemoteDeleteModel>, DataWrapper {
override fun areItemsTheSame(newItem: RemoteDeleteModel): Boolean {
return storyGroupReplyItemData.sender == newItem.storyGroupReplyItemData.sender &&
storyGroupReplyItemData.sentAtMillis == newItem.storyGroupReplyItemData.sentAtMillis
}
override fun areContentsTheSame(newItem: RemoteDeleteModel): Boolean {
return storyGroupReplyItemData == newItem.storyGroupReplyItemData &&
storyGroupReplyItemData.sender.hasSameContent(newItem.storyGroupReplyItemData.sender) &&
nameColor == newItem.nameColor
}
override fun getChangePayload(newItem: RemoteDeleteModel): Any? {
return if (nameColor != newItem.nameColor &&
storyGroupReplyItemData == newItem.storyGroupReplyItemData &&
storyGroupReplyItemData.sender.hasSameContent(newItem.storyGroupReplyItemData.sender)
) {
NAME_COLOR_CHANGED
} else {
null
}
}
}
val text: ReplyBody.Text,
val onMentionClick: (RecipientId) -> Unit,
@ColorInt nameColor: Int,
onCopyClick: OnCopyClick,
onDeleteClick: OnDeleteClick,
onTapForDetailsClick: OnTapForDetailsClick
) : Model<TextModel>(
replyBody = text,
nameColor = nameColor,
onCopyClick = onCopyClick,
onDeleteClick = onDeleteClick,
onTapForDetailsClick = onTapForDetailsClick
)
class ReactionModel(
override val storyGroupReplyItemData: StoryGroupReplyItemData,
val reaction: StoryGroupReplyItemData.ReplyBody.Reaction,
@ColorInt val nameColor: Int
) : PreferenceModel<ReactionModel>(), DataWrapper {
override fun areItemsTheSame(newItem: ReactionModel): Boolean {
return storyGroupReplyItemData.sender == newItem.storyGroupReplyItemData.sender &&
storyGroupReplyItemData.sentAtMillis == newItem.storyGroupReplyItemData.sentAtMillis
}
val reaction: ReplyBody.Reaction,
@ColorInt nameColor: Int,
onCopyClick: OnCopyClick,
onDeleteClick: OnDeleteClick,
onTapForDetailsClick: OnTapForDetailsClick
) : Model<ReactionModel>(
replyBody = reaction,
nameColor = nameColor,
onCopyClick = onCopyClick,
onDeleteClick = onDeleteClick,
onTapForDetailsClick = onTapForDetailsClick
)
override fun areContentsTheSame(newItem: ReactionModel): Boolean {
return storyGroupReplyItemData == newItem.storyGroupReplyItemData &&
storyGroupReplyItemData.sender.hasSameContent(newItem.storyGroupReplyItemData.sender) &&
nameColor == newItem.nameColor &&
super.areContentsTheSame(newItem)
}
class RemoteDeleteModel(
val remoteDelete: ReplyBody.RemoteDelete,
@ColorInt nameColor: Int,
onDeleteClick: OnDeleteClick,
onTapForDetailsClick: OnTapForDetailsClick
) : Model<RemoteDeleteModel>(
replyBody = remoteDelete,
nameColor = nameColor,
onCopyClick = null,
onDeleteClick = onDeleteClick,
onTapForDetailsClick = onTapForDetailsClick
)
override fun getChangePayload(newItem: ReactionModel): Any? {
return if (nameColor != newItem.nameColor &&
storyGroupReplyItemData == newItem.storyGroupReplyItemData &&
storyGroupReplyItemData.sender.hasSameContent(newItem.storyGroupReplyItemData.sender) &&
super.areContentsTheSame(newItem)
) {
NAME_COLOR_CHANGED
} else {
null
}
}
}
private abstract class BaseViewHolder<T : Model<T>>(itemView: View) : MappingViewHolder<T>(itemView) {
protected val bubble: View = findViewById(R.id.bubble)
protected val avatar: AvatarImageView = findViewById(R.id.avatar)
protected val name: FromTextView = findViewById(R.id.name)
protected val body: EmojiTextView = findViewById(R.id.body)
protected val date: TextView = findViewById(R.id.viewed_at)
protected val dateBelow: TextView = findViewById(R.id.viewed_at_below)
protected val status: DeliveryStatusView = findViewById(R.id.delivery_status)
protected val alertView: AlertView = findViewById(R.id.alert_view)
protected val reaction: EmojiImageView = itemView.findViewById(R.id.reaction)
interface DataWrapper {
val storyGroupReplyItemData: StoryGroupReplyItemData
}
private abstract class BaseViewHolder<T>(itemView: View) : MappingViewHolder<T>(itemView) {
protected val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
protected val name: FromTextView = itemView.findViewById(R.id.name)
protected val body: EmojiTextView = itemView.findViewById(R.id.body)
protected val date: TextView = itemView.findViewById(R.id.viewed_at)
protected val dateBelow: TextView = itemView.findViewById(R.id.viewed_at_below)
init {
body.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
if (body.lastLineWidth + date.measuredWidth > ViewUtil.dpToPx(242)) {
date.visible = false
dateBelow.visible = true
} else {
dateBelow.visible = false
date.visible = true
}
}
}
}
private class TextViewHolder(itemView: View) : BaseViewHolder<TextModel>(itemView) {
override fun bind(model: TextModel) {
@CallSuper
override fun bind(model: T) {
itemView.setOnLongClickListener {
displayContextMenu(model)
true
@ -171,29 +161,74 @@ object StoryGroupReplyItem {
return
}
AvatarUtil.loadIconIntoImageView(model.storyGroupReplyItemData.sender, avatar, DimensionUnit.DP.toPixels(28f).toInt())
name.text = resolveName(context, model.storyGroupReplyItemData.sender)
AvatarUtil.loadIconIntoImageView(model.replyBody.sender, avatar, DimensionUnit.DP.toPixels(28f).toInt())
name.text = resolveName(context, model.replyBody.sender)
if (model.isPending) {
status.setPending()
} else {
status.setNone()
}
if (model.isFailure) {
alertView.setFailed()
itemView.setOnClickListener {
model.onTapForDetailsClick(model.messageRecord)
}
date.setText(R.string.ConversationItem_error_not_sent_tap_for_details)
dateBelow.setText(R.string.ConversationItem_error_not_sent_tap_for_details)
} else {
alertView.setNone()
itemView.setOnClickListener(null)
val dateText = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.sentAtMillis)
date.text = dateText
dateBelow.text = dateText
}
itemView.post {
if (alertView.visible || body.lastLineWidth + date.measuredWidth > ViewUtil.dpToPx(242)) {
date.visible = false
dateBelow.visible = true
} else {
dateBelow.visible = false
date.visible = true
}
}
}
private fun displayContextMenu(model: Model<*>) {
itemView.isSelected = true
val actions = mutableListOf<ActionItem>()
if (model.onCopyClick != null) {
actions += ActionItem(R.drawable.ic_copy_24_solid_tinted, context.getString(R.string.StoryGroupReplyItem__copy)) {
val toCopy: CharSequence = when (model) {
is TextModel -> model.text.message.getDisplayBody(context)
else -> model.messageRecord.getDisplayBody(context)
}
model.onCopyClick.invoke(toCopy)
}
}
actions += ActionItem(R.drawable.ic_trash_24_solid_tinted, context.getString(R.string.StoryGroupReplyItem__delete)) { model.onDeleteClick(model.messageRecord) }
SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup)
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
.onDismiss { itemView.isSelected = false }
.show(actions)
}
}
private class TextViewHolder(itemView: View) : BaseViewHolder<TextModel>(itemView) {
override fun bind(model: TextModel) {
super.bind(model)
body.movementMethod = LinkMovementMethod.getInstance()
body.text = model.text.message.getDisplayBody(context).apply {
linkifyBody(model, this)
}
date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.storyGroupReplyItemData.sentAtMillis)
dateBelow.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.storyGroupReplyItemData.sentAtMillis)
}
private fun displayContextMenu(model: TextModel) {
itemView.isSelected = true
SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup)
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
.onDismiss { itemView.isSelected = false }
.show(
listOf(
ActionItem(R.drawable.ic_copy_24_solid_tinted, context.getString(R.string.StoryGroupReplyItem__copy)) { model.onCopyClick(model) },
ActionItem(R.drawable.ic_trash_24_solid_tinted, context.getString(R.string.StoryGroupReplyItem__delete)) { model.onDeleteClick(model) }
)
)
}
private fun linkifyBody(model: TextModel, body: Spannable) {
@ -211,59 +246,36 @@ object StoryGroupReplyItem {
model.onMentionClick(mentionedRecipientId)
}
override fun updateDrawState(ds: TextPaint) {}
override fun updateDrawState(ds: TextPaint) = Unit
}
}
private class ReactionViewHolder(itemView: View) : BaseViewHolder<ReactionModel>(itemView) {
init {
reaction.visible = true
bubble.visibility = View.INVISIBLE
itemView.padding(bottom = 0)
body.setText(R.string.StoryGroupReactionReplyItem__reacted_to_the_story)
body.updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginEnd = 0
}
(itemView as ConstraintLayout).changeConstraints {
connect(avatar.id, ConstraintSet.BOTTOM, body.id, ConstraintSet.BOTTOM)
}
}
override fun bind(model: ReactionModel) {
super.bind(model)
reaction.setImageEmoji(model.reaction.emoji)
}
}
private class RemoteDeleteViewHolder(itemView: View) : BaseViewHolder<RemoteDeleteModel>(itemView) {
override fun bind(model: RemoteDeleteModel) {
itemView.setOnLongClickListener {
displayContextMenu(model)
true
}
name.setTextColor(model.nameColor)
if (payload.contains(NAME_COLOR_CHANGED)) {
return
}
AvatarUtil.loadIconIntoImageView(model.storyGroupReplyItemData.sender, avatar, DimensionUnit.DP.toPixels(28f).toInt())
name.text = resolveName(context, model.storyGroupReplyItemData.sender)
date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.storyGroupReplyItemData.sentAtMillis)
dateBelow.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.storyGroupReplyItemData.sentAtMillis)
}
private fun displayContextMenu(model: RemoteDeleteModel) {
itemView.isSelected = true
SignalContextMenu.Builder(itemView, itemView.rootView as ViewGroup)
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
.onDismiss { itemView.isSelected = false }
.show(
listOf(
ActionItem(R.drawable.ic_trash_24_solid_tinted, context.getString(R.string.StoryGroupReplyItem__delete)) { model.onDeleteClick(model) }
)
)
}
}
private class ReactionViewHolder(itemView: View) : MappingViewHolder<ReactionModel>(itemView) {
private val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
private val name: FromTextView = itemView.findViewById(R.id.name)
private val reaction: EmojiImageView = itemView.findViewById(R.id.reaction)
private val date: TextView = itemView.findViewById(R.id.viewed_at)
override fun bind(model: ReactionModel) {
name.setTextColor(model.nameColor)
if (payload.contains(NAME_COLOR_CHANGED)) {
return
}
AvatarUtil.loadIconIntoImageView(model.storyGroupReplyItemData.sender, avatar, DimensionUnit.DP.toPixels(28f).toInt())
name.text = resolveName(context, model.storyGroupReplyItemData.sender)
reaction.setImageEmoji(model.reaction.emoji)
date.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), model.storyGroupReplyItemData.sentAtMillis)
init {
bubble.setBackgroundResource(R.drawable.rounded_outline_secondary_18)
body.setText(R.string.ThreadRecord_this_message_was_deleted)
}
}

Wyświetl plik

@ -1,24 +0,0 @@
package org.thoughtcrime.securesms.stories.viewer.reply.group
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.recipients.Recipient
data class StoryGroupReplyItemData(
val key: Key,
val sender: Recipient,
val sentAtMillis: Long,
val replyBody: ReplyBody
) {
sealed class ReplyBody {
data class Text(val message: ConversationMessage) : ReplyBody()
data class Reaction(val emoji: CharSequence) : ReplyBody()
data class RemoteDelete(val messageRecord: MessageRecord) : ReplyBody()
}
sealed class Key {
data class Text(val messageId: Long) : Key()
data class Reaction(val reactionId: Long) : Key()
data class RemoteDelete(val messageId: Long) : Key()
}
}

Wyświetl plik

@ -1,14 +1,23 @@
package org.thoughtcrime.securesms.stories.viewer.reply.group
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.paging.LivePagedData
import org.signal.core.util.ThreadUtil
import org.signal.paging.ObservablePagedData
import org.signal.paging.PagedData
import org.signal.paging.PagingConfig
import org.signal.paging.PagingController
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.colors.NameColors
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
class StoryGroupReplyRepository {
@ -19,38 +28,45 @@ class StoryGroupReplyRepository {
}.subscribeOn(Schedulers.io())
}
fun getPagedReplies(parentStoryId: Long): Observable<LivePagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> {
return Observable.create<LivePagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> { emitter ->
fun refresh() {
emitter.onNext(PagedData.createForLiveData(StoryGroupReplyDataSource(parentStoryId), PagingConfig.Builder().build()))
fun getPagedReplies(parentStoryId: Long): Observable<ObservablePagedData<MessageId, ReplyBody>> {
return getThreadId(parentStoryId)
.toObservable()
.flatMap { threadId ->
Observable.create<ObservablePagedData<MessageId, ReplyBody>> { emitter ->
val pagedData: ObservablePagedData<MessageId, ReplyBody> = PagedData.createForObservable(StoryGroupReplyDataSource(parentStoryId), PagingConfig.Builder().build())
val controller: PagingController<MessageId> = pagedData.controller
val updateObserver = DatabaseObserver.MessageObserver { controller.onDataItemChanged(it) }
val insertObserver = DatabaseObserver.MessageObserver { controller.onDataItemInserted(it, PagingController.POSITION_END) }
val conversationObserver = DatabaseObserver.Observer { controller.onDataInvalidated() }
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(updateObserver)
ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(threadId, insertObserver)
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(threadId, conversationObserver)
emitter.setCancellable {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(updateObserver)
ApplicationDependencies.getDatabaseObserver().unregisterObserver(insertObserver)
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver)
}
emitter.onNext(pagedData)
}.subscribeOn(Schedulers.io())
}
val observer = DatabaseObserver.Observer {
refresh()
}
val messageObserver = DatabaseObserver.MessageObserver {
refresh()
}
val threadId = SignalDatabase.mms.getThreadIdForMessage(parentStoryId)
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageObserver)
ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(threadId, messageObserver)
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(threadId, observer)
emitter.setCancellable {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer)
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver)
}
refresh()
}.subscribeOn(Schedulers.io())
}
fun getStoryOwner(storyId: Long): Single<RecipientId> {
return Single.fromCallable {
SignalDatabase.mms.getMessageRecord(storyId).individualRecipient.id
}.subscribeOn(Schedulers.io())
fun getNameColorsMap(storyId: Long, sessionMemberCache: MutableMap<GroupId, Set<Recipient>>): Observable<Map<RecipientId, NameColor>> {
return Single.fromCallable { SignalDatabase.mms.getMessageRecord(storyId).individualRecipient.id }
.subscribeOn(Schedulers.io())
.flatMapObservable { recipientId ->
Observable.create<Map<RecipientId, NameColor>?> { emitter ->
val nameColorsMapLiveData = NameColors.getNameColorsMapLiveData(MutableLiveData(recipientId), sessionMemberCache)
val observer = Observer<Map<RecipientId, NameColor>> { emitter.onNext(it) }
ThreadUtil.postToMain { nameColorsMapLiveData.observeForever(observer) }
emitter.setCancellable { ThreadUtil.postToMain { nameColorsMapLiveData.removeObserver(observer) } }
}.subscribeOn(Schedulers.io())
}
}
}

Wyświetl plik

@ -5,10 +5,12 @@ import org.thoughtcrime.securesms.recipients.RecipientId
data class StoryGroupReplyState(
val threadId: Long = 0L,
val noReplies: Boolean = true,
val replies: List<ReplyBody> = emptyList(),
val nameColors: Map<RecipientId, NameColor> = emptyMap(),
val loadState: LoadState = LoadState.INIT
) {
val noReplies: Boolean = replies.isEmpty()
enum class LoadState {
INIT,
READY

Wyświetl plik

@ -1,57 +1,52 @@
package org.thoughtcrime.securesms.stories.viewer.reply.group
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.signal.paging.LivePagedData
import org.signal.paging.PagingController
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.paging.ProxyPagingController
import org.thoughtcrime.securesms.conversation.colors.NameColors
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.livedata.Store
import org.thoughtcrime.securesms.util.rx.RxStore
class StoryGroupReplyViewModel(storyId: Long, repository: StoryGroupReplyRepository) : ViewModel() {
private val sessionMemberCache: MutableMap<GroupId, Set<Recipient>> = NameColors.createSessionMembersCache()
private val store = Store(StoryGroupReplyState())
private val store = RxStore(StoryGroupReplyState())
private val disposables = CompositeDisposable()
val stateSnapshot: StoryGroupReplyState = store.state
val state: LiveData<StoryGroupReplyState> = store.stateLiveData
val state: Flowable<StoryGroupReplyState> = store.stateFlowable
private val pagedData: MutableLiveData<LivePagedData<StoryGroupReplyItemData.Key, StoryGroupReplyItemData>> = MutableLiveData()
val pagingController: LiveData<PagingController<StoryGroupReplyItemData.Key>>
val pageData: LiveData<List<StoryGroupReplyItemData>>
val pagingController: ProxyPagingController<MessageId> = ProxyPagingController()
init {
disposables += repository.getThreadId(storyId).subscribe { threadId ->
store.update { it.copy(threadId = threadId) }
}
disposables += repository.getPagedReplies(storyId).subscribe {
pagedData.postValue(it)
}
pagingController = Transformations.map(pagedData) { it.controller }
pageData = Transformations.switchMap(pagedData) { it.data }
store.update(pageData) { data, state ->
state.copy(
noReplies = data.isEmpty(),
loadState = StoryGroupReplyState.LoadState.READY
)
}
disposables += repository.getStoryOwner(storyId).observeOn(AndroidSchedulers.mainThread()).subscribe { recipientId ->
store.update(NameColors.getNameColorsMapLiveData(MutableLiveData(recipientId), sessionMemberCache)) { nameColors, state ->
state.copy(nameColors = nameColors)
disposables += repository.getPagedReplies(storyId)
.doOnNext { pagingController.set(it.controller) }
.flatMap { it.data }
.subscribeBy { data ->
store.update { state ->
state.copy(
replies = data,
loadState = StoryGroupReplyState.LoadState.READY
)
}
}
disposables += repository.getNameColorsMap(storyId, sessionMemberCache)
.subscribeBy { nameColors ->
store.update { state ->
state.copy(nameColors = nameColors)
}
}
}
}
override fun onCleared() {

Wyświetl plik

@ -8,7 +8,6 @@ import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask
@ -75,7 +74,7 @@ object DeleteDialog {
private fun deleteForEveryone(messageRecords: Set<MessageRecord>, emitter: SingleEmitter<Boolean>) {
SignalExecutors.BOUNDED.execute {
messageRecords.forEach { message ->
MessageSender.sendRemoteDelete(ApplicationDependencies.getApplication(), message.id, message.isMms)
MessageSender.sendRemoteDelete(message.id, message.isMms)
}
emitter.onSuccess(false)

Wyświetl plik

@ -1,12 +1,24 @@
package org.thoughtcrime.securesms.util
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
var View.visible: Boolean
get() {
return this.visibility == View.VISIBLE
}
set(value) {
this.visibility = if (value) View.VISIBLE else View.GONE
}
fun View.padding(left: Int = paddingLeft, top: Int = paddingTop, right: Int = paddingRight, bottom: Int = paddingBottom) {
setPadding(left, top, right, bottom)
}
fun ConstraintLayout.changeConstraints(change: ConstraintSet.() -> Unit) {
val set = ConstraintSet()
set.clone(this)
set.change()
set.applyTo(this)
}

Wyświetl plik

@ -21,7 +21,7 @@ public abstract class MappingViewHolder<Model> extends RecyclerView.ViewHolder {
payload = new LinkedList<>();
}
public <T extends View> T findViewById(@IdRes int id) {
public final <T extends View> T findViewById(@IdRes int id) {
return itemView.findViewById(id);
}

Wyświetl plik

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="12dp">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/avatar"
android:layout_width="28dp"
android:layout_height="28dp"
app:layout_constraintBottom_toBottomOf="@id/body"
app:layout_constraintStart_toStartOf="parent" />
<org.thoughtcrime.securesms.components.FromTextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="7dp"
android:textAppearance="@style/TextAppearance.Signal.Subtitle.Bold"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@id/reaction"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="Miles Morales" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="7dp"
android:text="@string/StoryGroupReactionReplyItem__reacted_to_the_story"
android:textAppearance="@style/Signal.Text.Body"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/reaction"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/name" />
<TextView
android:id="@+id/viewed_at"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="5dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/signal_inverse_transparent_60"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/reaction"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/body"
tools:text="15m" />
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
android:id="@+id/reaction"
android:layout_width="28dp"
android:layout_height="28dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -1,105 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:background="@drawable/selectable_list_item_background"
android:clipToPadding="false"
android:paddingHorizontal="8dp"
android:paddingTop="6dp"
android:paddingBottom="6dp">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/avatar"
android:layout_width="28dp"
android:layout_height="28dp"
app:fallbackImageSize="small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/rounded_outline_secondary_18"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent">
<org.thoughtcrime.securesms.components.FromTextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="7dp"
android:layout_marginEnd="12dp"
android:textAppearance="@style/TextAppearance.Signal.Subtitle.Bold"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Miles Morales" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="1dp"
android:text="@string/ThreadRecord_this_message_was_deleted"
android:textAppearance="@style/Signal.Text.Body"
android:textStyle="italic"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/viewed_at_below"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_goneMarginBottom="7dp"
app:measureLastLine="true" />
<TextView
android:id="@+id/viewed_at"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="5dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/transparent_white_60"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/body"
tools:text="15m"
tools:textColor="@color/signal_text_secondary"
tools:visibility="visible" />
<TextView
android:id="@+id/viewed_at_below"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="5dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/transparent_white_60"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/bubble"
app:layout_constraintEnd_toEndOf="@id/bubble"
tools:text="15m"
tools:textColor="@color/signal_text_secondary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -19,86 +19,156 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bubble_start_barrier"
android:layout_width="1dp"
android:layout_height="1dp"
app:barrierDirection="end"
app:barrierMargin="8dp"
app:constraint_referenced_ids="avatar" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bubble_end_barrier"
android:layout_width="1dp"
android:layout_height="1dp"
app:barrierDirection="end"
app:barrierMargin="12dp"
app:constraint_referenced_ids="viewed_at,viewed_at_below,delivery_status,name,body" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/message_content_end_barrier"
android:layout_width="1dp"
android:layout_height="1dp"
app:barrierDirection="start"
app:barrierMargin="-8dp"
app:constraint_referenced_ids="alert_view" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/viewed_at_end_barrier"
android:layout_width="1dp"
android:layout_height="1dp"
app:barrierDirection="end"
app:barrierMargin="0dp"
app:constraint_referenced_ids="viewed_at,viewed_at_below" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/viewed_at_bottom_barrier"
android:layout_width="1dp"
android:layout_height="1dp"
app:barrierDirection="bottom"
app:barrierMargin="0dp"
app:constraint_referenced_ids="viewed_at,viewed_at_below" />
<View
android:id="@+id/bubble"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/rounded_rectangle_secondary_18"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/bubble_end_barrier"
app:layout_constraintStart_toStartOf="@+id/bubble_start_barrier"
app:layout_constraintTop_toTopOf="parent" />
<org.thoughtcrime.securesms.components.FromTextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="7dp"
android:layout_marginEnd="12dp"
android:textAppearance="@style/TextAppearance.Signal.Subtitle.Bold"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="@+id/message_content_end_barrier"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@+id/bubble_start_barrier"
app:layout_constraintTop_toTopOf="parent"
tools:text="Miles Morales" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="1dp"
android:textAppearance="@style/Signal.Text.Body"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/viewed_at_below"
app:layout_constraintEnd_toEndOf="@+id/message_content_end_barrier"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@+id/bubble_start_barrier"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_goneMarginBottom="7dp"
app:measureLastLine="true"
tools:text="This is a very long message that is supposed to properly wrap when it hits the end." />
<TextView
android:id="@+id/viewed_at"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginBottom="5dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/transparent_white_60"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/body"
tools:text="15m"
tools:textColor="@color/signal_text_secondary"
tools:visibility="visible" />
<TextView
android:id="@+id/viewed_at_below"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginBottom="5dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/transparent_white_60"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/message_content_end_barrier"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toStartOf="@+id/bubble_start_barrier"
tools:text="Not sent, tap for details"
tools:textColor="@color/signal_text_secondary" />
<org.thoughtcrime.securesms.components.DeliveryStatusView
android:id="@+id/delivery_status"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_marginStart="6dp"
android:layout_marginBottom="5dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/viewed_at_bottom_barrier"
app:layout_constraintStart_toEndOf="@+id/viewed_at_end_barrier" />
<org.thoughtcrime.securesms.components.AlertView
android:id="@+id/alert_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/rounded_rectangle_secondary_18"
app:layout_constrainedWidth="true"
android:layout_marginEnd="8dp"
android:visibility="gone"
app:layout_goneMarginEnd="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/reaction"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/bubble_end_barrier"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
android:id="@+id/reaction"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginStart="8dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent">
<org.thoughtcrime.securesms.components.FromTextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="7dp"
android:layout_marginEnd="12dp"
android:textAppearance="@style/TextAppearance.Signal.Subtitle.Bold"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Miles Morales" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="1dp"
android:textAppearance="@style/Signal.Text.Body"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/viewed_at_below"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_goneMarginBottom="7dp"
app:measureLastLine="true"
tools:text="This is a very long message that is supposed to properly wrap when it hits the end." />
<TextView
android:id="@+id/viewed_at"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="5dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/transparent_white_60"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/body"
tools:text="15m"
tools:textColor="@color/signal_text_secondary"
tools:visibility="visible" />
<TextView
android:id="@+id/viewed_at_below"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="5dp"
android:textAppearance="@style/Signal.Text.Caption"
android:textColor="@color/transparent_white_60"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/bubble"
app:layout_constraintEnd_toEndOf="@id/bubble"
tools:text="15m"
tools:textColor="@color/signal_text_secondary" />
</androidx.constraintlayout.widget.ConstraintLayout>
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -4561,6 +4561,8 @@
<string name="MyStories__delete_story">Delete story?</string>
<!-- Message of dialog to confirm deletion of story -->
<string name="MyStories__this_story_will_be_deleted">This story will be deleted for you and everyone who received it.</string>
<!-- Toast shown when story media cannot be saved -->
<string name="MyStories__unable_to_save">Unable to save</string>
<!-- Displayed at bottom of story viewer when current item has views -->
<plurals name="StoryViewerFragment__d_views">
<item quantity="one">%1$d view</item>

Wyświetl plik

@ -21,7 +21,7 @@ import java.util.concurrent.Executor;
*/
class FixedSizePagingController<Key, Data> implements PagingController<Key> {
private static final String TAG = FixedSizePagingController.class.getSimpleName();
private static final String TAG = Log.tag(FixedSizePagingController.class);
private static final Executor FETCH_EXECUTOR = SignalExecutors.newCachedSingleThreadExecutor("signal-FixedSizePagingController");
private static final boolean DEBUG = false;
@ -182,10 +182,15 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
}
@Override
public void onDataItemInserted(Key key, int position) {
if (DEBUG) Log.d(TAG, buildItemInsertedLog(key, position, ""));
public void onDataItemInserted(Key key, int inputPosition) {
if (DEBUG) Log.d(TAG, buildItemInsertedLog(key, inputPosition, ""));
FETCH_EXECUTOR.execute(() -> {
int position = inputPosition;
if (position == POSITION_END) {
position = data.size();
}
if (keyToPosition.containsKey(key)) {
Log.w(TAG, "Notified of key " + key + " being inserted at " + position + ", but the item already exists!");
return;
@ -245,6 +250,6 @@ class FixedSizePagingController<Key, Data> implements PagingController<Key> {
}
private String buildItemChangedLog(Key key, String message) {
return "[onDataItemInserted(" + key + "), size: " + loadState.size() + "] " + message;
return "[onDataItemChanged(" + key + "), size: " + loadState.size() + "] " + message;
}
}

Wyświetl plik

@ -2,6 +2,8 @@ package org.signal.paging;
public interface PagingController<Key> {
int POSITION_END = -1;
void onDataNeededAroundIndex(int aroundIndex);
void onDataInvalidated();
void onDataItemChanged(Key key);