Invitation improvements

This commit is contained in:
Phillip Thelen 2023-05-04 12:55:39 +02:00
parent 5c46ffeaa0
commit 79d9d744b4
10 changed files with 110 additions and 63 deletions

View file

@ -2,5 +2,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:topLeftRadius="22dp" android:topRightRadius="22dp" />
<solid android:color="?background" />
</shape>

View file

@ -8,6 +8,11 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?attr/colorContentBackground">
<include layout="@layout/shop_header"
android:id="@+id/npc_header"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:background="@color/window_background"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -11,6 +11,7 @@
android:background="@drawable/shop_header_background"
android:clipChildren="true"
android:clipToOutline="true"
android:clipToPadding="true"
android:layout_marginTop="8dp">
<com.habitrpg.android.habitica.ui.views.NPCBannerView
android:id="@+id/npcBannerView"

View file

@ -513,7 +513,7 @@
<style name="SectionHeaderCaps">
<item name="android:fontFamily">@string/font_family_medium</item>
<item name="android:textSize">10sp</item>
<item name="android:textSize">12sp</item>
<item name="android:textColor">@color/text_ternary</item>
<item name="android:textAllCaps">true</item>
<item name="android:layout_marginStart">@dimen/spacing_large</item>

View file

@ -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

View file

@ -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) {

View file

@ -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<InviteResponse>? {
suspend fun sendInvites(): List<InviteResponse>? {
val inviteMap = mapOf<String, MutableList<String>>(
"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<FragmentComposeBinding>() {
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<FragmentComposeBinding>() {
@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
}

View file

@ -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<PagingData<Member>>
val seekingUsers : Flow<PagingData<Member>>
val successfulInvites = mutableStateListOf<String>()
val inviteStates = mutableStateMapOf<String, LoadingButtonState>()
init {
seekingUsers = Pager(
config = PagingConfig(
@ -163,9 +165,6 @@ fun PartySeekingView(
val pullRefreshState = rememberPullRefreshState(refreshing, { pageData.refresh() })
val scope = rememberCoroutineScope()
val successfulInvites = remember { mutableListOf<String>() }
val inviteStates = remember { mutableMapOf<String, LoadingButtonState>() }
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
}
}
}

View file

@ -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<FragmentFaqOverviewBinding>() {
@ -32,6 +34,8 @@ class FAQOverviewFragment : BaseMainFragment<FragmentFaqOverviewBinding>() {
@Inject
lateinit var faqRepository: FAQRepository
@Inject
lateinit var configManager: AppConfigManager
override fun onCreateView(
inflater: LayoutInflater,
@ -46,6 +50,11 @@ class FAQOverviewFragment : BaseMainFragment<FragmentFaqOverviewBinding>() {
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<ImageView>(R.id.icon_view)?.setImageBitmap(
HabiticaIconsHelper.imageOfHeartLarge()
)

View file

@ -1,3 +1,7 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.4.0'
}
include 'Habitica', ':Habitica'
include ':wearos'
include ':common'