From 79d9d744b4501f003d598809175221b412ad09b8 Mon Sep 17 00:00:00 2001 From: Phillip Thelen Date: Thu, 4 May 2023 12:55:39 +0200 Subject: [PATCH] Invitation improvements --- .../res/drawable/shop_header_background.xml | 1 - Habitica/res/layout/fragment_faq_overview.xml | 5 + Habitica/res/layout/shop_header.xml | 1 + Habitica/res/values/styles.xml | 2 +- .../habitica/models/tasks/ChecklistItem.kt | 2 +- .../habitica/models/tasks/RemindersItem.kt | 5 +- .../social/party/PartyInviteFragment.kt | 109 +++++++++++------- .../social/party/PartySeekingFragment.kt | 33 +++--- .../fragments/support/FAQOverviewFragment.kt | 11 +- settings.gradle | 4 + 10 files changed, 110 insertions(+), 63 deletions(-) diff --git a/Habitica/res/drawable/shop_header_background.xml b/Habitica/res/drawable/shop_header_background.xml index c00c2c6b2..c5aee6ded 100644 --- a/Habitica/res/drawable/shop_header_background.xml +++ b/Habitica/res/drawable/shop_header_background.xml @@ -2,5 +2,4 @@ - diff --git a/Habitica/res/layout/fragment_faq_overview.xml b/Habitica/res/layout/fragment_faq_overview.xml index 24ce17a89..38a92a57c 100644 --- a/Habitica/res/layout/fragment_faq_overview.xml +++ b/Habitica/res/layout/fragment_faq_overview.xml @@ -8,6 +8,11 @@ android:layout_height="wrap_content" android:orientation="vertical" android:background="?attr/colorContentBackground"> + @string/font_family_medium - 10sp + 12sp @color/text_ternary true @dimen/spacing_large diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/ChecklistItem.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/ChecklistItem.kt index 7a8c538e6..f526f11d7 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/ChecklistItem.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/ChecklistItem.kt @@ -18,7 +18,7 @@ open class ChecklistItem : RealmObject, BaseMainObject, Parcelable { get() = "id" @PrimaryKey - var id: String? = null + var id: String? = UUID.randomUUID().toString() var text: String? = null var completed: Boolean = false var position: Int = 0 diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/RemindersItem.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/RemindersItem.kt index ef70a2b6a..f7250cf4a 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/RemindersItem.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/RemindersItem.kt @@ -11,6 +11,7 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatterBuilder import java.time.temporal.TemporalAccessor +import java.util.UUID open class RemindersItem : RealmObject, Parcelable { @PrimaryKey @@ -43,7 +44,9 @@ open class RemindersItem : RealmObject, Parcelable { time = source.readString() } - constructor() + constructor() { + id = UUID.randomUUID().toString() + } override fun equals(other: Any?): Boolean { return if (other is RemindersItem) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInviteFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInviteFragment.kt index bf050d9db..b7bcd3e08 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInviteFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartyInviteFragment.kt @@ -11,11 +11,10 @@ import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -24,8 +23,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.material.TextFieldDefaults @@ -39,11 +38,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate -import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -67,15 +64,23 @@ import dagger.hilt.android.lifecycle.HiltViewModel import java.util.UUID import javax.inject.Inject +fun uUIDFromStringOrNull(name: String): UUID? { + return try { + UUID.fromString(name) + } catch (_: IllegalArgumentException) { + null + } +} + @HiltViewModel class PartyInviteViewModel @Inject constructor( - userRepository : UserRepository, - userViewModel : MainUserViewModel, - val socialRepository : SocialRepository + userRepository: UserRepository, + userViewModel: MainUserViewModel, + val socialRepository: SocialRepository ) : BaseViewModel(userRepository, userViewModel) { val invites = mutableStateListOf("") - suspend fun sendInvites() : List? { + suspend fun sendInvites(): List? { val inviteMap = mapOf>( "emails" to mutableListOf(), "uuids" to mutableListOf(), @@ -84,7 +89,7 @@ class PartyInviteViewModel @Inject constructor( for (invite in invites) { if (invite.isValidEmail()) { inviteMap["emails"]?.add(invite) - } else if (UUID.fromString(invite) != null) { + } else if (uUIDFromStringOrNull(invite) != null) { inviteMap["uuids"]?.add(invite) } else if (invite.isNotBlank()) { inviteMap["usernames"]?.add(invite) @@ -96,22 +101,22 @@ class PartyInviteViewModel @Inject constructor( @AndroidEntryPoint class PartyInviteFragment : BaseFragment() { - val viewModel : PartyInviteViewModel by viewModels() + val viewModel: PartyInviteViewModel by viewModels() - override var binding : FragmentComposeBinding? = null + override var binding: FragmentComposeBinding? = null override fun createBinding( - inflater : LayoutInflater, - container : ViewGroup? - ) : FragmentComposeBinding { + inflater: LayoutInflater, + container: ViewGroup? + ): FragmentComposeBinding { return FragmentComposeBinding.inflate(inflater, container, false) } override fun onCreateView( - inflater : LayoutInflater, - container : ViewGroup?, - savedInstanceState : Bundle? - ) : View? { + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { val view = super.onCreateView(inflater, container, savedInstanceState) binding?.composeView?.setContent { HabiticaTheme { @@ -125,18 +130,18 @@ class PartyInviteFragment : BaseFragment() { @OptIn(ExperimentalFoundationApi::class) @Composable fun PartyInviteView( - viewModel : PartyInviteViewModel + viewModel: PartyInviteViewModel ) { - var inviteButtonState : LoadingButtonState by remember { mutableStateOf(LoadingButtonState.CONTENT) } + var inviteButtonState: LoadingButtonState by remember { mutableStateOf(LoadingButtonState.CONTENT) } val scope = rememberCoroutineScope() val scrollableState = rememberScrollState() - val invites = viewModel.invites LazyColumn( Modifier .fillMaxSize() .padding(14.dp) - .scrollable(scrollableState, Orientation.Vertical)) { + .scrollable(scrollableState, Orientation.Vertical) + ) { item { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -155,40 +160,61 @@ fun PartyInviteView( ) } } - items(invites.indices.toList()) { index -> - val invite = invites[index] - val transition = updateTransition(invites.size - 1 == index, label = "isLast") + items(viewModel.invites.indices.toList()) { index -> + val invite = viewModel.invites[index] + val transition = updateTransition(viewModel.invites.size - 1 == index, label = "isLast") val rotation = transition.animateFloat( label = "isAssigned", transitionSpec = { spring(Spring.DampingRatioLowBouncy, Spring.StiffnessMediumLow) } ) { if (it) 135f else 0f } - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .fillMaxWidth() - .padding(0.dp, 4.dp) - .background(HabiticaTheme.colors.windowBackground, HabiticaTheme.shapes.medium) - .padding(4.dp, 4.dp) - .animateItemPlacement()) { - Image( - painterResource(R.drawable.ic_close_white_24dp), - null, - colorFilter = ColorFilter.tint(HabiticaTheme.colors.textPrimary), + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier + .fillMaxWidth() + .padding(0.dp, 4.dp) + .background(HabiticaTheme.colors.windowBackground, HabiticaTheme.shapes.medium) + .padding(4.dp, 4.dp) + .animateItemPlacement() + ) { + Button( + onClick = { + if (viewModel.invites.size - 1 >= index && viewModel.invites[index].isNotBlank()) { + viewModel.invites.removeAt(index) + } + }, + colors = ButtonDefaults.textButtonColors(), + elevation = ButtonDefaults.elevation(0.dp), + contentPadding = PaddingValues(0.dp), modifier = Modifier - .rotate(rotation.value) .size(32.dp) .padding(3.dp) - ) + ) { + Image( + painterResource(R.drawable.ic_close_white_24dp), + null, + colorFilter = ColorFilter.tint(HabiticaTheme.colors.textPrimary), + modifier = Modifier + .rotate(rotation.value) + .size(32.dp) + ) + } + TextField( value = invite, onValueChange = { value -> - if (invites.size - 1 == index && invites[index].isBlank()) { + if (viewModel.invites.size - 1 == index && viewModel.invites[index].isBlank()) { viewModel.invites.add("") } viewModel.invites[index] = value }, singleLine = true, placeholder = { Text(stringResource(R.string.username_or_email)) }, - colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent), + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + textColor = HabiticaTheme.colors.textPrimary + ), modifier = Modifier .onFocusChanged { if (!it.isFocused) { @@ -202,7 +228,7 @@ fun PartyInviteView( } item { InviteButton( - state = if (invites.any { it.isNotBlank() }) inviteButtonState else LoadingButtonState.DISABLED, + state = if (viewModel.invites.any { it.isNotBlank() }) inviteButtonState else LoadingButtonState.DISABLED, modifier = Modifier.fillMaxWidth(), onClick = { inviteButtonState = LoadingButtonState.LOADING @@ -213,6 +239,7 @@ fun PartyInviteView( if (responses?.isNotEmpty() == true) { inviteButtonState = LoadingButtonState.SUCCESS viewModel.invites.clear() + viewModel.invites.add("") } else { inviteButtonState = LoadingButtonState.FAILED } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt index 44f3fc5ca..401066fe1 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/party/PartySeekingFragment.kt @@ -20,8 +20,9 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -73,8 +74,9 @@ class PartySeekingViewModel @Inject constructor( val socialRepository : SocialRepository ) : BaseViewModel(userRepository, userViewModel) { val isRefreshing = mutableStateOf(false) - var seekingUsers : Flow> - + val seekingUsers : Flow> + val successfulInvites = mutableStateListOf() + val inviteStates = mutableStateMapOf() init { seekingUsers = Pager( config = PagingConfig( @@ -163,9 +165,6 @@ fun PartySeekingView( val pullRefreshState = rememberPullRefreshState(refreshing, { pageData.refresh() }) val scope = rememberCoroutineScope() - val successfulInvites = remember { mutableListOf() } - val inviteStates = remember { mutableMapOf() } - Box( modifier = modifier .fillMaxSize() @@ -209,26 +208,26 @@ fun PartySeekingView( if (it == null) return@items PartySeekingListItem( user = it, - inviteState = inviteStates[it.id] ?: LoadingButtonState.CONTENT, - isInvited = successfulInvites.contains(it.id), + inviteState =viewModel.inviteStates[it.id] ?: LoadingButtonState.CONTENT, + isInvited = viewModel.successfulInvites.contains(it.id), modifier = Modifier.animateItemPlacement() ) { member -> scope.launchCatching({ - inviteStates[member.id] = LoadingButtonState.FAILED + viewModel.inviteStates[member.id] = LoadingButtonState.FAILED }) { - inviteStates[member.id] = LoadingButtonState.LOADING - val response = if (successfulInvites.contains(member.id)) viewModel.inviteUser(member) else viewModel.inviteUser(member) + viewModel.inviteStates[member.id] = LoadingButtonState.LOADING + val response: Any? = if (viewModel.successfulInvites.contains(member.id)) viewModel.rescindInvite(member) else viewModel.inviteUser(member) if (response != null) { - if (successfulInvites.contains(member.id)) { - successfulInvites.remove(member.id) + if (viewModel.successfulInvites.contains(member.id)) { + viewModel.successfulInvites.remove(member.id) } else { - successfulInvites.add(member.id) + viewModel.successfulInvites.add(member.id) } - inviteStates[member.id] = LoadingButtonState.SUCCESS + viewModel.inviteStates[member.id] = LoadingButtonState.SUCCESS delay(4.toDuration(DurationUnit.SECONDS)) - inviteStates[member.id] = LoadingButtonState.CONTENT + viewModel.inviteStates[member.id] = LoadingButtonState.CONTENT } else { - inviteStates[member.id] = LoadingButtonState.FAILED + viewModel.inviteStates[member.id] = LoadingButtonState.FAILED } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/support/FAQOverviewFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/support/FAQOverviewFragment.kt index 3dfd60305..90cce8a47 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/support/FAQOverviewFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/support/FAQOverviewFragment.kt @@ -7,19 +7,21 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.core.os.bundleOf +import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.data.FAQRepository import com.habitrpg.android.habitica.databinding.FragmentFaqOverviewBinding import com.habitrpg.android.habitica.databinding.SupportFaqItemBinding +import com.habitrpg.android.habitica.helpers.AppConfigManager import com.habitrpg.android.habitica.helpers.MainNavigationController import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper import com.habitrpg.common.habitica.extensions.layoutInflater import com.habitrpg.common.habitica.helpers.launchCatching import com.habitrpg.common.habitica.helpers.setMarkdown -import javax.inject.Inject import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class FAQOverviewFragment : BaseMainFragment() { @@ -32,6 +34,8 @@ class FAQOverviewFragment : BaseMainFragment() { @Inject lateinit var faqRepository: FAQRepository + @Inject + lateinit var configManager: AppConfigManager override fun onCreateView( inflater: LayoutInflater, @@ -46,6 +50,11 @@ class FAQOverviewFragment : BaseMainFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding?.npcHeader?.npcBannerView?.shopSpriteSuffix = configManager.shopSpriteSuffix() + binding?.npcHeader?.npcBannerView?.identifier = "tavern" + binding?.npcHeader?.namePlate?.setText(R.string.tavern_owner) + binding?.npcHeader?.descriptionView?.isVisible = false + binding?.healthSection?.findViewById(R.id.icon_view)?.setImageBitmap( HabiticaIconsHelper.imageOfHeartLarge() ) diff --git a/settings.gradle b/settings.gradle index d2fe38dbf..24fb3f996 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,7 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.4.0' +} + include 'Habitica', ':Habitica' include ':wearos' include ':common'