kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add sending and error states for story group replies.
rodzic
a29bc1da8c
commit
2a91c67c51
|
@ -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());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Ładowanie…
Reference in New Issue