party invite flow improvements

This commit is contained in:
Phillip Thelen 2023-05-08 10:48:50 +02:00
parent 3619ed2810
commit 89b7408c3b
17 changed files with 138 additions and 116 deletions

View file

@ -6,6 +6,7 @@
<attr name="colorTintedBackground" format="color" />
<attr name="colorTintedBackgroundOffset" format="color" />
<attr name="colorContentBackgroundOffset" format="color" />
<attr name="colorOffsetBackground" format="color" />
<attr name="colorWindowBackground" format="color" />
<attr name="colorGradientTransparentBackground" format="color" />
<attr name="textColorPrimary" format="color" />

View file

@ -1367,11 +1367,11 @@
<string name="pause_damage_3_description">Damage to a boss or found collection items will be stored until you resume damage</string>
<string name="list">List</string>
<string name="by_invite">By Invite</string>
<string name="habiticans_looking_party">Heres a list of Habiticans looking to join a Party</string>
<string name="habiticans_looking_party">Heres a list of players looking to join a Party</string>
<string name="habiticans_looking_party_empty">There\'s no one looking for a Party right now. You can check back later!</string>
<string name="invited">Invited</string>
<string name="invite_with_username_email">Invite with @username or email</string>
<string name="habiticans_send_invite">Send an invite directly to Habiticans you know</string>
<string name="habiticans_send_invite">Send an invite directly to players you know</string>
<string name="username_or_email">Username or email address</string>
<string name="seeking_party_description">Want to join a Party with others but dont know any other players? Let Party leaders know youre looking for an invite!</string>
<string name="seeking_party_title">Looking for a Party?</string>
@ -1384,7 +1384,7 @@
<string name="shop_armoire_description">New gear is released during the seasonal Galas. Until then, theres %d pieces of gear in the Enchanted Armoire to find!</string>
<string name="shop_armoire_empty_description">New gear is released during the seasonal Galas. The Enchanted Armoire gets new stock every month too!</string>
<string name="unlock_gear_and_skills">Unlock %s gear and skills</string>
<string name="rescind_invite">Rescind Invite</string>
<string name="rescind_invite">Cancel Invite</string>
<string name="rescinded">Rescinded</string>
<string name="pending_invite">Pending Invite</string>
<string name="invalid_input">Input invalid</string>

View file

@ -39,6 +39,7 @@
<item name="colorContentBackground">@color/content_background</item>
<item name="colorContentBackgroundOffset">@color/content_background_offset</item>
<item name="colorOffsetBackground">@color/offset_background</item>
<item name="colorWindowBackground">@color/window_background</item>
<item name="colorTintedBackground">@color/brand_800</item>
<item name="colorGradientTransparentBackground">@color/transparent</item>

View file

@ -44,10 +44,8 @@ class NavigationDrawerAdapter(tintColor: Int, backgroundTintColor: Int) : Recycl
internal val items: MutableList<HabiticaDrawerItem> = ArrayList()
var selectedItem: Int? = null
set(value) {
val oldValue = field
field = value
oldValue?.let { notifyItemChanged(it) }
value?.let { notifyItemChanged(it) }
notifyDataSetChanged()
}
var itemSelectedEvents: ((HabiticaDrawerItem) -> Unit)? = null

View file

