diff --git a/wallet/res/drawable/ic_thumb_down_red.xml b/wallet/res/drawable/ic_thumb_down_red.xml
new file mode 100644
index 000000000..3ca1632e8
--- /dev/null
+++ b/wallet/res/drawable/ic_thumb_down_red.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/wallet/res/drawable/ic_thumb_down_white.xml b/wallet/res/drawable/ic_thumb_down_white.xml
new file mode 100644
index 000000000..8156ea16a
--- /dev/null
+++ b/wallet/res/drawable/ic_thumb_down_white.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/wallet/res/drawable/ic_thumb_up_blue.xml b/wallet/res/drawable/ic_thumb_up_blue.xml
new file mode 100644
index 000000000..55dec1e2d
--- /dev/null
+++ b/wallet/res/drawable/ic_thumb_up_blue.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/wallet/res/drawable/ic_thumb_up_white.xml b/wallet/res/drawable/ic_thumb_up_white.xml
new file mode 100644
index 000000000..a22d1332f
--- /dev/null
+++ b/wallet/res/drawable/ic_thumb_up_white.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/wallet/res/layout/dialog_username_request_filters.xml b/wallet/res/layout/dialog_username_request_filters.xml
index 7f5c0b87d..6b25ae8c8 100644
--- a/wallet/res/layout/dialog_username_request_filters.xml
+++ b/wallet/res/layout/dialog_username_request_filters.xml
@@ -66,12 +66,40 @@
android:layout_marginTop="5dp"
app:layout_constraintTop_toBottomOf="@id/header">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wallet/res/layout/username_request_group_view.xml b/wallet/res/layout/username_request_group_view.xml
index 7d21e9eab..e0e284c1e 100644
--- a/wallet/res/layout/username_request_group_view.xml
+++ b/wallet/res/layout/username_request_group_view.xml
@@ -62,7 +62,7 @@
android:id="@+id/link_included"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="link included"
+ android:text="@string/link_included"
style="@style/Caption.Blue"
app:layout_constraintTop_toTopOf="@id/link_badge"
app:layout_constraintStart_toEndOf="@id/link_badge" />
@@ -79,32 +79,61 @@
app:layout_constraintBottom_toBottomOf="@id/username"
tools:text="4 requests" />
-
+ tools:text="2" >
+
+
+
-
-
+ tools:text="2" >
+
+
+
-
+ tools:text="2" >
+
+
+
\ No newline at end of file
diff --git a/wallet/res/navigation/nav_voting.xml b/wallet/res/navigation/nav_voting.xml
index f05381875..7a557d922 100644
--- a/wallet/res/navigation/nav_voting.xml
+++ b/wallet/res/navigation/nav_voting.xml
@@ -74,6 +74,10 @@
android:name="requestId"
app:argType="string" />
+
+
Only duplicates
Only requests with links
With links
- Date: %s
+ Request Date: %s
Votes: %s
+ Voting ends: %s
+
+
+ - None
+ - Soonest first
+ - Latest first
+
- New to old
@@ -296,16 +303,6 @@
- I have not approved
- Has Blocked votes
- Unblock
-
- - Block
- - Blocks
-
-
- - Approval
- - Approvals
-
- %d Block(s)
Create your username
Please note that you will not be able to change it in the future
Request Username
@@ -452,4 +449,10 @@
To help prevent other people from seeing who you make payments to, it is recommended to mix your balance before you create your username.
XKm5koXHMm7UrVV3ki2pbGA8yZztiSR2F9x6ucEyCSuqTHBMjJix
cUR2TrX4U6t6g9kRfDoBbwF2WRKBNjV3gK7HwTgQgmUUQCKXcT3N
+ Voting Period
+ Voting ends in %d days
+ Voting ends today
+ Voting ends tomorrow
+ link included
+ Group by
diff --git a/wallet/src/de/schildbach/wallet/database/dao/UsernameRequestDao.kt b/wallet/src/de/schildbach/wallet/database/dao/UsernameRequestDao.kt
index b42a0c709..6d8ac770e 100644
--- a/wallet/src/de/schildbach/wallet/database/dao/UsernameRequestDao.kt
+++ b/wallet/src/de/schildbach/wallet/database/dao/UsernameRequestDao.kt
@@ -35,6 +35,9 @@ interface UsernameRequestDao {
@Query("SELECT * FROM username_requests WHERE requestId = :requestId")
suspend fun getRequest(requestId: String): UsernameRequest?
+ @Query("SELECT * FROM username_requests WHERE normalizedLabel = :normalizedLabel")
+ suspend fun getRequestsByNormalizedLabel(normalizedLabel: String): List
+
@Query("SELECT * FROM username_requests WHERE requestId = :requestId")
fun observeRequest(requestId: String): Flow
diff --git a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestDetailsFragment.kt b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestDetailsFragment.kt
index 8a2827652..fa51aa890 100644
--- a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestDetailsFragment.kt
+++ b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestDetailsFragment.kt
@@ -1,6 +1,7 @@
package de.schildbach.wallet.ui.username
import android.os.Bundle
+import android.text.format.DateFormat
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
@@ -92,6 +93,13 @@ class UsernameRequestDetailsFragment : Fragment(R.layout.fragment_username_reque
}
}
}
+ val votingPeriod = args.startDate.let { startTime ->
+ val endTime = startTime + UsernameRequest.VOTING_PERIOD_MILLIS
+ val dateFormat = DateFormat.getMediumDateFormat(requireContext())
+ String.format("%s - %s", dateFormat.format(startTime), dateFormat.format(endTime))
+ }
+
+ binding.votingPeriod.text = votingPeriod
}
viewModel.selectUsernameRequest(args.requestId)
diff --git a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestFilterDialog.kt b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestFilterDialog.kt
index 7b38e7feb..0913f2f4b 100644
--- a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestFilterDialog.kt
+++ b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestFilterDialog.kt
@@ -29,6 +29,16 @@ import org.dash.wallet.common.ui.radio_group.RadioGroupAdapter
import org.dash.wallet.common.ui.radio_group.setupRadioGroup
import org.dash.wallet.common.ui.viewBinding
+enum class UsernameGroupOption {
+ VotingPeriodNone,
+ VotingPeriodSoonest,
+ VotingPeriodLatest;
+
+ companion object {
+ val defaultOption = VotingPeriodSoonest
+ }
+}
+
enum class UsernameSortOption {
DateDescending,
DateAscending,
@@ -36,7 +46,7 @@ enum class UsernameSortOption {
VotesAscending;
companion object {
- val defaultOption = DateDescending
+ val defaultOption = DateAscending
}
}
@@ -60,6 +70,7 @@ class UsernameRequestFilterDialog : OffsetDialogFragment(R.layout.dialog_usernam
private lateinit var typeOptionsAdapter: RadioGroupAdapter
private lateinit var sortByOptionsAdapter: RadioGroupAdapter
+ private lateinit var groupByOptionsAdapter: RadioGroupAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -81,17 +92,37 @@ class UsernameRequestFilterDialog : OffsetDialogFragment(R.layout.dialog_usernam
}
private fun setupSortByOptions() {
- val optionNames = binding.root.resources.getStringArray(R.array.usernames_sort_by_options).mapIndexed { i, it ->
+
+ val sortByOptionNames = binding.root.resources.getStringArray(R.array.usernames_sort_by_options).mapIndexed { i, it ->
IconifiedViewItem(getString(if (i < 2) R.string.date else R.string.votes, it))
}
+ val groupByOptionNames = binding.root.resources.getStringArray(R.array.usernames_group_by_options).mapIndexed { i, it ->
+ IconifiedViewItem(
+ if (i == 0) {
+ it
+ } else {
+ getString(R.string.voting_ends, it)
+ }
+ )
+ }
+
+ val groupOption = viewModel.filterState.value.groupByOption
+ val initialGroupIndex = UsernameGroupOption.entries.indexOf(groupOption)
+ val groupAdapter = RadioGroupAdapter(initialGroupIndex) { _, _ ->
+ checkResetButton()
+ }
+ binding.groupByFilter.setupRadioGroup(groupAdapter)
+ groupAdapter.submitList(groupByOptionNames)
+ groupByOptionsAdapter = groupAdapter
+
val sortOption = viewModel.filterState.value.sortByOption
- val initialIndex = UsernameSortOption.values().indexOf(sortOption)
+ val initialIndex = UsernameSortOption.entries.indexOf(sortOption)
val adapter = RadioGroupAdapter(initialIndex) { _, _ ->
checkResetButton()
}
binding.sortByFilter.setupRadioGroup(adapter)
- adapter.submitList(optionNames)
+ adapter.submitList(sortByOptionNames)
sortByOptionsAdapter = adapter
}
@@ -116,18 +147,20 @@ class UsernameRequestFilterDialog : OffsetDialogFragment(R.layout.dialog_usernam
}
private fun applyFilters() {
- val sortByOption = UsernameSortOption.values()[sortByOptionsAdapter.selectedIndex]
- val typeOption = UsernameTypeOption.values()[typeOptionsAdapter.selectedIndex]
+ val groupByOption = UsernameGroupOption.entries[groupByOptionsAdapter.selectedIndex]
+ val sortByOption = UsernameSortOption.entries[sortByOptionsAdapter.selectedIndex]
+ val typeOption = UsernameTypeOption.entries[typeOptionsAdapter.selectedIndex]
val onlyDuplicates = binding.onlyDuplicatesCheckbox.isChecked
val onlyLinks = binding.onlyLinksCheckbox.isChecked
- viewModel.applyFilters(sortByOption, typeOption, onlyDuplicates, onlyLinks)
+ viewModel.applyFilters(groupByOption, sortByOption, typeOption, onlyDuplicates, onlyLinks)
}
private fun checkResetButton() {
var isEnabled = false
- if (sortByOptionsAdapter.selectedIndex != UsernameSortOption.defaultOption.ordinal ||
+ if (groupByOptionsAdapter.selectedIndex != UsernameGroupOption.defaultOption.ordinal ||
+ sortByOptionsAdapter.selectedIndex != UsernameSortOption.defaultOption.ordinal ||
typeOptionsAdapter.selectedIndex != UsernameTypeOption.defaultOption.ordinal
) {
isEnabled = true
@@ -145,6 +178,7 @@ class UsernameRequestFilterDialog : OffsetDialogFragment(R.layout.dialog_usernam
}
private fun resetFilters() {
+ groupByOptionsAdapter.selectedIndex = UsernameGroupOption.defaultOption.ordinal
sortByOptionsAdapter.selectedIndex = UsernameSortOption.defaultOption.ordinal
typeOptionsAdapter.selectedIndex = UsernameTypeOption.defaultOption.ordinal
binding.onlyDuplicatesCheckbox.isChecked = true
diff --git a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsFragment.kt b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsFragment.kt
index 0bda5358d..e0f9e8360 100644
--- a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsFragment.kt
+++ b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsFragment.kt
@@ -23,6 +23,7 @@ import android.view.ViewTreeObserver
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
@@ -31,14 +32,18 @@ import de.schildbach.wallet.database.entity.UsernameRequest
import de.schildbach.wallet.database.entity.UsernameVote
import de.schildbach.wallet.livedata.Status
import de.schildbach.wallet.ui.dashpay.work.BroadcastUsernameVotesWorker
+import de.schildbach.wallet.ui.main.HistoryRowView
import de.schildbach.wallet.ui.username.adapters.UsernameRequestGroupAdapter
import de.schildbach.wallet.ui.username.adapters.UsernameRequestGroupView
+import de.schildbach.wallet.ui.username.adapters.UsernameRequestRowView
import de.schildbach.wallet.ui.username.utils.votingViewModels
import de.schildbach.wallet.ui.username.voting.OneVoteLeftDialogFragment
import de.schildbach.wallet_test.R
import de.schildbach.wallet_test.databinding.FragmentUsernameRequestsBinding
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.dash.wallet.common.services.analytics.AnalyticsConstants
import org.dash.wallet.common.ui.dialogs.AdaptiveDialog
import org.dash.wallet.common.ui.viewBinding
@@ -50,6 +55,8 @@ import org.dashj.platform.dpp.voting.LockVoteChoice
import org.dashj.platform.dpp.voting.ResourceVoteChoice
import org.dashj.platform.dpp.voting.TowardsIdentity
import org.slf4j.LoggerFactory
+import java.time.Instant
+import java.time.ZoneId
@AndroidEntryPoint
class UsernameRequestsFragment : Fragment(R.layout.fragment_username_requests) {
@@ -79,7 +86,10 @@ class UsernameRequestsFragment : Fragment(R.layout.fragment_username_requests) {
lifecycleScope.launch {
if (request.requestId != "") {
viewModel.logEvent(AnalyticsConstants.UsernameVoting.DETAILS)
- safeNavigate(UsernameRequestsFragmentDirections.requestsToDetails(request.requestId))
+ val votingStartDate = withContext(Dispatchers.IO) {
+ viewModel.getVotingStartDate(request.normalizedLabel)
+ }
+ safeNavigate(UsernameRequestsFragmentDirections.requestsToDetails(request.requestId, votingStartDate))
} else {
performVote(request)
}
@@ -326,7 +336,18 @@ class UsernameRequestsFragment : Fragment(R.layout.fragment_username_requests) {
val list = filterByQuery(itemList, binding.search.text.toString())
val layoutManager = binding.requestGroups.layoutManager as LinearLayoutManager
val scrollPosition = layoutManager.findFirstVisibleItemPosition()
- adapter.submitList(list)
+ val listForAdapter = if (list.isNotEmpty() && viewModel.filterState.value.groupByOption != UsernameGroupOption.VotingPeriodNone) {
+ list.groupBy {
+ Instant.ofEpochMilli(it.votingEndDate).atZone(ZoneId.systemDefault()).toLocalDate()
+ }.map {
+ val outList = mutableListOf()
+ outList.add(UsernameRequestRowView(it.key))
+ outList.apply { addAll(it.value) }
+ }.reduce { acc, list -> acc.apply { addAll(list) } }
+ } else {
+ requests
+ }
+ adapter.submitList(listForAdapter)
binding.requestGroups.scrollToPosition(scrollPosition)
}
@@ -430,14 +451,17 @@ class UsernameRequestsFragment : Fragment(R.layout.fragment_username_requests) {
.start()
viewModel.voteHandled()
+ // replace with our custom toast?
binding.voteSubmittedIndicator.postDelayed({
- binding.voteSubmittedIndicator.animate()
- .alpha(0f)
- .setDuration(animationDuration)
- .withEndAction {
- binding.voteSubmittedIndicator.isVisible = false
- }
- .start()
+ if (isAdded && viewLifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
+ binding.voteSubmittedIndicator.animate()
+ .alpha(0f)
+ .setDuration(animationDuration)
+ .withEndAction {
+ binding.voteSubmittedIndicator.isVisible = false
+ }
+ .start()
+ }
}, 3000L)
}
diff --git a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt
index e792516c6..0e114c1c0 100644
--- a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt
+++ b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt
@@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -56,13 +57,8 @@ import org.bitcoinj.core.Base58
import org.bitcoinj.core.DumpedPrivateKey
import org.bitcoinj.core.ECKey
import org.bitcoinj.core.KeyId
-import org.bitcoinj.core.MasternodeAddress
-import org.bitcoinj.core.NetworkParameters
import org.bitcoinj.core.Sha256Hash
import org.bitcoinj.core.Utils
-import org.bitcoinj.crypto.BLSLazyPublicKey
-import org.bitcoinj.crypto.BLSPublicKey
-import org.bitcoinj.evolution.SimplifiedMasternodeListEntry
import org.bitcoinj.evolution.SimplifiedMasternodeListManager
import org.bitcoinj.wallet.AuthenticationKeyChain
import org.bitcoinj.wallet.authentication.AuthenticationKeyStatus
@@ -102,6 +98,7 @@ data class UsernameRequestsUIState(
)
data class FiltersUIState(
+ val groupByOption: UsernameGroupOption = UsernameGroupOption.defaultOption,
val sortByOption: UsernameSortOption = UsernameSortOption.defaultOption,
val typeOption: UsernameTypeOption = UsernameTypeOption.defaultOption,
val onlyDuplicates: Boolean = true,
@@ -189,8 +186,48 @@ class UsernameRequestsViewModel @Inject constructor(
}?.username?.lowercase() ?: list[0].username
}
}
- UsernameRequestGroupView(prettyUsername, sortedList, isExpanded = isExpanded(prettyUsername), votes)
- }.filterNot { it.requests.isEmpty() }
+ val votingEndDate = if (sortedList.isNotEmpty()) {
+ sortedList.minOf { request -> request.createdAt } + UsernameRequest.VOTING_PERIOD_MILLIS
+ } else {
+ -1L
+ }
+ UsernameRequestGroupView(prettyUsername, sortedList, isExpanded = isExpanded(prettyUsername), votes, votingEndDate)
+ }.filterNot { it.requests.isEmpty() && it.votingEndDate < System.currentTimeMillis() }
+ }.map { groupViews -> // Sort the list emitted by the Flow
+ when (_filterState.value.groupByOption) {
+ UsernameGroupOption.VotingPeriodSoonest -> groupViews.sortedWith(
+ when (_filterState.value.sortByOption) {
+ UsernameSortOption.DateAscending -> compareBy { group -> group.localDate }.thenBy { group -> group.requests.minOf { request -> request.createdAt } }
+ UsernameSortOption.DateDescending -> compareBy { group -> group.localDate }.thenByDescending { group -> group.requests.minOf { request -> request.createdAt } }
+ UsernameSortOption.VotesAscending -> compareBy { group -> group.localDate }.thenBy { group -> group.requests.maxOf { request -> request.votes } }
+ UsernameSortOption.VotesDescending -> compareBy { group -> group.localDate }.thenByDescending { group -> group.requests.maxOf { request -> request.votes } }
+ }
+ )
+ UsernameGroupOption.VotingPeriodLatest -> groupViews.sortedWith(
+ when (_filterState.value.sortByOption) {
+ UsernameSortOption.DateAscending -> compareByDescending { group -> group.localDate }.thenBy { group -> group.requests.minOf { request -> request.createdAt } }
+ UsernameSortOption.DateDescending -> compareByDescending { group -> group.localDate }.thenByDescending { group -> group.requests.minOf { request -> request.createdAt } }
+ UsernameSortOption.VotesAscending -> compareByDescending { group -> group.localDate }.thenBy { group -> group.requests.maxOf { request -> request.votes } }
+ UsernameSortOption.VotesDescending -> compareByDescending { group -> group.localDate }.thenByDescending { group -> group.requests.maxOf { request -> request.votes } }
+ }
+ )
+ else -> {
+ when (_filterState.value.sortByOption) {
+ UsernameSortOption.DateAscending -> groupViews.sortedBy {
+ it.requests.minOf { request -> request.createdAt }
+ }
+ UsernameSortOption.DateDescending -> groupViews.sortedByDescending {
+ it.requests.minOf { request -> request.createdAt }
+ }
+ UsernameSortOption.VotesAscending -> groupViews.sortedBy {
+ it.requests.maxOf { request -> request.votes }
+ }
+ UsernameSortOption.VotesDescending -> groupViews.sortedByDescending {
+ it.requests.maxOf { request -> request.votes }
+ }
+ }
+ }
+ }
}
}.onEach { requests -> _uiState.update { it.copy(filteredUsernameRequests = requests) } }
.launchIn(viewModelWorkerScope)
@@ -245,6 +282,7 @@ class UsernameRequestsViewModel @Inject constructor(
}
fun applyFilters(
+ groupByOption: UsernameGroupOption,
sortByOption: UsernameSortOption,
typeOption: UsernameTypeOption,
onlyDuplicates: Boolean,
@@ -252,6 +290,7 @@ class UsernameRequestsViewModel @Inject constructor(
) {
_filterState.update {
it.copy(
+ groupByOption = groupByOption,
sortByOption = sortByOption,
typeOption = typeOption,
onlyDuplicates = onlyDuplicates,
@@ -714,4 +753,10 @@ class UsernameRequestsViewModel @Inject constructor(
suspend fun isImported(masternode: ImportedMasternodeKey): Boolean {
return importedMasternodeKeyDao.contains(masternode.proTxHash)
}
+
+ suspend fun getVotingStartDate(normalizedLabel: String): Long {
+ return usernameRequestDao.getRequestsByNormalizedLabel(normalizedLabel).minOf {
+ it.createdAt
+ }
+ }
}
diff --git a/wallet/src/de/schildbach/wallet/ui/username/adapters/UsernameRequestGroupAdapter.kt b/wallet/src/de/schildbach/wallet/ui/username/adapters/UsernameRequestGroupAdapter.kt
index cb59a1c2e..d50a13b16 100644
--- a/wallet/src/de/schildbach/wallet/ui/username/adapters/UsernameRequestGroupAdapter.kt
+++ b/wallet/src/de/schildbach/wallet/ui/username/adapters/UsernameRequestGroupAdapter.kt
@@ -16,12 +16,14 @@
*/
package de.schildbach.wallet.ui.username.adapters
-import android.util.TypedValue
import android.util.TypedValue.COMPLEX_UNIT_SP
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
@@ -30,11 +32,14 @@ import androidx.recyclerview.widget.RecyclerView
import de.schildbach.wallet.database.entity.UsernameRequest
import de.schildbach.wallet.database.entity.UsernameVote
import de.schildbach.wallet_test.R
+import de.schildbach.wallet_test.databinding.UsernameRequestDateViewBinding
import de.schildbach.wallet_test.databinding.UsernameRequestGroupViewBinding
import de.schildbach.wallet_test.databinding.UsernameRequestViewBinding
import org.dash.wallet.common.ui.setRoundedRippleBackground
import org.dashj.platform.sdk.platform.Names
import java.text.SimpleDateFormat
+import java.time.LocalDate
+import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.Locale
@@ -48,52 +53,122 @@ fun Button.setVoteThemeColors(
setTextSize(COMPLEX_UNIT_SP, 12.0f)
}
+private fun setVoteThemeColors(
+ button: LinearLayout,
+ textView: TextView,
+ imageView: ImageView,
+ backgroundStyle: Int,
+ textColor: Int,
+ iconResource: Int
+) {
+ button.setRoundedRippleBackground(backgroundStyle)
+ textView.setTextColor(textView.context.getColor(textColor))
+ textView.setTextSize(COMPLEX_UNIT_SP, 12.0f)
+ imageView.setImageResource(iconResource)
+}
+
class UsernameRequestGroupAdapter(
private val usernameDetailsClickListener: (UsernameRequest) -> Unit,
private val voteClickListener: (UsernameRequest) -> Unit
-): ListAdapter(
+): ListAdapter(
DiffCallback()
) {
- class DiffCallback : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: UsernameRequestGroupView, newItem: UsernameRequestGroupView): Boolean {
- return oldItem.username == newItem.username
+ companion object {
+ private const val DATE_TYPE = 0
+ private const val GROUP_TYPE = 1
+ }
+ class DiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: UsernameRequestRowView, newItem: UsernameRequestRowView): Boolean {
+ return oldItem is UsernameRequestGroupView &&
+ newItem is UsernameRequestGroupView &&
+ oldItem.username == newItem.username || oldItem.localDate == newItem.localDate
}
- override fun areContentsTheSame(oldItem: UsernameRequestGroupView, newItem: UsernameRequestGroupView): Boolean {
+ override fun areContentsTheSame(oldItem: UsernameRequestRowView, newItem: UsernameRequestRowView): Boolean {
return oldItem == newItem
}
}
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UsernameRequestGroupViewHolder {
- val binding = UsernameRequestGroupViewBinding.inflate(
- LayoutInflater.from(parent.context),
- parent,
- false
- )
+ override fun getItemViewType(position: Int): Int {
+ return when {
+ currentList[position] is UsernameRequestGroupView -> GROUP_TYPE
+ else -> DATE_TYPE
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractUsernameRequestGroupViewHolder {
+ return when (viewType) {
+ GROUP_TYPE -> {
+ val binding = UsernameRequestGroupViewBinding.inflate(
- return UsernameRequestGroupViewHolder(binding)
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ UsernameRequestGroupViewHolder(binding)
+ }
+
+ DATE_TYPE -> {
+ val binding = UsernameRequestDateViewBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ DateViewHolder(binding)
+ }
+ else -> error("unknown viewType $viewType")
+ }
}
- override fun onBindViewHolder(holder: UsernameRequestGroupViewHolder, position: Int) {
- val item = currentList[position]
- val hasMoreThanOneRequest = item.requests.size > 1
- holder.bind(item, usernameDetailsClickListener, voteClickListener)
- holder.binding.root.setOnClickListener {
- // expand if there is more than 1 request, otherwise show details on click
- if (hasMoreThanOneRequest) {
- val index = currentList.indexOfFirst { it.username == item.username }
- currentList[index].isExpanded = !currentList[index].isExpanded
- notifyItemChanged(index)
- } else {
- usernameDetailsClickListener.invoke(item.requests.first())
+ override fun onBindViewHolder(holder: AbstractUsernameRequestGroupViewHolder, position: Int) {
+ when (val item = currentList[position]) {
+ is UsernameRequestGroupView -> {
+ val hasMoreThanOneRequest = item.requests.size > 1
+ holder as UsernameRequestGroupViewHolder
+ holder.bind(item, usernameDetailsClickListener, voteClickListener)
+ holder.binding.root.setOnClickListener {
+ // expand if there is more than 1 request, otherwise show details on click
+ if (hasMoreThanOneRequest) {
+ val index = currentList.indexOfFirst { it is UsernameRequestGroupView && it.username == item.username }
+ item.isExpanded = !item.isExpanded
+ notifyItemChanged(index)
+ } else {
+ usernameDetailsClickListener.invoke(item.requests.first())
+ }
+ }
}
+ is UsernameRequestRowView -> {
+ holder as DateViewHolder
+ holder.bind(item)
+ }
+ }
+ }
+}
+
+abstract class AbstractUsernameRequestGroupViewHolder(view: View): RecyclerView.ViewHolder(view)
+
+class DateViewHolder(
+ val binding: UsernameRequestDateViewBinding
+): AbstractUsernameRequestGroupViewHolder(binding.root) {
+ fun bind(row: UsernameRequestRowView) {
+ val now = LocalDate.now()
+ val isToday = now == row.localDate
+ val isTomorrow = !isToday && row.localDate == now.plusDays(1)
+
+ binding.dateHeading.text = when {
+ isToday -> binding.root.context.getString(R.string.voting_period_ends_today)
+ isTomorrow -> binding.root.context.getString(R.string.voting_period_ends_tomorrow)
+ else -> binding.root.context.getString(
+ R.string.voting_period_ends_in_days,
+ ChronoUnit.DAYS.between(now, row.localDate)
+ )
}
}
}
class UsernameRequestGroupViewHolder(
val binding: UsernameRequestGroupViewBinding
-): RecyclerView.ViewHolder(binding.root) {
+): AbstractUsernameRequestGroupViewHolder(binding.root) {
fun bind(option: UsernameRequestGroupView, usernameClickListener: (UsernameRequest) -> Unit, voteClickListener: (UsernameRequest) -> Unit) {
val hasMoreThanOneRequest = option.requests.size > 1
binding.username.text = option.username
@@ -106,13 +181,13 @@ class UsernameRequestGroupViewHolder(
binding.linkIncluded.isVisible = !hasMoreThanOneRequest && option.requests.first().link != null
val context = binding.root.context
- binding.blocksButton.text = context.getString(R.string.two_lines_number_text, option.lockVotes(), context.resources.getQuantityString(R.plurals.block_vote_button, option.lockVotes()))
- binding.blocksButton.setOnClickListener {
+ binding.blocksButtonText.text = option.lockVotes().toString()
+ binding.blocksButtonContainer.setOnClickListener {
usernameClickListener.invoke(UsernameRequest.block(option.username, Names.normalizeString(option.username)))
}
- binding.approvalsButton.isVisible = !hasMoreThanOneRequest
- binding.approvalsButton.text = context.getString(R.string.two_lines_number_text, option.requests.maxOf { it.votes }, context.resources.getQuantityString(R.plurals.approval_button, option.requests.maxOf { it.votes }))
- binding.approvalsButton.setOnClickListener {
+ binding.approvalsButtonContainer.isVisible = !hasMoreThanOneRequest
+ binding.approvalsButtonText.text = option.requests.maxOf { it.votes }.toString()
+ binding.approvalsButtonContainer.setOnClickListener {
// vote for the first request, which is the only request
voteClickListener.invoke(option.requests.first())
}
@@ -120,19 +195,19 @@ class UsernameRequestGroupViewHolder(
// change the button colors based on our vote(s)
when (option.lastVote?.type?.lowercase()) {
UsernameVote.APPROVE -> {
- binding.blocksButton.text = context.getString(R.string.two_lines_number_text, option.lockVotes(), context.resources.getQuantityString(R.plurals.block_vote_button, option.lockVotes()))
- binding.blocksButton.setVoteThemeColors(R.style.PrimaryButtonTheme_Large_LightRed, R.color.red)
- binding.approvalsButton.setVoteThemeColors(R.style.PrimaryButtonTheme_Large_Blue, R.color.dash_white)
+ binding.blocksButtonText.text = option.lockVotes().toString()
+ setVoteThemeColors(binding.blocksButtonContainer, binding.blocksButtonText, binding.blocksButtonIcon, R.style.PrimaryButtonTheme_Large_LightRed, R.color.red, R.drawable.ic_thumb_down_red)
+ setVoteThemeColors(binding.approvalsButtonContainer, binding.approvalsButtonText, binding.approvalsButtonIcon, R.style.PrimaryButtonTheme_Large_Blue, R.color.dash_white, R.drawable.ic_thumb_up_white)
}
UsernameVote.LOCK -> {
- binding.blocksButton.text = context.getString(R.string.two_lines_number_text, option.lockVotes(), context.getString(R.string.unblock_button))
- binding.blocksButton.setVoteThemeColors(R.style.PrimaryButtonTheme_Large_Red, R.color.dash_white)
- binding.approvalsButton.setVoteThemeColors(R.style.PrimaryButtonTheme_Large_LightBlue, R.color.dash_blue)
+ binding.blocksButtonText.text = option.lockVotes().toString()
+ setVoteThemeColors(binding.blocksButtonContainer, binding.blocksButtonText, binding.blocksButtonIcon, R.style.PrimaryButtonTheme_Large_Red, R.color.dash_white, R.drawable.ic_thumb_down_white)
+ setVoteThemeColors(binding.approvalsButtonContainer, binding.approvalsButtonText, binding.approvalsButtonIcon, R.style.PrimaryButtonTheme_Large_LightBlue, R.color.dash_blue, R.drawable.ic_thumb_up_blue)
}
else -> {
- binding.blocksButton.text = context.getString(R.string.two_lines_number_text, option.lockVotes(), context.resources.getQuantityString(R.plurals.block_vote_button, option.lockVotes()))
- binding.blocksButton.setVoteThemeColors(R.style.PrimaryButtonTheme_Large_LightRed, R.color.red)
- binding.approvalsButton.setVoteThemeColors(R.style.PrimaryButtonTheme_Large_LightBlue, R.color.dash_blue)
+ binding.blocksButtonText.text = option.lockVotes().toString()
+ setVoteThemeColors(binding.blocksButtonContainer, binding.blocksButtonText, binding.blocksButtonIcon, R.style.PrimaryButtonTheme_Large_LightRed, R.color.red, R.drawable.ic_thumb_down_red)
+ setVoteThemeColors(binding.approvalsButtonContainer, binding.approvalsButtonText, binding.approvalsButtonIcon, R.style.PrimaryButtonTheme_Large_LightBlue, R.color.dash_blue, R.drawable.ic_thumb_up_blue)
}
}
@@ -215,25 +290,24 @@ class UsernameRequestViewHolder(
val binding: UsernameRequestViewBinding
): AbstractUsernameRequestViewHolder(binding.root) {
override fun bind(request: UsernameRequest, votes: List) {
- val context = binding.root.context
val dateFormat = SimpleDateFormat("dd MMM yyyy ยท hh:mm a", Locale.getDefault())
binding.dateRegistered.text = dateFormat.format(Date(request.createdAt))
- binding.approvalsButton.text = context.getString(R.string.two_lines_number_text, request.votes, context.resources.getQuantityString(R.plurals.approval_button, request.votes))
+ binding.approvalsButtonText.text = request.votes.toString()
val lastVote = votes.lastOrNull()
when (lastVote?.type) {
UsernameVote.APPROVE -> {
if (lastVote.identity == request.identity) {
- binding.approvalsButton.setVoteThemeColors(R.style.PrimaryButtonTheme_Large_Blue, R.color.dash_white)
+ setVoteThemeColors(binding.approvalsButtonContainer, binding.approvalsButtonText, binding.approvalsButtonIcon, R.style.PrimaryButtonTheme_Large_Blue, R.color.dash_white, R.drawable.ic_thumb_up_white)
} else {
- binding.approvalsButton.setVoteThemeColors(R.style.PrimaryButtonTheme_Large_LightBlue, R.color.dash_blue)
+ setVoteThemeColors(binding.approvalsButtonContainer, binding.approvalsButtonText, binding.approvalsButtonIcon, R.style.PrimaryButtonTheme_Large_LightBlue, R.color.dash_blue, R.drawable.ic_thumb_up_blue)
}
}
UsernameVote.LOCK, UsernameVote.ABSTAIN -> {
- binding.approvalsButton.setVoteThemeColors(R.style.PrimaryButtonTheme_Large_LightBlue, R.color.dash_blue)
+ setVoteThemeColors(binding.approvalsButtonContainer, binding.approvalsButtonText, binding.approvalsButtonIcon, R.style.PrimaryButtonTheme_Large_LightBlue, R.color.dash_blue, R.drawable.ic_thumb_up_blue)
}
else -> {
- binding.approvalsButton.setVoteThemeColors(R.style.PrimaryButtonTheme_Large_LightBlue, R.color.dash_blue)
+ setVoteThemeColors(binding.approvalsButtonContainer, binding.approvalsButtonText, binding.approvalsButtonIcon, R.style.PrimaryButtonTheme_Large_LightBlue, R.color.dash_blue, R.drawable.ic_thumb_up_blue)
}
}
@@ -245,7 +319,7 @@ class UsernameRequestViewHolder(
binding.root.setOnClickListener {
listener.invoke(usernameRequest)
}
- binding.approvalsButton.setOnClickListener {
+ binding.approvalsButtonContainer.setOnClickListener {
voteClickListener.invoke(usernameRequest)
}
}
diff --git a/wallet/src/de/schildbach/wallet/ui/username/adapters/UsernameRequestGroupView.kt b/wallet/src/de/schildbach/wallet/ui/username/adapters/UsernameRequestGroupView.kt
index e26ec05c0..bb9c57b30 100644
--- a/wallet/src/de/schildbach/wallet/ui/username/adapters/UsernameRequestGroupView.kt
+++ b/wallet/src/de/schildbach/wallet/ui/username/adapters/UsernameRequestGroupView.kt
@@ -19,13 +19,31 @@ package de.schildbach.wallet.ui.username.adapters
import de.schildbach.wallet.database.entity.UsernameRequest
import de.schildbach.wallet.database.entity.UsernameVote
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+/** Base class for items in the Username Request list. This will be used to display
+ * hold a date to display some presentation of time.
+ */
+open class UsernameRequestRowView(
+ val localDate: LocalDate
+) {
+ override fun equals(other: Any?): Boolean {
+ return other is UsernameRequestRowView && other.localDate == localDate
+ }
+}
+
+/** Represents a username contest that has one or more username requests
+ * for the same normalizedLabel.
+ */
data class UsernameRequestGroupView(
val username: String,
val requests: List,
var isExpanded: Boolean = false,
- var votes: List
-) {
+ var votes: List,
+ val votingEndDate: Long
+) : UsernameRequestRowView(Instant.ofEpochMilli(votingEndDate).atZone(ZoneId.systemDefault()).toLocalDate()) {
val lastVote: UsernameVote?
get() = votes.lastOrNull()
diff --git a/wallet/src/de/schildbach/wallet/ui/username/voting/RequestUserNameViewModel.kt b/wallet/src/de/schildbach/wallet/ui/username/voting/RequestUserNameViewModel.kt
index bd158cbf6..f92d660ad 100644
--- a/wallet/src/de/schildbach/wallet/ui/username/voting/RequestUserNameViewModel.kt
+++ b/wallet/src/de/schildbach/wallet/ui/username/voting/RequestUserNameViewModel.kt
@@ -420,4 +420,10 @@ class RequestUserNameViewModel @Inject constructor(
fun logEvent(event: String) {
analytics.logEvent(event, mapOf())
}
+
+ suspend fun getVotingStartDate(normalizedLabel: String): Long {
+ return usernameRequestDao.getRequestsByNormalizedLabel(normalizedLabel).minOf {
+ it.createdAt
+ }
+ }
}
diff --git a/wallet/src/de/schildbach/wallet/ui/username/voting/VotingRequestDetailsFragment.kt b/wallet/src/de/schildbach/wallet/ui/username/voting/VotingRequestDetailsFragment.kt
index 675548d2a..74cb0a830 100644
--- a/wallet/src/de/schildbach/wallet/ui/username/voting/VotingRequestDetailsFragment.kt
+++ b/wallet/src/de/schildbach/wallet/ui/username/voting/VotingRequestDetailsFragment.kt
@@ -24,10 +24,14 @@ import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
import de.schildbach.wallet.Constants
import de.schildbach.wallet.database.entity.UsernameRequest
import de.schildbach.wallet_test.R
import de.schildbach.wallet_test.databinding.FragmentVotingRequestDetailsBinding
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.bitcoinj.core.NetworkParameters
import org.dash.wallet.common.ui.dialogs.AdaptiveDialog
import org.dash.wallet.common.ui.viewBinding
@@ -58,35 +62,44 @@ class VotingRequestDetailsFragment : Fragment(R.layout.fragment_voting_request_d
binding.username.text = myUsernameRequest?.username
binding.identity.text = myUsernameRequest?.identity
var isVotingOver = false
- val votingResults = myUsernameRequest?.createdAt?.let { startTime ->
- val endTime = startTime + UsernameRequest.VOTING_PERIOD_MILLIS
- val dateFormat = DateFormat.getMediumDateFormat(requireContext())
- isVotingOver = endTime < System.currentTimeMillis()
- if (isVotingOver) {
- getString(R.string.request_username_taken_results)
- } else {
- dateFormat.format(endTime)
+ lifecycleScope.launch {
+ val startDate = withContext(Dispatchers.IO) {
+ myUsernameRequest?.let {
+ requestUserNameViewModel.getVotingStartDate(it.normalizedLabel)
+ }
}
- } ?: "Voting Period not found"
- binding.votingRange.text = votingResults
- when {
- isVotingOver -> {
- binding.link.text = if (myUsernameRequest?.link != null && myUsernameRequest.link != "") {
- myUsernameRequest.link
+ val votingResults = startDate?.let { startTime ->
+ val endTime = startTime + UsernameRequest.VOTING_PERIOD_MILLIS
+ val dateFormat = DateFormat.getMediumDateFormat(requireContext())
+ isVotingOver = endTime < System.currentTimeMillis()
+ if (isVotingOver) {
+ getString(R.string.request_username_taken_results)
} else {
- getString(R.string.none)
+ dateFormat.format(endTime)
+ }
+ } ?: "Voting period not found"
+ binding.votingRange.text = votingResults
+ when {
+ isVotingOver -> {
+ binding.link.text = if (myUsernameRequest?.link != null && myUsernameRequest.link != "") {
+ myUsernameRequest.link
+ } else {
+ getString(R.string.none)
+ }
+ binding.linkLayout.isVisible = true
+ binding.verifyNowLayout.isVisible = false
+ }
+
+ myUsernameRequest?.link != null && myUsernameRequest.link != "" -> {
+ binding.link.text = myUsernameRequest.link
+ binding.linkLayout.isVisible = true
+ binding.verifyNowLayout.isVisible = false
+ }
+
+ else -> {
+ binding.linkLayout.isVisible = false
+ binding.verifyNowLayout.isVisible = true
}
- binding.linkLayout.isVisible = true
- binding.verifyNowLayout.isVisible = false
- }
- myUsernameRequest?.link != null && myUsernameRequest.link != "" -> {
- binding.link.text = myUsernameRequest.link
- binding.linkLayout.isVisible = true
- binding.verifyNowLayout.isVisible = false
- }
- else -> {
- binding.linkLayout.isVisible = false
- binding.verifyNowLayout.isVisible = true
}
}
}