mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-07-15 10:41:58 +00:00
Begin implementing username autocomplete
This commit is contained in:
parent
d6d43e7fda
commit
67c6d5675e
14 changed files with 240 additions and 15 deletions
17
Habitica/res/layout/autocomplete_username.xml
Normal file
17
Habitica/res/layout/autocomplete_username.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_medium"
|
||||
android:layout_marginBottom="@dimen/spacing_medium"
|
||||
android:layout_marginStart="@dimen/spacing_large"
|
||||
android:layout_marginEnd="@dimen/spacing_large">
|
||||
<com.habitrpg.android.habitica.ui.views.social.UsernameLabel
|
||||
android:id="@+id/display_name_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<TextView
|
||||
android:id="@+id/username_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
app:hintEnabled="false"
|
||||
android:layout_marginBottom="@dimen/spacing_medium"
|
||||
android:layout_marginTop="@dimen/spacing_medium">
|
||||
<net.pherth.android.emoji_library.EmojiEditText
|
||||
<MultiAutoCompleteTextView
|
||||
android:id="@+id/chatEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.habitrpg.android.habitica.models.shops.Shop;
|
|||
import com.habitrpg.android.habitica.models.shops.ShopItem;
|
||||
import com.habitrpg.android.habitica.models.social.Challenge;
|
||||
import com.habitrpg.android.habitica.models.social.ChatMessage;
|
||||
import com.habitrpg.android.habitica.models.social.FindUsernameResult;
|
||||
import com.habitrpg.android.habitica.models.social.Group;
|
||||
import com.habitrpg.android.habitica.models.tasks.Task;
|
||||
import com.habitrpg.android.habitica.models.tasks.TaskList;
|
||||
|
|
@ -287,6 +288,9 @@ public interface ApiService {
|
|||
@POST("members/send-private-message")
|
||||
Flowable<HabitResponse<PostChatMessageResult>> postPrivateMessage(@Body Map<String, String> messageDetails);
|
||||
|
||||
@GET("members/find/{username}")
|
||||
Flowable<HabitResponse<List<FindUsernameResult>>> findUsernames(@Path("username") String username, @Query("context") String context, @Query("id") String id);
|
||||
|
||||
@GET("shops/{identifier}")
|
||||
Flowable<HabitResponse<Shop>> retrieveShopInventory(@Path("identifier") String identifier);
|
||||
@GET("shops/market-gear")
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.habitrpg.android.habitica.models.members.Member;
|
|||
import com.habitrpg.android.habitica.models.responses.FeedResponse;
|
||||
import com.habitrpg.android.habitica.models.social.Challenge;
|
||||
import com.habitrpg.android.habitica.models.social.ChatMessage;
|
||||
import com.habitrpg.android.habitica.models.social.FindUsernameResult;
|
||||
import com.habitrpg.android.habitica.models.social.Group;
|
||||
import com.habitrpg.android.habitica.models.tasks.Task;
|
||||
import com.habitrpg.android.habitica.models.tasks.TaskList;
|
||||
|
|
@ -41,6 +42,7 @@ import com.habitrpg.android.habitica.utils.EggListDeserializer;
|
|||
import com.habitrpg.android.habitica.utils.EquipmentListDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.FAQArticleListDeserilializer;
|
||||
import com.habitrpg.android.habitica.utils.FeedResponseDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.FindUsernameResultDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.FoodListDeserializer;
|
||||
import com.habitrpg.android.habitica.utils.GroupSerialization;
|
||||
import com.habitrpg.android.habitica.utils.HatchingPotionListDeserializer;
|
||||
|
|
@ -130,6 +132,7 @@ public class GSonFactoryCreator {
|
|||
.registerTypeAdapter(Quest.class, new QuestDeserializer())
|
||||
.registerTypeAdapter(Member.class, new MemberSerialization())
|
||||
.registerTypeAdapter(WorldState.class, new WorldStateSerialization())
|
||||
.registerTypeAdapter(FindUsernameResult.class, new FindUsernameResultDeserializer())
|
||||
//.registerTypeAdapter(Task.class, new TaskDeserializer())
|
||||
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
||||
.create();
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ import com.habitrpg.android.habitica.ui.fragments.social.party.PartyMemberListFr
|
|||
import com.habitrpg.android.habitica.ui.fragments.tasks.TaskRecyclerViewFragment;
|
||||
import com.habitrpg.android.habitica.ui.fragments.tasks.TasksFragment;
|
||||
import com.habitrpg.android.habitica.ui.viewmodels.GroupViewModel;
|
||||
import com.habitrpg.android.habitica.ui.views.social.ChatBarView;
|
||||
import com.habitrpg.android.habitica.ui.views.stats.BulkAllocateStatsDialog;
|
||||
import com.habitrpg.android.habitica.ui.views.shops.PurchaseDialog;
|
||||
import com.habitrpg.android.habitica.ui.views.tasks.TaskFilterDialog;
|
||||
|
|
@ -308,4 +309,6 @@ public interface AppComponent {
|
|||
void inject(@NotNull GiftIAPActivity giftIAPActivity);
|
||||
|
||||
void inject(@NotNull AboutFragment aboutFragment);
|
||||
|
||||
void inject(@NotNull ChatBarView chatBarView);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.habitrpg.android.habitica.models.shops.Shop
|
|||
import com.habitrpg.android.habitica.models.shops.ShopItem
|
||||
import com.habitrpg.android.habitica.models.social.Challenge
|
||||
import com.habitrpg.android.habitica.models.social.ChatMessage
|
||||
import com.habitrpg.android.habitica.models.social.FindUsernameResult
|
||||
import com.habitrpg.android.habitica.models.social.Group
|
||||
import com.habitrpg.android.habitica.models.tasks.Task
|
||||
import com.habitrpg.android.habitica.models.tasks.TaskList
|
||||
|
|
@ -248,4 +249,5 @@ interface ApiClient {
|
|||
fun retrieveMarketGear(): Flowable<Shop>
|
||||
fun verifyUsername(username: String): Flowable<VerifyUsernameResponse>
|
||||
fun updateServerUrl(newAddress: String?)
|
||||
fun findUsernames(username: String, context: String?, id: String?): Flowable<List<FindUsernameResult>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ import com.habitrpg.android.habitica.models.AchievementResult
|
|||
import com.habitrpg.android.habitica.models.inventory.Quest
|
||||
import com.habitrpg.android.habitica.models.members.Member
|
||||
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
|
||||
import com.habitrpg.android.habitica.models.social.Challenge
|
||||
import com.habitrpg.android.habitica.models.social.ChatMessage
|
||||
import com.habitrpg.android.habitica.models.social.Group
|
||||
import com.habitrpg.android.habitica.models.social.GroupMembership
|
||||
import com.habitrpg.android.habitica.models.social.*
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Single
|
||||
|
|
@ -60,6 +57,8 @@ interface SocialRepository : BaseRepository {
|
|||
fun getMember(userId: String?): Flowable<Member>
|
||||
fun getMemberWithUsername(username: String?): Flowable<Member>
|
||||
|
||||
fun findUsernames(username: String, context: String? = null, id: String? = null): Flowable<List<FindUsernameResult>>
|
||||
|
||||
fun markPrivateMessagesRead(user: User?): Flowable<Void>
|
||||
|
||||
fun acceptQuest(user: User?, partyId: String): Flowable<Void>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.habitrpg.android.habitica.models.shops.Shop
|
|||
import com.habitrpg.android.habitica.models.shops.ShopItem
|
||||
import com.habitrpg.android.habitica.models.social.Challenge
|
||||
import com.habitrpg.android.habitica.models.social.ChatMessage
|
||||
import com.habitrpg.android.habitica.models.social.FindUsernameResult
|
||||
import com.habitrpg.android.habitica.models.social.Group
|
||||
import com.habitrpg.android.habitica.models.tasks.Task
|
||||
import com.habitrpg.android.habitica.models.tasks.TaskList
|
||||
|
|
@ -583,6 +584,10 @@ class ApiClientImpl//private OnHabitsAPIResult mResultListener;
|
|||
return apiService.getMemberAchievements(memberId).compose(configureApiCallObserver())
|
||||
}
|
||||
|
||||
override fun findUsernames(username: String, context: String?, id: String?): Flowable<List<FindUsernameResult>> {
|
||||
return apiService.findUsernames(username, context, id).compose(configureApiCallObserver())
|
||||
}
|
||||
|
||||
override fun postPrivateMessage(messageDetails: Map<String, String>): Flowable<PostChatMessageResult> {
|
||||
return apiService.postPrivateMessage(messageDetails).compose(configureApiCallObserver())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,7 @@ import com.habitrpg.android.habitica.models.AchievementResult
|
|||
import com.habitrpg.android.habitica.models.inventory.Quest
|
||||
import com.habitrpg.android.habitica.models.members.Member
|
||||
import com.habitrpg.android.habitica.models.responses.PostChatMessageResult
|
||||
import com.habitrpg.android.habitica.models.social.Challenge
|
||||
import com.habitrpg.android.habitica.models.social.ChatMessage
|
||||
import com.habitrpg.android.habitica.models.social.Group
|
||||
import com.habitrpg.android.habitica.models.social.GroupMembership
|
||||
import com.habitrpg.android.habitica.models.social.*
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Single
|
||||
|
|
@ -21,6 +18,7 @@ import io.reactivex.functions.Consumer
|
|||
import io.realm.RealmResults
|
||||
|
||||
class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: ApiClient, userID: String) : BaseRepositoryImpl<SocialLocalRepository>(localRepository, apiClient, userID), SocialRepository {
|
||||
|
||||
override fun getGroupMembership(id: String): Flowable<GroupMembership> {
|
||||
return localRepository.getGroupMembership(userID, id)
|
||||
}
|
||||
|
|
@ -208,6 +206,10 @@ class SocialRepositoryImpl(localRepository: SocialLocalRepository, apiClient: Ap
|
|||
} else apiClient.getMemberWithUsername(username)
|
||||
}
|
||||
|
||||
override fun findUsernames(username: String, context: String?, id: String?): Flowable<List<FindUsernameResult>> {
|
||||
return apiClient.findUsernames(username, context, id)
|
||||
}
|
||||
|
||||
override fun markPrivateMessagesRead(user: User?): Flowable<Void> {
|
||||
return apiClient.markPrivateMessagesRead()
|
||||
.doOnNext { _ ->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package com.habitrpg.android.habitica.models.social
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.habitrpg.android.habitica.models.user.Authentication
|
||||
import com.habitrpg.android.habitica.models.user.ContributorInfo
|
||||
import io.realm.annotations.PrimaryKey
|
||||
|
||||
class FindUsernameResult {
|
||||
|
||||
@SerializedName("_id")
|
||||
var id: String? = null
|
||||
|
||||
var contributor: ContributorInfo? = null
|
||||
var authentication: Authentication? = null
|
||||
|
||||
val username: String?
|
||||
get() = authentication?.localAuthentication?.username
|
||||
val formattedUsername: String?
|
||||
get() = if (username != null) "@$username" else null
|
||||
|
||||
override fun toString(): String {
|
||||
return authentication?.localAuthentication?.username ?: ""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.habitrpg.android.habitica.ui.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.widget.TextView
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.data.SocialRepository
|
||||
import com.habitrpg.android.habitica.extensions.inflate
|
||||
import com.habitrpg.android.habitica.models.social.FindUsernameResult
|
||||
import com.habitrpg.android.habitica.ui.views.social.UsernameLabel
|
||||
|
||||
class AutocompleteAdapter(val context: Context, val socialRepository: SocialRepository) : BaseAdapter(), Filterable {
|
||||
private var results: List<FindUsernameResult> = arrayListOf()
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
val filterResults = FilterResults()
|
||||
if (constraint != null && constraint.isNotEmpty()) {
|
||||
results = socialRepository.findUsernames(constraint.toString()).blockingFirst(arrayListOf())
|
||||
filterResults.values = results
|
||||
filterResults.count = results.size
|
||||
}
|
||||
return filterResults
|
||||
}
|
||||
|
||||
override fun publishResults(contraint: CharSequence?, results: FilterResults?) {
|
||||
if (results != null && results.count > 0) {
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
notifyDataSetInvalidated()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
val view = parent?.inflate(R.layout.autocomplete_username)
|
||||
val result = getItem(position)
|
||||
val displaynameView = view?.findViewById<UsernameLabel>(R.id.display_name_view)
|
||||
displaynameView?.username = result.username
|
||||
displaynameView?.tier = result.contributor?.level ?: 0
|
||||
view?.findViewById<TextView>(R.id.username_view)?.text = result.formattedUsername
|
||||
return view ?: View(context)
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): FindUsernameResult {
|
||||
return results[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return getItem(position).id.hashCode().toLong()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return results.size
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.habitrpg.android.habitica.ui.helpers
|
||||
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.TextUtils
|
||||
import android.widget.MultiAutoCompleteTextView
|
||||
|
||||
|
||||
class ChatInputTokenizer : MultiAutoCompleteTextView.Tokenizer {
|
||||
|
||||
override fun findTokenStart(text: CharSequence, cursor: Int): Int {
|
||||
var i = cursor
|
||||
|
||||
while (i > 0 && text[i - 1] != '@') {
|
||||
i--
|
||||
}
|
||||
|
||||
//Check if token really started with @, else we don't have a valid token
|
||||
return if (i < 1 || text[i - 1] != '@') {
|
||||
cursor
|
||||
} else i
|
||||
|
||||
}
|
||||
|
||||
override fun findTokenEnd(text: CharSequence, cursor: Int): Int {
|
||||
var i = cursor
|
||||
val len = text.length
|
||||
|
||||
while (i < len) {
|
||||
if (text[i] == ' ') {
|
||||
return i
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return len
|
||||
}
|
||||
|
||||
override fun terminateToken(text: CharSequence): CharSequence {
|
||||
var i = text.length
|
||||
|
||||
while (i > 0 && text[i - 1] == ' ') {
|
||||
i--
|
||||
}
|
||||
|
||||
return if (i > 0 && text[i - 1] == ' ') {
|
||||
text
|
||||
} else {
|
||||
if (text is Spanned) {
|
||||
val sp = SpannableString(text.toString() + " ")
|
||||
TextUtils.copySpansFrom(text, 0, text.length,
|
||||
Any::class.java, sp, 0)
|
||||
sp
|
||||
} else {
|
||||
text.toString() + " "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ package com.habitrpg.android.habitica.ui.views.social
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
|
|
@ -12,18 +10,26 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.habitrpg.android.habitica.HabiticaBaseApplication
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.data.SocialRepository
|
||||
import com.habitrpg.android.habitica.ui.helpers.AutocompleteAdapter
|
||||
import com.habitrpg.android.habitica.ui.helpers.ChatInputTokenizer
|
||||
import com.habitrpg.android.habitica.ui.helpers.NavbarUtils
|
||||
import com.habitrpg.android.habitica.ui.helpers.bindView
|
||||
import net.pherth.android.emoji_library.EmojiEditText
|
||||
import net.pherth.android.emoji_library.EmojiPopup
|
||||
import net.pherth.android.emoji_library.EmojiTextView
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class ChatBarView : FrameLayout {
|
||||
|
||||
@Inject
|
||||
lateinit var socialRepository: SocialRepository
|
||||
|
||||
private val sendButton: ImageButton by bindView(R.id.sendButton)
|
||||
private val chatEditText: EmojiEditText by bindView(R.id.chatEditText)
|
||||
private val chatEditText: MultiAutoCompleteTextView by bindView(R.id.chatEditText)
|
||||
private val textIndicator: TextView by bindView(R.id.text_indicator)
|
||||
private val indicatorSpacing: View by bindView(R.id.indicator_spacing)
|
||||
private val emojiButton: ImageButton by bindView(R.id.emojiButton)
|
||||
|
|
@ -50,6 +56,8 @@ class ChatBarView : FrameLayout {
|
|||
inflater?.inflate(R.layout.tavern_chat_new_entry_item, this)
|
||||
this.setBackgroundResource(R.color.white)
|
||||
|
||||
HabiticaBaseApplication.component?.inject(this)
|
||||
|
||||
chatEditText.addTextChangedListener(object: TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {}
|
||||
|
||||
|
|
@ -98,6 +106,12 @@ class ChatBarView : FrameLayout {
|
|||
chatEditText.dispatchKeyEvent(event)
|
||||
}
|
||||
resizeForDrawingUnderNavbar()
|
||||
|
||||
val tagArray = AutocompleteAdapter(context, socialRepository)
|
||||
chatEditText.setAdapter(tagArray)
|
||||
chatEditText.threshold = 2
|
||||
|
||||
chatEditText.setTokenizer(ChatInputTokenizer())
|
||||
}
|
||||
|
||||
private fun updateTextIndicator(text: String) {
|
||||
|
|
@ -166,7 +180,7 @@ class ChatBarView : FrameLayout {
|
|||
}
|
||||
}
|
||||
|
||||
private inner class EmojiClickListener internal constructor(internal var view: EmojiEditText) : View.OnClickListener {
|
||||
private inner class EmojiClickListener internal constructor(internal var view: MultiAutoCompleteTextView) : View.OnClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (!popup.isShowing) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package com.habitrpg.android.habitica.utils
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.habitrpg.android.habitica.models.social.FindUsernameResult
|
||||
import com.habitrpg.android.habitica.models.user.Authentication
|
||||
import com.habitrpg.android.habitica.models.user.ContributorInfo
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class FindUsernameResultDeserializer : JsonDeserializer<FindUsernameResult> {
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): FindUsernameResult {
|
||||
val obj = json.asJsonObject
|
||||
|
||||
val result = FindUsernameResult()
|
||||
|
||||
if (obj.has("contributor")) {
|
||||
result.contributor = context.deserialize<ContributorInfo>(obj.get("contributor"), ContributorInfo::class.java)
|
||||
}
|
||||
if (obj.has("auth")) {
|
||||
result.authentication = context.deserialize<Authentication>(obj.get("auth"), Authentication::class.java)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue