diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt index 71bcc68db6..f677b42e06 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt @@ -36,6 +36,8 @@ import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory +import org.thoughtcrime.securesms.polls.PollOption +import org.thoughtcrime.securesms.polls.PollRecord import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.stickers.StickerLocator @@ -348,5 +350,11 @@ class V2ConversationItemShapeTest { override fun onShowUnverifiedProfileSheet(forGroup: Boolean) = Unit override fun onUpdateSignalClicked() = Unit + + override fun onViewResultsClicked(pollId: Long) = Unit + + override fun onViewPollClicked(messageId: Long) = Unit + + override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean) = Unit } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/PollTablesTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/PollTablesTest.kt new file mode 100644 index 0000000000..85f7d2b870 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/PollTablesTest.kt @@ -0,0 +1,131 @@ +package org.thoughtcrime.securesms.database + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.signal.core.util.deleteAll +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.mms.IncomingMessage +import org.thoughtcrime.securesms.polls.PollOption +import org.thoughtcrime.securesms.polls.PollRecord +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.testing.SignalActivityRule + +@RunWith(AndroidJUnit4::class) +class PollTablesTest { + + @get:Rule + val harness = SignalActivityRule() + + private lateinit var poll1: PollRecord + + @Before + fun setUp() { + poll1 = PollRecord( + id = 1, + question = "how do you feel about unit testing?", + pollOptions = listOf( + PollOption(1, "yay", listOf(1)), + PollOption(2, "ok", emptyList()), + PollOption(3, "nay", emptyList()) + ), + allowMultipleVotes = false, + hasEnded = false, + authorId = 1, + messageId = 1 + ) + + SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollTable.TABLE_NAME) + SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollOptionTable.TABLE_NAME) + SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollVoteTable.TABLE_NAME) + + val message = IncomingMessage(type = MessageType.NORMAL, from = harness.others[0], sentTimeMillis = 100, serverTimeMillis = 100, receivedTimeMillis = 100) + SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(harness.others[0], isGroup = false)) + } + + @Test + fun givenAPollWithVoting_whenIGetPoll_thenIExpectThatPoll() { + SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1) + SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 1, messageId = MessageId(1)) + + assertEquals(poll1, SignalDatabase.polls.getPoll(1)) + } + + @Test + fun givenAPoll_whenIGetItsOptionIds_thenIExpectAllOptionsIds() { + SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1) + assertEquals(poll1.pollOptions.map { it.id }, SignalDatabase.polls.getPollOptionIds(1)) + } + + @Test + fun givenAPollAndVoter_whenIGetItsVoteCount_thenIExpectTheCorrectVoterCount() { + SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1) + SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 1, messageId = MessageId(1)) + SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(2), voterId = 2, voteCount = 2, messageId = MessageId(1)) + SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(3), voterId = 3, voteCount = 3, messageId = MessageId(1)) + + assertEquals(1, SignalDatabase.polls.getCurrentPollVoteCount(1, 1)) + assertEquals(2, SignalDatabase.polls.getCurrentPollVoteCount(1, 2)) + assertEquals(3, SignalDatabase.polls.getCurrentPollVoteCount(1, 3)) + } + + @Test + fun givenMultipleRoundsOfVoting_whenIGetItsCount_thenIExpectTheMostRecentResults() { + SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1) + SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(2), voterId = 1, voteCount = 1, messageId = MessageId(1)) + SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(3), voterId = 1, voteCount = 2, messageId = MessageId(1)) + SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 3, messageId = MessageId(1)) + + assertEquals(poll1, SignalDatabase.polls.getPoll(1)) + } + + @Test + fun givenAPoll_whenITerminateIt_thenIExpectItToEnd() { + SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1) + SignalDatabase.polls.endPoll(1, System.currentTimeMillis()) + + assertEquals(true, SignalDatabase.polls.getPoll(1)!!.hasEnded) + } + + @Test + fun givenAPoll_whenIIVote_thenIExpectThatVote() { + SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1) + val poll = SignalDatabase.polls.getPoll(1)!! + val pollOption = poll.pollOptions.first() + + val voteCount = SignalDatabase.polls.insertVote(poll, pollOption) + + assertEquals(1, voteCount) + assertEquals(listOf(0), SignalDatabase.polls.getVotes(poll.id, false)) + } + + @Test + fun givenAPoll_whenIRemoveVote_thenVoteIsCleared() { + SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1) + val poll = SignalDatabase.polls.getPoll(1)!! + val pollOption = poll.pollOptions.first() + + val voteCount = SignalDatabase.polls.removeVote(poll, pollOption) + SignalDatabase.polls.markPendingAsRemoved(poll.id, Recipient.self().id.toLong(), voteCount, 1) + + assertEquals(1, voteCount) + val status = SignalDatabase.polls.getPollVoteStateForGivenVote(poll.id, voteCount) + assertEquals(PollTables.VoteState.REMOVED, status) + } + + @Test + fun givenAVote_whenISetPollOptionId_thenOptionIdIsUpdated() { + SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1) + val poll = SignalDatabase.polls.getPoll(1)!! + val option = poll.pollOptions.first() + + SignalDatabase.polls.insertVotes(poll.id, listOf(option.id), Recipient.self().id.toLong(), 5, MessageId(1)) + SignalDatabase.polls.setPollVoteStateForGivenVote(poll.id, Recipient.self().id.toLong(), 5, 1, true) + val status = SignalDatabase.polls.getPollVoteStateForGivenVote(poll.id, 5) + + assertEquals(PollTables.VoteState.ADDED, status) + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/DataMessageProcessorTest_polls.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/DataMessageProcessorTest_polls.kt new file mode 100644 index 0000000000..d72c78e1b8 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/DataMessageProcessorTest_polls.kt @@ -0,0 +1,322 @@ +package org.thoughtcrime.securesms.messages + +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.thoughtcrime.securesms.database.MessageTable +import org.thoughtcrime.securesms.database.MessageType +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.mms.IncomingMessage +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.testing.GroupTestingUtils +import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember +import org.thoughtcrime.securesms.testing.MessageContentFuzzer +import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata +import org.whispersystems.signalservice.internal.push.DataMessage + +@Suppress("ClassName") +@RunWith(AndroidJUnit4::class) +class DataMessageProcessorTest_polls { + + @get:Rule + val harness = SignalActivityRule(createGroup = true) + + private lateinit var alice: Recipient + private lateinit var bob: Recipient + private lateinit var charlie: Recipient + private lateinit var groupId: GroupId.V2 + private lateinit var groupRecipientId: RecipientId + + @Before + fun setUp() { + alice = Recipient.resolved(harness.others[0]) + bob = Recipient.resolved(harness.others[1]) + charlie = Recipient.resolved(harness.others[2]) + + val groupInfo = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), alice.asMember(), bob.asMember()) + groupId = groupInfo.groupId + groupRecipientId = groupInfo.recipientId + } + + @Test + fun handlePollCreate_whenIHaveAValidPollProto_createPoll() { + val insertResult = handlePollCreate( + pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false), + senderRecipient = alice, + threadRecipient = Recipient.resolved(groupRecipientId), + groupId = groupId + ) + + assert(insertResult != null) + val poll = SignalDatabase.polls.getPoll(insertResult!!.messageId) + assert(poll != null) + assertThat(poll!!.question).isEqualTo("question?") + assertThat(poll.pollOptions.size).isEqualTo(3) + assertThat(poll.allowMultipleVotes).isEqualTo(false) + assertThat(poll.hasEnded).isEqualTo(false) + } + + @Test + fun handlePollCreate_whenSenderIsNotInGroup_dropMessage() { + val insertResult = handlePollCreate( + pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false), + senderRecipient = charlie, + threadRecipient = Recipient.resolved(groupRecipientId), + groupId = groupId + ) + + assert(insertResult == null) + } + + @Test + fun handlePollCreate_whenTargetRecipientIsNotAGroup_dropMessage() { + val insertResult = handlePollCreate( + pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false), + senderRecipient = alice, + threadRecipient = bob, + groupId = null + ) + + assert(insertResult == null) + } + + @Test + fun handlePollTerminate_whenIHaveValidProto_endPoll() { + val pollMessageId = insertPoll() + + val insertResult = DataMessageProcessor.handlePollTerminate( + context = ApplicationProvider.getApplicationContext(), + envelope = MessageContentFuzzer.envelope(200), + message = DataMessage(pollTerminate = DataMessage.PollTerminate(targetSentTimestamp = 100)), + senderRecipient = alice, + metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()), + threadRecipient = bob, + groupId = groupId, + receivedTime = 200 + ) + + assert(insertResult?.messageId != null) + val poll = SignalDatabase.polls.getPoll(pollMessageId) + assert(poll != null) + assert(poll!!.hasEnded) + } + + @Test + fun handlePollTerminate_whenIHaveDifferentTimestamp_dropMessage() { + insertPoll() + + val insertResult = DataMessageProcessor.handlePollTerminate( + context = ApplicationProvider.getApplicationContext(), + envelope = MessageContentFuzzer.envelope(200), + message = DataMessage(pollTerminate = DataMessage.PollTerminate(200)), + senderRecipient = alice, + metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()), + threadRecipient = bob, + groupId = groupId, + receivedTime = 200 + ) + + assert(insertResult == null) + } + + @Test + fun handlePollTerminate_whenMessageIsNotFromCreatorOfPoll_dropMessage() { + insertPoll() + + val insertResult = DataMessageProcessor.handlePollTerminate( + context = ApplicationProvider.getApplicationContext(), + envelope = MessageContentFuzzer.envelope(200), + message = DataMessage(pollTerminate = DataMessage.PollTerminate(100)), + senderRecipient = bob, + metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()), + threadRecipient = bob, + groupId = groupId, + receivedTime = 200 + ) + + assert(insertResult == null) + } + + @Test + fun handlePollTerminate_whenPollDoesNotExist_dropMessage() { + val insertResult = DataMessageProcessor.handlePollTerminate( + context = ApplicationProvider.getApplicationContext(), + envelope = MessageContentFuzzer.envelope(200), + message = DataMessage(pollTerminate = DataMessage.PollTerminate(100)), + senderRecipient = alice, + metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()), + threadRecipient = bob, + groupId = groupId, + receivedTime = 200 + ) + + assert(insertResult == null) + } + + @Test + fun handlePollVote_whenValidPollVote_processVote() { + insertPoll() + + val messageId = handlePollVote( + DataMessage.PollVote( + targetAuthorAciBinary = alice.asMember().aciBytes, + targetSentTimestamp = 100, + optionIndexes = listOf(0), + voteCount = 1 + ), + bob + ) + + assert(messageId != null) + assertThat(messageId!!.id).isEqualTo(1) + val poll = SignalDatabase.polls.getPoll(messageId.id) + assert(poll != null) + assertThat(poll!!.pollOptions[0].voterIds).isEqualTo(listOf(bob.id.toLong())) + } + + @Test + fun handlePollVote_whenMultipleVoteAllowed_processAllVote() { + insertPoll() + + val messageId = handlePollVote( + DataMessage.PollVote( + targetAuthorAciBinary = alice.asMember().aciBytes, + targetSentTimestamp = 100, + optionIndexes = listOf(0, 1, 2), + voteCount = 1 + ), + bob + ) + + assert(messageId != null) + val poll = SignalDatabase.polls.getPoll(messageId!!.id) + assert(poll != null) + assertThat(poll!!.pollOptions[0].voterIds).isEqualTo(listOf(bob.id.toLong())) + assertThat(poll.pollOptions[1].voterIds).isEqualTo(listOf(bob.id.toLong())) + assertThat(poll.pollOptions[2].voterIds).isEqualTo(listOf(bob.id.toLong())) + } + + @Test + fun handlePollVote_whenMultipleVoteSentToSingleVotePolls_dropMessage() { + insertPoll(false) + + val messageId = handlePollVote( + DataMessage.PollVote( + targetAuthorAciBinary = alice.asMember().aciBytes, + targetSentTimestamp = 100, + optionIndexes = listOf(0, 1, 2), + voteCount = 1 + ), + bob + ) + + assert(messageId == null) + } + + @Test + fun handlePollVote_whenVoteCountIsNotHigher_dropMessage() { + insertPoll() + + val messageId = handlePollVote( + DataMessage.PollVote( + targetAuthorAciBinary = alice.asMember().aciBytes, + targetSentTimestamp = 100, + optionIndexes = listOf(0, 1, 2), + voteCount = -1 + ), + bob + ) + + assert(messageId == null) + } + + @Test + fun handlePollVote_whenVoteOptionDoesNotExist_dropMessage() { + insertPoll() + + val messageId = handlePollVote( + DataMessage.PollVote( + targetAuthorAciBinary = alice.asMember().aciBytes, + targetSentTimestamp = 100, + optionIndexes = listOf(5), + voteCount = 1 + ), + bob + ) + + assert(messageId == null) + } + + @Test + fun handlePollVote_whenVoterNotInGroup_dropMessage() { + insertPoll() + + val messageId = handlePollVote( + DataMessage.PollVote( + targetAuthorAciBinary = alice.asMember().aciBytes, + targetSentTimestamp = 100, + optionIndexes = listOf(0, 1, 2), + voteCount = 1 + + ), + charlie + ) + + assert(messageId == null) + } + + @Test + fun handlePollVote_whenPollDoesNotExist_dropMessage() { + val messageId = handlePollVote( + DataMessage.PollVote( + targetAuthorAciBinary = alice.asMember().aciBytes, + targetSentTimestamp = 100, + optionIndexes = listOf(0, 1, 2), + voteCount = 1 + ), + bob + ) + + assert(messageId == null) + } + + private fun handlePollCreate(pollCreate: DataMessage.PollCreate, senderRecipient: Recipient, threadRecipient: Recipient, groupId: GroupId.V2?): MessageTable.InsertResult? { + return DataMessageProcessor.handlePollCreate( + envelope = MessageContentFuzzer.envelope(100), + message = DataMessage(pollCreate = pollCreate), + senderRecipient = senderRecipient, + threadRecipient = threadRecipient, + groupId = groupId, + receivedTime = 0, + context = ApplicationProvider.getApplicationContext(), + metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()) + ) + } + + private fun handlePollVote(pollVote: DataMessage.PollVote, senderRecipient: Recipient): MessageId? { + return DataMessageProcessor.handlePollVote( + context = ApplicationProvider.getApplicationContext(), + envelope = MessageContentFuzzer.envelope(100), + message = DataMessage(pollVote = pollVote), + senderRecipient = senderRecipient, + earlyMessageCacheEntry = null + ) + } + + private fun insertPoll(allowMultiple: Boolean = true): Long { + val envelope = MessageContentFuzzer.envelope(100) + val pollMessage = IncomingMessage(type = MessageType.NORMAL, from = alice.id, sentTimeMillis = envelope.timestamp!!, serverTimeMillis = envelope.serverTimestamp!!, receivedTimeMillis = 0, groupId = groupId) + val messageId = SignalDatabase.messages.insertMessageInbox(pollMessage).get() + SignalDatabase.polls.insertPoll("question?", allowMultiple, listOf("a", "b", "c"), alice.id.toLong(), messageId.messageId) + return messageId.messageId + } +} diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/ConversationElementGenerator.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/ConversationElementGenerator.kt index d350ba235b..2f7724dd71 100644 --- a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/ConversationElementGenerator.kt +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/ConversationElementGenerator.kt @@ -115,6 +115,7 @@ class ConversationElementGenerator { null, null, null, + null, -1, null, null, diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt index d6475e411f..2ca0d4e4d8 100644 --- a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt @@ -44,6 +44,8 @@ import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory +import org.thoughtcrime.securesms.polls.PollOption +import org.thoughtcrime.securesms.polls.PollRecord import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.stickers.StickerLocator @@ -336,5 +338,17 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra override fun onUpdateSignalClicked() { Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() } + + override fun onViewResultsClicked(pollId: Long) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onViewPollClicked(messageId: Long) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } + + override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } } } diff --git a/app/src/main/assets/fonts/SignalSymbols-Regular.otf b/app/src/main/assets/fonts/SignalSymbols-Regular.otf index c7e7d677f7..b647bf5897 100644 Binary files a/app/src/main/assets/fonts/SignalSymbols-Regular.otf and b/app/src/main/assets/fonts/SignalSymbols-Regular.otf differ diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index f3d357a23e..0ebf960358 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -30,6 +30,8 @@ import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory; +import org.thoughtcrime.securesms.polls.PollRecord; +import org.thoughtcrime.securesms.polls.PollOption; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.stickers.StickerLocator; @@ -143,5 +145,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, void onDisplayMediaNoLongerAvailableSheet(); void onShowUnverifiedProfileSheet(boolean forGroup); void onUpdateSignalClicked(); + void onViewResultsClicked(long pollId); + void onViewPollClicked(long messageId); + void onToggleVote(@NonNull PollRecord poll, @NonNull PollOption pollOption, Boolean isChecked); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/compose/RoundCheckbox.kt b/app/src/main/java/org/thoughtcrime/securesms/components/compose/RoundCheckbox.kt index 985a4c7902..8ab9d3fb88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/compose/RoundCheckbox.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/compose/RoundCheckbox.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Checkbox @@ -26,6 +25,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -34,6 +34,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.signal.core.ui.compose.DayNightPreviews import org.signal.core.ui.compose.theme.SignalTheme @@ -50,7 +51,11 @@ import org.thoughtcrime.securesms.R fun RoundCheckbox( checked: Boolean, onCheckedChange: (Boolean) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + size: Dp = 24.dp, + enabled: Boolean = true, + outlineColor: Color = MaterialTheme.colorScheme.outline, + checkedColor: Color = MaterialTheme.colorScheme.primary ) { val contentDescription = if (checked) { stringResource(R.string.SignalCheckbox_accessibility_checked_description) @@ -60,15 +65,14 @@ fun RoundCheckbox( Box( modifier = modifier - .padding(12.dp) - .size(24.dp) + .size(size) .aspectRatio(1f) .border( width = 1.5.dp, color = if (checked) { - MaterialTheme.colorScheme.primary + checkedColor } else { - MaterialTheme.colorScheme.outline + outlineColor }, shape = CircleShape ) @@ -76,7 +80,8 @@ fun RoundCheckbox( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { onCheckedChange(!checked) }, - onClickLabel = stringResource(R.string.SignalCheckbox_accessibility_on_click_label) + onClickLabel = stringResource(R.string.SignalCheckbox_accessibility_on_click_label), + enabled = enabled ) .semantics(mergeDescendants = true) { this.role = Role.Checkbox @@ -90,7 +95,7 @@ fun RoundCheckbox( ) { Image( imageVector = ImageVector.vectorResource(id = R.drawable.ic_check_circle_solid_24), - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), + colorFilter = ColorFilter.tint(checkedColor), contentDescription = null, modifier = Modifier.fillMaxSize() ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiStrings.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiStrings.java index 8e9678175a..3da38e6020 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiStrings.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiStrings.java @@ -10,5 +10,6 @@ public final class EmojiStrings { public static final String STICKER = "\u2B50"; public static final String GIFT = "\uD83C\uDF81"; public static final String CARD = "\uD83D\uDCB3"; + public static final String POLL = "\uD83D\uDCCA"; public static final String FAILED_STORY = "\u2757"; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java index c16ae1037d..68eeb4eb81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java @@ -35,6 +35,7 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout. private static final List DEFAULT_BUTTONS = Arrays.asList( AttachmentKeyboardButton.GALLERY, AttachmentKeyboardButton.FILE, + AttachmentKeyboardButton.POLL, AttachmentKeyboardButton.CONTACT, AttachmentKeyboardButton.LOCATION, AttachmentKeyboardButton.PAYMENT diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java index 20812700f1..63bc50ff3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java @@ -11,7 +11,8 @@ public enum AttachmentKeyboardButton { FILE(R.string.AttachmentKeyboard_file, R.drawable.symbol_file_24), PAYMENT(R.string.AttachmentKeyboard_payment, R.drawable.symbol_payment_24), CONTACT(R.string.AttachmentKeyboard_contact, R.drawable.symbol_person_circle_24), - LOCATION(R.string.AttachmentKeyboard_location, R.drawable.symbol_location_circle_24); + LOCATION(R.string.AttachmentKeyboard_location, R.drawable.symbol_location_circle_24), + POLL(R.string.AttachmentKeyboard_poll, R.drawable.symbol_poll_24); private final int titleRes; private final int iconRes; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index f9ba8715d9..e53a133599 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -56,6 +56,7 @@ import androidx.annotation.ColorInt; import androidx.annotation.DimenRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.compose.ui.platform.ComposeView; import androidx.core.content.ContextCompat; import androidx.lifecycle.LifecycleOwner; import androidx.media3.common.MediaItem; @@ -73,7 +74,6 @@ import org.signal.core.util.BidiUtil; import org.signal.core.util.DimensionUnit; import org.signal.core.util.StringUtil; import org.signal.core.util.logging.Log; -import org.signal.ringrtc.CallLinkRootKey; import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; @@ -130,6 +130,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.mms.TextSlide; import org.thoughtcrime.securesms.mms.VideoSlide; +import org.thoughtcrime.securesms.polls.PollRecord; import org.thoughtcrime.securesms.reactions.ReactionsConversationView; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; @@ -236,6 +237,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo private Stub