@ -8,6 +8,8 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@ -49,7 +51,9 @@ class ChatFragment : BaseFragment<FragmentChatBinding>() {
return FragmentChatBinding.inflate(inflater, container, false)
}
lateinit var viewModel : GroupViewModel
val viewModel: GroupViewModel by viewModels(
ownerProducer = { parentFragment as Fragment }
)
@Inject
lateinit var configManager : AppConfigManager
@ -131,6 +135,11 @@ class ChatFragment : BaseFragment<FragmentChatBinding>() {
}
}
override fun onResume() {
super.onResume()
setNavigatedToFragment()
}
private fun setReplyTo(username : String?) {
val previousMessage = binding?.chatBarView?.message ?: ""
if (previousMessage.contains("@$username")) {

View file

@ -18,8 +18,8 @@ import com.habitrpg.android.habitica.models.social.Group
import com.habitrpg.android.habitica.ui.fragments.BaseMainFragment
import com.habitrpg.android.habitica.ui.viewmodels.GroupViewModel
import com.habitrpg.android.habitica.ui.viewmodels.GroupViewType
import javax.inject.Inject
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class TavernFragment : BaseMainFragment<FragmentViewpagerBinding>() {
@ -87,9 +87,7 @@ class TavernFragment : BaseMainFragment<FragmentViewpagerBinding>() {
TavernDetailFragment()
}
1 -> {
val fragment = ChatFragment()
fragment.viewModel = viewModel
fragment
ChatFragment()
}
else -> Fragment()
}

View file

@ -9,6 +9,8 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.MainNavDirections
import com.habitrpg.android.habitica.R
@ -54,16 +56,18 @@ class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
return FragmentGuildDetailBinding.inflate(inflater, container, false)
}
var viewModel: GroupViewModel? = null
val viewModel: GroupViewModel by viewModels(
ownerProducer = { parentFragment as Fragment }
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.refreshLayout?.setOnRefreshListener { this.refresh() }
viewModel?.getGroupData()?.observe(viewLifecycleOwner, { updateGuild(it) })
viewModel?.getLeaderData()?.observe(viewLifecycleOwner, { setLeader(it) })
viewModel?.getIsMemberData()?.observe(viewLifecycleOwner, { updateMembership(it) })
viewModel.getGroupData().observe(viewLifecycleOwner, { updateGuild(it) })
viewModel.getLeaderData().observe(viewLifecycleOwner, { setLeader(it) })
viewModel.getIsMemberData().observe(viewLifecycleOwner, { updateMembership(it) })
binding?.guildDescription?.movementMethod = LinkMovementMethod.getInstance()
binding?.guildSummary?.movementMethod = LinkMovementMethod.getInstance()
@ -72,7 +76,7 @@ class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
leaveGuild()
}
binding?.joinButton?.setOnClickListener {
viewModel?.joinGroup {
viewModel.joinGroup {
(this.activity as? SnackbarActivity)?.showSnackbar(title = getString(R.string.joined_guild))
}
}
@ -81,7 +85,7 @@ class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
sendInvitesResult.launch(intent)
}
binding?.leaderWrapper?.setOnClickListener {
viewModel?.leaderID?.let { leaderID ->
viewModel.leaderID?.let { leaderID ->
val profileDirections = MainNavDirections.openProfileActivity(leaderID)
MainNavigationController.navigate(profileDirections)
}
@ -106,7 +110,7 @@ class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
private val sendInvitesResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val inviteData = HashMap<String, Any>()
inviteData["inviter"] = viewModel?.user?.value?.profile?.name ?: ""
inviteData["inviter"] = viewModel.user.value?.profile?.name ?: ""
val emails = it.data?.getStringArrayExtra(GroupInviteActivity.EMAILS_KEY)
if (emails != null && emails.isNotEmpty()) {
val invites = ArrayList<HashMap<String, String>>()
@ -124,12 +128,12 @@ class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
userIDs.forEach { invites.add(it) }
inviteData["usernames"] = invites
}
viewModel?.inviteToGroup(inviteData)
viewModel.inviteToGroup(inviteData)
}
}
private fun refresh() {
viewModel?.retrieveGroup {
viewModel.retrieveGroup {
binding?.refreshLayout?.isRefreshing = false
}
}
@ -140,7 +144,7 @@ class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
userRepository.getUser().collect {
it?.challenges?.forEach { membership ->
val challenge = challengeRepository.getChallenge(membership.challengeID).firstOrNull()
if (challenge != null && challenge.groupId == viewModel?.groupID) {
if (challenge != null && challenge.groupId == viewModel.groupID) {
groupChallenges.add(challenge)
}
}
@ -200,12 +204,11 @@ class GuildDetailFragment : BaseFragment<FragmentGuildDetailBinding>() {
}
companion object {
fun newInstance(viewModel: GroupViewModel?): GuildDetailFragment {
fun newInstance(): GuildDetailFragment {
val args = Bundle()
val fragment = GuildDetailFragment()
fragment.arguments = args
fragment.viewModel = viewModel
return fragment
}
}

View file

@ -90,7 +90,6 @@ class GuildFragment : BaseMainFragment<FragmentViewpagerBinding>() {
}
if (childFragment is GuildDetailFragment) {
guildInformationFragment = childFragment
childFragment.viewModel = viewModel
}
}
}
@ -144,12 +143,11 @@ class GuildFragment : BaseMainFragment<FragmentViewpagerBinding>() {
when (position) {
0 -> {
guildInformationFragment = GuildDetailFragment.newInstance(viewModel)
guildInformationFragment = GuildDetailFragment.newInstance()
fragment = guildInformationFragment
}
1 -> {
chatFragment = ChatFragment()
chatFragment?.viewModel = viewModel
fragment = chatFragment
}
else -> fragment = Fragment()

View file

@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.Column
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.ChallengeRepository
@ -54,8 +56,9 @@ import javax.inject.Inject
@AndroidEntryPoint
class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
var viewModel: PartyViewModel? = null
val viewModel: PartyViewModel by viewModels(
ownerProducer = { parentFragment as Fragment }
)
override var binding: FragmentPartyDetailBinding? = null
@ -99,7 +102,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
binding?.invitationsView?.getLeader = null
binding?.invitationsView?.acceptCall = {
viewModel?.joinGroup(it) {
viewModel.joinGroup(it) {
lifecycleScope.launch(ExceptionHandler.coroutine()) {
val user = userRepository.retrieveUser(false)
parentFragmentManager.popBackStack()
@ -114,39 +117,38 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
binding?.invitationsView?.rejectCall = {
lifecycleScope.launchCatching {
socialRepository.rejectGroupInvite(it)
userRepository.retrieveUser(false, true)
userRepository.retrieveUser(withTasks = false, forced = true)
}
}
binding?.invitesWrapper?.setContent {
HabiticaTheme {
val invitedMembers = viewModel?.pendingInvites
Column {
for (invitedMember in (invitedMembers ?: emptyList())) {
val state = viewModel?.pendingInviteStates?.getOrDefault(
for (invitedMember in viewModel.pendingInvites) {
val state = viewModel.pendingInviteStates.getOrDefault(
invitedMember.id,
LoadingButtonState.CONTENT
) ?: LoadingButtonState.CONTENT
)
PartySeekingListItem(
user = invitedMember,
inviteState = state,
isInvited = state != LoadingButtonState.SUCCESS,
isInvited = true,
showHeader = true,
showExtendedInfo = false,
onInvite = {
viewModel?.rescindInvite(invitedMember)
viewModel.rescindInvite(invitedMember)
})
}
}
}
}
viewModel?.getGroupData()?.observe(viewLifecycleOwner) { updateParty(it) }
viewModel?.user?.observe(viewLifecycleOwner) { updateUser(it) }
viewModel?.getMembersData()?.observe(viewLifecycleOwner) { updateMembersList(it) }
viewModel.getGroupData().observe(viewLifecycleOwner) { updateParty(it) }
viewModel.user.observe(viewLifecycleOwner) { updateUser(it) }
viewModel.getMembersData().observe(viewLifecycleOwner) { updateMembersList(it) }
}
private fun refreshParty() {
viewModel?.retrieveGroup {
viewModel.retrieveGroup {
binding?.refreshLayout?.isRefreshing = false
}
}
@ -180,7 +182,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
binding?.questProgressView?.visibility = View.GONE
}
binding?.findNewMember?.isVisible = viewModel?.isLeader == true
binding?.findNewMember?.isVisible = viewModel.isLeader == true
}
private fun updateUser(user: User?) {
@ -205,7 +207,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
}
}
binding?.questProgressView?.configure(user, viewModel?.isUserOnQuest)
binding?.questProgressView?.configure(user, viewModel.isUserOnQuest)
if ((user.invitations?.parties?.count() ?: 0) > 0) {
binding?.partyInvitationWrapper?.visibility = View.VISIBLE
@ -243,7 +245,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
}
private fun showParticipantButtons(): Boolean {
return viewModel?.showParticipantButtons() ?: false
return viewModel.showParticipantButtons()
}
private fun updateQuestContent(questContent: QuestContent) {
@ -277,15 +279,15 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
)
)
}
if (viewModel?.isQuestActive == true) {
if (viewModel.isQuestActive) {
binding?.questProgressView?.visibility = View.VISIBLE
binding?.questProgressView?.setData(
questContent,
viewModel?.getGroupData()?.value?.quest?.progress
viewModel.getGroupData().value?.quest?.progress
)
val questParticipants = viewModel?.getGroupData()?.value?.quest?.members
if (questParticipants?.find { it.key == viewModel?.userViewModel?.userID } != null) {
val questParticipants = viewModel.getGroupData().value?.quest?.members
if (questParticipants?.find { it.key == viewModel.userViewModel.userID } != null) {
binding?.questParticipationView?.text =
context?.getString(R.string.number_participants, questParticipants.size)
} else {
@ -304,7 +306,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
}
} else {
binding?.questProgressView?.visibility = View.GONE
val members = viewModel?.getGroupData()?.value?.quest?.members
val members = viewModel.getGroupData().value?.quest?.members
val responded = members?.filter { it.isParticipating != null }
binding?.questParticipationView?.text =
context?.getString(R.string.number_responded, responded?.size, members?.size)
@ -312,7 +314,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
}
private fun updateMembersList(members: List<Member>?) {
val leaderID = viewModel?.leaderID
val leaderID = viewModel.leaderID
members?.forEachIndexed { index, member ->
val memberView = (
if ((binding?.membersWrapper?.childCount ?: 0) > index) {
@ -324,18 +326,18 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
}
) ?: return@forEachIndexed
val viewHolder = GroupMemberViewHolder(memberView)
viewHolder.bind(member, leaderID ?: "", viewModel?.user?.value?.id)
viewHolder.bind(member, leaderID ?: "", viewModel.user.value?.id)
viewHolder.onClickEvent = {
FullProfileActivity.open(member.id)
}
viewHolder.sendMessageEvent = {
member.id.let { showSendMessageToUserDialog(it, member.displayName) }
showSendMessageToUserDialog(member.id, member.displayName)
}
viewHolder.transferOwnershipEvent = {
member.id.let { showTransferOwnerShipDialog(it, member.displayName) }
showTransferOwnerShipDialog(member.id, member.displayName)
}
viewHolder.removeMemberEvent = {
member.id.let { showRemoveMemberDialog(it, member.displayName) }
showRemoveMemberDialog(member.id, member.displayName)
}
}
}
@ -376,7 +378,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
val dialog = context?.let { HabiticaAlertDialog(it) }
dialog?.addButton(R.string.transfer, true) { _, _ ->
lifecycleScope.launch(ExceptionHandler.coroutine()) {
socialRepository.transferGroupOwnership(viewModel?.groupID ?: "", userID)
socialRepository.transferGroupOwnership(viewModel.groupID ?: "", userID)
(activity as? MainActivity)?.snackbarContainer?.let { it1 ->
HabiticaSnackbar.showSnackbar(
it1,
@ -402,7 +404,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
val dialog = context?.let { HabiticaAlertDialog(it) }
dialog?.addButton(R.string.remove, true) { _, _ ->
lifecycleScope.launch(ExceptionHandler.coroutine()) {
socialRepository.removeMemberFromGroup(viewModel?.groupID ?: "", userID)
socialRepository.removeMemberFromGroup(viewModel.groupID ?: "", userID)
(activity as? MainActivity)?.snackbarContainer?.let { it1 ->
HabiticaSnackbar.showSnackbar(
it1,
@ -434,7 +436,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
it?.challenges?.forEach { membership ->
val challenge =
challengeRepository.getChallenge(membership.challengeID).firstOrNull()
if (challenge != null && challenge.groupId == viewModel?.groupID) {
if (challenge != null && challenge.groupId == viewModel.groupID) {
groupChallenges.add(challenge)
}
}
@ -454,7 +456,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
alert.setTitle(R.string.party_challenges)
alert.setMessage(R.string.leave_party_challenges_confirmation)
alert.addButton(R.string.keep_challenges, true) { _, _ ->
viewModel?.leaveGroup(groupChallenges, true) {
viewModel.leaveGroup(groupChallenges, true) {
parentFragmentManager.popBackStack()
MainNavigationController.navigate(R.id.noPartyFragment)
}
@ -464,7 +466,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
false,
isDestructive = true
) { _, _ ->
viewModel?.leaveGroup(groupChallenges, false) {
viewModel.leaveGroup(groupChallenges, false) {
parentFragmentManager.popBackStack()
MainNavigationController.navigate(R.id.noPartyFragment)
}
@ -480,7 +482,7 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
isPrimary = true,
isDestructive = true
) { _, _ ->
viewModel?.leaveGroup(groupChallenges, false) {
viewModel.leaveGroup(groupChallenges, false) {
parentFragmentManager.popBackStack()
MainNavigationController.navigate(R.id.noPartyFragment)
}
@ -494,12 +496,12 @@ class PartyDetailFragment : BaseFragment<FragmentPartyDetailBinding>() {
private fun onQuestAccept() {
HapticFeedbackManager.tap(requireView())
viewModel?.acceptQuest()
viewModel.acceptQuest()
}
private fun onQuestReject() {
HapticFeedbackManager.tap(requireView())
viewModel?.rejectQuest()
viewModel.rejectQuest()
}
private fun questDetailButtonClicked() {

View file

@ -13,7 +13,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.FragmentViewpagerBinding
@ -30,7 +29,6 @@ import dagger.hilt.android.AndroidEntryPoint
class PartyFragment : BaseMainFragment<FragmentViewpagerBinding>() {
private var detailFragment: PartyDetailFragment? = null
private var chatFragment: ChatFragment? = null
private var viewPagerAdapter: FragmentStateAdapter? = null
internal val viewModel: PartyViewModel by viewModels()
@ -189,13 +187,10 @@ class PartyFragment : BaseMainFragment<FragmentViewpagerBinding>() {
return when (position) {
0 -> {
detailFragment = PartyDetailFragment()
detailFragment?.viewModel = viewModel
detailFragment
}
1 -> {
chatFragment = ChatFragment()
chatFragment?.viewModel = viewModel
chatFragment
ChatFragment()
}
else -> Fragment()
} ?: Fragment()
@ -207,24 +202,6 @@ class PartyFragment : BaseMainFragment<FragmentViewpagerBinding>() {
}
binding?.viewPager?.adapter = viewPagerAdapter
binding?.viewPager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
if (position == 1) {
chatFragment?.setNavigatedToFragment()
}
}
override fun onPageSelected(position: Int) {
if (position == 1) {
chatFragment?.setNavigatedToFragment()
}
}
})
tabLayout?.let {
binding?.viewPager?.let { it1 ->
TabLayoutMediator(it, it1) { tab, position ->

View file

@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -51,6 +52,7 @@ import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.data.SocialRepository
import com.habitrpg.android.habitica.data.UserRepository
import com.habitrpg.android.habitica.databinding.FragmentComposeBinding
import com.habitrpg.android.habitica.helpers.MainNavigationController
import com.habitrpg.android.habitica.models.invitations.InviteResponse
import com.habitrpg.android.habitica.ui.fragments.BaseFragment
import com.habitrpg.android.habitica.ui.theme.HabiticaTheme
@ -61,8 +63,11 @@ import com.habitrpg.common.habitica.extensions.isValidEmail
import com.habitrpg.common.habitica.helpers.launchCatching
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import java.util.UUID
import javax.inject.Inject
import kotlin.time.DurationUnit
import kotlin.time.toDuration
fun uUIDFromStringOrNull(name: String): UUID? {
return try {
@ -120,7 +125,9 @@ class PartyInviteFragment : BaseFragment<FragmentComposeBinding>() {
val view = super.onCreateView(inflater, container, savedInstanceState)
binding?.composeView?.setContent {
HabiticaTheme {
PartyInviteView(viewModel)
PartyInviteView(viewModel) {
MainNavigationController.navigateBack()
}
}
}
return view
@ -130,7 +137,8 @@ class PartyInviteFragment : BaseFragment<FragmentComposeBinding>() {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PartyInviteView(
viewModel: PartyInviteViewModel
viewModel: PartyInviteViewModel,
dismiss: () -> Unit
) {
var inviteButtonState: LoadingButtonState by remember { mutableStateOf(LoadingButtonState.CONTENT) }
val scope = rememberCoroutineScope()
@ -152,11 +160,13 @@ fun PartyInviteView(
stringResource(R.string.invite_with_username_email),
color = HabiticaTheme.colors.textPrimary,
fontSize = 16.sp,
fontWeight = FontWeight.Medium
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(bottom = 4.dp)
)
Text(
stringResource(R.string.habiticans_send_invite),
color = HabiticaTheme.colors.textSecondary
color = HabiticaTheme.colors.textSecondary,
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Normal),
)
}
}
@ -209,6 +219,7 @@ fun PartyInviteView(
viewModel.invites[index] = value
},
singleLine = true,
textStyle = TextStyle(fontSize = 16.sp),
placeholder = { Text(stringResource(R.string.username_or_email)) },
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
@ -241,6 +252,8 @@ fun PartyInviteView(
inviteButtonState = LoadingButtonState.SUCCESS
viewModel.invites.clear()
viewModel.invites.add("")
delay(2.toDuration(DurationUnit.SECONDS))
dismiss()
} else {
inviteButtonState = LoadingButtonState.FAILED
}

View file

@ -30,6 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@ -57,6 +58,7 @@ import com.habitrpg.android.habitica.ui.viewmodels.BaseViewModel
import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel
import com.habitrpg.android.habitica.ui.views.LoadingButton
import com.habitrpg.android.habitica.ui.views.LoadingButtonState
import com.habitrpg.android.habitica.ui.views.LoadingButtonType
import com.habitrpg.android.habitica.ui.views.progress.HabiticaCircularProgressView
import com.habitrpg.android.habitica.ui.views.progress.HabiticaPullRefreshIndicator
import com.habitrpg.android.habitica.ui.views.social.PartySeekingListItem
@ -138,14 +140,16 @@ fun InviteButton(
modifier : Modifier = Modifier,
isAlreadyInvited: Boolean = false,
) {
LoadingButton(state = state, onClick = onClick, colors = ButtonDefaults.buttonColors(
LoadingButton(state = state, onClick = onClick,
type = if (isAlreadyInvited) LoadingButtonType.DESTRUCTIVE else LoadingButtonType.NORMAL,
colors = ButtonDefaults.buttonColors(
backgroundColor = if (isAlreadyInvited) HabiticaTheme.colors.errorBackground else HabiticaTheme.colors.tintedUiSub,
contentColor = Color.White,
), modifier = modifier, successContent = {
if (isAlreadyInvited) {
Text(stringResource(R.string.invited))
} else {
Text(stringResource(R.string.rescinded))
} else {
Text(stringResource(R.string.invited))
}
}) {
if (isAlreadyInvited) {
@ -177,18 +181,20 @@ fun PartySeekingView(
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
.fillMaxWidth()
.padding(top = 22.dp, bottom = 14.dp)
.padding(top = 36.dp, bottom = 14.dp)
) {
Text(
stringResource(R.string.find_more_members),
color = HabiticaTheme.colors.textPrimary,
fontSize = 16.sp,
fontWeight = FontWeight.Medium
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(bottom = 4.dp)
)
if (pageData.itemCount == 0 && pageData.loadState.refresh is LoadState.NotLoading && pageData.loadState.append is LoadState.NotLoading) {
Text(
stringResource(R.string.habiticans_looking_party_empty),
textAlign = TextAlign.Center,
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Normal),
color = HabiticaTheme.colors.textSecondary, modifier = Modifier
.width(250.dp)
.align(alignment = Alignment.CenterHorizontally)
@ -226,13 +232,13 @@ fun PartySeekingView(
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) {
viewModel.inviteStates[member.id] = LoadingButtonState.SUCCESS
delay(4.toDuration(DurationUnit.SECONDS))
if (viewModel.successfulInvites.contains(member.id)) {
viewModel.successfulInvites.remove(member.id)
} else {
viewModel.successfulInvites.add(member.id)
}
viewModel.inviteStates[member.id] = LoadingButtonState.SUCCESS
delay(4.toDuration(DurationUnit.SECONDS))
viewModel.inviteStates[member.id] = LoadingButtonState.CONTENT
} else {
viewModel.inviteStates[member.id] = LoadingButtonState.FAILED

View file

@ -136,6 +136,7 @@ object HabiticaTheme {
windowBackground = Color(context.getThemeColor(R.attr.colorWindowBackground)),
contentBackground = Color(context.getThemeColor(R.attr.colorContentBackground)),
contentBackgroundOffset = Color(context.getThemeColor(R.attr.colorContentBackgroundOffset)),
offsetBackground = Color(context.getThemeColor(R.attr.colorOffsetBackground)),
textPrimary = Color(context.getThemeColor(R.attr.textColorPrimary)),
textSecondary = Color(context.getThemeColor(R.attr.textColorSecondary)),
textTertiary = Color(ContextCompat.getColor(context, R.color.text_ternary)),
@ -157,6 +158,7 @@ class HabiticaColors(
val windowBackground: Color,
val contentBackground: Color,
val contentBackgroundOffset: Color,
val offsetBackground: Color,
val textPrimary: Color,
val textSecondary: Color,
val textTertiary: Color,

View file

@ -7,7 +7,7 @@ import com.habitrpg.android.habitica.helpers.GroupPlanInfoProvider
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
import com.habitrpg.android.habitica.models.tasks.Task
import com.habitrpg.shared.habitica.models.responses.TaskDirection
import java.text.DateFormat
import java.text.SimpleDateFormat
class TodoViewHolder(
itemView: View,
@ -18,7 +18,7 @@ class TodoViewHolder(
assignedTextProvider: GroupPlanInfoProvider?
) : ChecklistedViewHolder(itemView, scoreTaskFunc, scoreChecklistItemFunc, openTaskFunc, brokenTaskFunc, assignedTextProvider) {
private val dateFormatter: DateFormat = android.text.format.DateFormat.getDateFormat(context)
private val dateFormatter = SimpleDateFormat.getDateInstance()
override fun bind(
data: Task,

View file

@ -63,38 +63,52 @@ enum class LoadingButtonState {
SUCCESS
}
enum class LoadingButtonType {
NORMAL,
DESTRUCTIVE
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun LoadingButton(
state : LoadingButtonState,
onClick : () -> Unit,
modifier : Modifier = Modifier,
type : LoadingButtonType = LoadingButtonType.NORMAL,
elevation : ButtonElevation? = ButtonDefaults.elevation(0.dp),
shape : Shape = MaterialTheme.shapes.medium,
border : BorderStroke? = null,
colors : ButtonColors = ButtonDefaults.buttonColors(
backgroundColor = HabiticaTheme.colors.tintedUiSub,
contentColor = Color.White
),
colors : ButtonColors? = null,
contentPadding : PaddingValues = ButtonDefaults.ContentPadding,
successContent : (@Composable RowScope.() -> Unit)? = null,
failedContent : (@Composable RowScope.() -> Unit)? = null,
content : @Composable RowScope.() -> Unit
) {
val colorStyle = if (type == LoadingButtonType.DESTRUCTIVE) ButtonDefaults.buttonColors(
backgroundColor = HabiticaTheme.colors.errorBackground,
contentColor = Color.White,
disabledBackgroundColor = HabiticaTheme.colors.offsetBackground,
disabledContentColor = HabiticaTheme.colors.textQuad
) else ButtonDefaults.buttonColors(
backgroundColor = HabiticaTheme.colors.tintedUiSub,
contentColor = Color.White,
disabledBackgroundColor = HabiticaTheme.colors.offsetBackground,
disabledContentColor = HabiticaTheme.colors.textQuad
)
val colorSpec = tween<Color>(350)
val backgroundColor = animateColorAsState(
targetValue = when (state) {
LoadingButtonState.FAILED -> HabiticaTheme.colors.errorBackground
LoadingButtonState.SUCCESS -> Color.Transparent
else -> colors.backgroundColor(enabled = state != LoadingButtonState.DISABLED).value
else -> colorStyle.backgroundColor(enabled = state != LoadingButtonState.DISABLED).value
},
animationSpec = colorSpec
)
val contentColor = animateColorAsState(
targetValue = when (state) {
LoadingButtonState.FAILED -> Color.White
LoadingButtonState.SUCCESS -> HabiticaTheme.colors.successColor
else -> colors.contentColor(enabled = state != LoadingButtonState.DISABLED).value
LoadingButtonState.SUCCESS -> if (type == LoadingButtonType.DESTRUCTIVE) HabiticaTheme.colors.errorColor else HabiticaTheme.colors.successColor
else -> colorStyle.contentColor(enabled = state != LoadingButtonState.DISABLED).value
},
animationSpec = colorSpec
)
@ -124,7 +138,7 @@ fun LoadingButton(
shape = shape,
border = if (state == LoadingButtonState.SUCCESS) BorderStroke(
borderWidth.value,
HabiticaTheme.colors.successBackground
if (type == LoadingButtonType.DESTRUCTIVE) HabiticaTheme.colors.errorColor else HabiticaTheme.colors.successColor
) else border,
colors = buttonColors,
contentPadding = PaddingValues(0.dp)

View file

@ -1,9 +1,9 @@
New in 4.1.9:
-Various subscription status optimizations
-Party invites will show the senders username and profile
-You can view, complete, assign, and add tasks to your Group Plans shared task board
-Tap your name on a task screen to switch to different task boards
-More intuitive system notification settings
-New Avatar Customization interface
-Audio will be controlled by media volume now
-Task details are now tinted based on task health
New in 4.2:
It's easier than ever to party up with our newest feature: Look for Party and Find Members!
- Solo players can let Party leaders know they want an invite by going to Menu > Party and tapping 'Look for Party'
- Party leaders can see players looking for Party by tapping 'Find Members' on the Party screen and send invites from the list
- Armoire shows in Market when you have all gear
- Change class from Market when viewing gear from another class
- Various bug fixes and improvements

View file

@ -1,2 +1,2 @@
NAME=4.2
CODE=5991
CODE=6001