diff --git a/Habitica/res/layout/autocomplete_username.xml b/Habitica/res/layout/autocomplete_username.xml
new file mode 100644
index 000000000..a413fd56c
--- /dev/null
+++ b/Habitica/res/layout/autocomplete_username.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Habitica/res/layout/tavern_chat_new_entry_item.xml b/Habitica/res/layout/tavern_chat_new_entry_item.xml
index 8f6c2c4dc..dd6d74035 100644
--- a/Habitica/res/layout/tavern_chat_new_entry_item.xml
+++ b/Habitica/res/layout/tavern_chat_new_entry_item.xml
@@ -34,7 +34,7 @@
app:hintEnabled="false"
android:layout_marginBottom="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_medium">
- > postPrivateMessage(@Body Map messageDetails);
+ @GET("members/find/{username}")
+ Flowable>> findUsernames(@Path("username") String username, @Query("context") String context, @Query("id") String id);
+
@GET("shops/{identifier}")
Flowable> retrieveShopInventory(@Path("identifier") String identifier);
@GET("shops/market-gear")
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/api/GSonFactoryCreator.java b/Habitica/src/main/java/com/habitrpg/android/habitica/api/GSonFactoryCreator.java
index 74701d2a4..2d01c1960 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/api/GSonFactoryCreator.java
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/api/GSonFactoryCreator.java
@@ -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();
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/components/AppComponent.java b/Habitica/src/main/java/com/habitrpg/android/habitica/components/AppComponent.java
index 42989e025..645ee3e22 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/components/AppComponent.java
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/components/AppComponent.java
@@ -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);
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt
index 1622fe670..40bc07022 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/ApiClient.kt
@@ -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
fun verifyUsername(username: String): Flowable
fun updateServerUrl(newAddress: String?)
+ fun findUsernames(username: String, context: String?, id: String?): Flowable>
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt
index 238339cd5..3f8eb5d2f 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/SocialRepository.kt
@@ -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
fun getMemberWithUsername(username: String?): Flowable
+ fun findUsernames(username: String, context: String? = null, id: String? = null): Flowable>
+
fun markPrivateMessagesRead(user: User?): Flowable
fun acceptQuest(user: User?, partyId: String): Flowable
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt
index f1edd32ab..66dfe714d 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/ApiClientImpl.kt
@@ -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> {
+ return apiService.findUsernames(username, context, id).compose(configureApiCallObserver())
+ }
+
override fun postPrivateMessage(messageDetails: Map): Flowable {
return apiService.postPrivateMessage(messageDetails).compose(configureApiCallObserver())
}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt
index 9bbf7d36b..38182c1f3 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/data/implementation/SocialRepositoryImpl.kt
@@ -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(localRepository, apiClient, userID), SocialRepository {
+
override fun getGroupMembership(id: String): Flowable {
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> {
+ return apiClient.findUsernames(username, context, id)
+ }
+
override fun markPrivateMessagesRead(user: User?): Flowable {
return apiClient.markPrivateMessagesRead()
.doOnNext { _ ->
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/FindUsernameResult.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/FindUsernameResult.kt
new file mode 100644
index 000000000..c54f66c18
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/social/FindUsernameResult.kt
@@ -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 ?: ""
+ }
+}
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/AutocompleteAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/AutocompleteAdapter.kt
new file mode 100644
index 000000000..15f116083
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/AutocompleteAdapter.kt
@@ -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 = 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(R.id.display_name_view)
+ displaynameView?.username = result.username
+ displaynameView?.tier = result.contributor?.level ?: 0
+ view?.findViewById(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
+ }
+
+
+}
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/ChatInputTokenizer.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/ChatInputTokenizer.kt
new file mode 100644
index 000000000..2fc519a2e
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/helpers/ChatInputTokenizer.kt
@@ -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() + " "
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/ChatBarView.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/ChatBarView.kt
index 0b011ed09..2020e7cbd 100644
--- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/ChatBarView.kt
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/social/ChatBarView.kt
@@ -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) {
diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/utils/FindUsernameResultDeserializer.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/utils/FindUsernameResultDeserializer.kt
new file mode 100644
index 000000000..bef613db6
--- /dev/null
+++ b/Habitica/src/main/java/com/habitrpg/android/habitica/utils/FindUsernameResultDeserializer.kt
@@ -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 {
+ @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(obj.get("contributor"), ContributorInfo::class.java)
+ }
+ if (obj.has("auth")) {
+ result.authentication = context.deserialize(obj.get("auth"), Authentication::class.java)
+ }
+
+ return result
+ }
+}
\ No newline at end of file