Commit 436e7d60 authored by Leonardo Aramaki's avatar Leonardo Aramaki

Fixed conflicts when rebasing

parent a639df30
......@@ -5,6 +5,7 @@ import android.support.v4.graphics.drawable.DrawableCompat
import android.widget.EditText
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.common.model.UserStatus
object DrawableHelper {
......@@ -78,7 +79,7 @@ object DrawableHelper {
* @param drawables The array of Drawable.
* @see compoundDrawable
*/
fun compoundDrawables(textView: Array<EditText>, drawables: Array<Drawable>) {
fun compoundDrawables(textView: Array<TextView>, drawables: Array<Drawable>) {
if (textView.size != drawables.size) {
return
} else {
......@@ -104,15 +105,15 @@ object DrawableHelper {
* @param context The context.
* @return The user status drawable.
*/
fun getUserStatusDrawable(userStatus: String, context: Context): Drawable {
fun getUserStatusDrawable(userStatus: UserStatus, context: Context): Drawable {
val userStatusDrawable = getDrawableFromId(R.drawable.ic_user_status_black, context).mutate()
wrapDrawable(userStatusDrawable)
when (userStatus) {
// TODO: create a enum or check if it will come from the SDK
"online" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOnline)
"busy" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusBusy)
"away" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusAway)
"offline" -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
is UserStatus.Online -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOnline)
is UserStatus.Busy -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusBusy)
is UserStatus.Away -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusAway)
is UserStatus.Offline -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
else -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
}
return userStatusDrawable
}
......
package chat.rocket.android.app
data class User(val id: String,
val name: String,
val username: String,
val status: String,
val avatarUri: String)
\ No newline at end of file
......@@ -58,7 +58,7 @@ class SignupFragment : Fragment(), SignupView {
setUpNewUserAgreementListener()
button_sign_up.setOnClickListener {
presenter.signup(text_name.textContent, text_username.textContent, text_password.textContent, text_email.textContent)
presenter.signup(text_username.textContent, text_username.textContent, text_password.textContent, text_email.textContent)
}
}
......@@ -69,8 +69,8 @@ class SignupFragment : Fragment(), SignupView {
override fun alertBlankName() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_name)
text_name.requestFocus()
AnimationHelper.shakeView(text_username)
text_username.requestFocus()
}
override fun alertBlankUsername() {
......@@ -122,7 +122,7 @@ class SignupFragment : Fragment(), SignupView {
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, appContext, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_password, text_email), drawables)
DrawableHelper.compoundDrawables(arrayOf(text_username, text_username, text_password, text_email), drawables)
}
private fun setUpNewUserAgreementListener() {
......@@ -149,7 +149,7 @@ class SignupFragment : Fragment(), SignupView {
private fun enableUserInput(value: Boolean) {
button_sign_up.isEnabled = value
text_name.isEnabled = value
text_username.isEnabled = value
text_username.isEnabled = value
text_password.isEnabled = value
text_email.isEnabled = value
......
package chat.rocket.android.chatroom.adapter
import android.support.annotation.IntDef
const val PEOPLE = 0L
const val ROOMS = 1L
@Retention(AnnotationRetention.SOURCE)
@IntDef(value = [PEOPLE, ROOMS])
annotation class AutoCompleteType
package chat.rocket.android.chatroom.adapter
import DrawableHelper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter.PeopleSuggestionViewHolder
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import chat.rocket.common.model.UserStatus
import com.facebook.drawee.view.SimpleDraweeView
class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>("@") {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeopleSuggestionViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_member_item, parent,
false)
return PeopleSuggestionViewHolder(view)
}
class PeopleSuggestionViewHolder(view: View) : BaseSuggestionViewHolder(view) {
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as PeopleViewModel
with(itemView) {
val username = itemView.findViewById<TextView>(R.id.text_username)
val name = itemView.findViewById<TextView>(R.id.text_name)
val avatar = itemView.findViewById<SimpleDraweeView>(R.id.image_avatar)
val statusView = itemView.findViewById<ImageView>(R.id.image_status)
username.text = item.username
name.text = item.name
if (item.imageUri.isEmpty()) {
avatar.setVisible(false)
} else {
avatar.setVisible(true)
avatar.setImageURI(item.imageUri)
}
val status = item.status ?: UserStatus.Offline()
val statusDrawable = DrawableHelper.getUserStatusDrawable(status, itemView.context)
statusView.setImageDrawable(statusDrawable)
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter.RoomSuggestionsViewHolder
import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
class RoomSuggestionsAdapter : SuggestionsAdapter<RoomSuggestionsViewHolder>("#") {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomSuggestionsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_room_item, parent,
false)
return RoomSuggestionsViewHolder(view)
}
class RoomSuggestionsViewHolder(view: View) : BaseSuggestionViewHolder(view) {
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as ChatRoomViewModel
with(itemView) {
val fullname = itemView.findViewById<TextView>(R.id.text_fullname)
val name = itemView.findViewById<TextView>(R.id.text_name)
name.text = item.name
fullname.text = item.fullName
setOnClickListener {
itemClickListener?.onClick(item)
}
}
}
}
}
\ No newline at end of file
......@@ -2,15 +2,23 @@ package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.AutoCompleteType
import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.ROOMS
import chat.rocket.android.chatroom.domain.UriInteractor
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.UserStatus
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.State
......@@ -31,9 +39,13 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val strategy: CancelStrategy,
getSettingsInteractor: GetSettingsInteractor,
private val serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val permissions: GetPermissionsInteractor,
private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository,
private val usersRepository: UsersRepository,
private val roomsRepository: RoomRepository,
private val localRepository: LocalRepository,
factory: ConnectionManagerFactory,
private val mapper: ViewModelMapper) {
private val currentServer = serverInteractor.get()!!
......@@ -60,7 +72,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
// TODO: For now we are marking the room as read if we can get the messages (I mean, no exception occurs)
// but should mark only when the user see the first unread message.
markRoomAsRead(chatRoomId)
val messagesViewModels = mapper.map(messages)
view.showMessages(messagesViewModels)
......@@ -97,7 +109,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
view.showGenericErrorMessage()
}
} finally {
view.enableSendMessageButton()
......@@ -340,7 +352,109 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
}
fun loadActiveMembers(chatRoomId: String, chatRoomType: String, offset: Long = 0, filterSelfOut: Boolean = false) {
launchUI(strategy) {
try {
val members = client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 50).result
usersRepository.saveAll(members)
val self = localRepository.get(LocalRepository.USERNAME_KEY)
// Take at most the 100 most recent messages distinguished by user. Can return less.
val recentMessages = messagesRepository.getRecentMessages(chatRoomId, 100)
.filterNot { filterSelfOut && it.sender?.username == self }
val activeUsers = mutableListOf<PeopleViewModel>()
recentMessages.forEach {
val sender = it.sender!!
val username = sender.username ?: ""
val name = sender.name ?: ""
val avatarUrl = UrlHelper.getAvatarUrl(currentServer, username)
val found = members.firstOrNull { member -> member.username == username }
val status = if (found != null) found.status else UserStatus.Offline()
val searchList = mutableListOf(username, name)
activeUsers.add(PeopleViewModel(avatarUrl, username, username, name, status,
true, searchList))
}
// Filter out from members list the active users.
val others = members.filterNot { member ->
activeUsers.firstOrNull {
it.username == member.username
} != null
}
// Add found room members who're not active enough and add them in without pinning.
activeUsers.addAll(others.map {
val username = it.username ?: ""
val name = it.name ?: ""
val avatarUrl = UrlHelper.getAvatarUrl(currentServer, username)
val searchList = mutableListOf(username, name)
PeopleViewModel(avatarUrl, username, username, name, it.status, true, searchList)
})
view.populateMembers(activeUsers)
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
fun spotlight(query: String, @AutoCompleteType type: Long, filterSelfOut: Boolean = false) {
launchUI(strategy) {
try {
val (users, rooms) = client.spotlight(query)
when (type) {
PEOPLE -> {
if (users.isNotEmpty()) {
usersRepository.saveAll(users)
}
val self = localRepository.get(LocalRepository.USERNAME_KEY)
view.populateMembers(users.map {
val username = it.username ?: ""
val name = it.name ?: ""
val searchList = mutableListOf(username, name)
it.emails?.forEach { email -> searchList.add(email.address) }
PeopleViewModel(UrlHelper.getAvatarUrl(currentServer, username),
username, username, name, it.status, false, searchList)
}.filterNot { filterSelfOut && self != null && self == it.text })
}
ROOMS -> {
if (rooms.isNotEmpty()) {
roomsRepository.saveAll(rooms)
}
view.populateRooms(rooms.map {
val fullName = it.fullName ?: ""
val name = it.name ?: ""
val searchList = mutableListOf(fullName, name)
ChatRoomViewModel(name, fullName, name, searchList)
})
}
}
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
<<<<<<< 61f4135f2fbe9097b2d53bf96aa9e55d47ec62d8
fun toMembersList(chatRoomId: String, chatRoomType: String) = navigator.toMembersList(chatRoomId, chatRoomType)
=======
fun loadChatRooms() {
launchUI(strategy) {
try {
val chatRooms = getChatRoomsInteractor.get(currentServer).map { chatRoom ->
val name = chatRoom.name
val fullName = chatRoom.fullName ?: ""
ChatRoomViewModel(
text = name,
name = name,
fullName = fullName,
searchList = listOf(name, fullName)
)
}
view.populateRooms(chatRooms)
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
>>>>>>> Load rooms from subscriptions
private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) {
......
......@@ -2,6 +2,8 @@ package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State
......@@ -100,4 +102,6 @@ interface ChatRoomView : LoadingView, MessageView {
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
fun showConnectionState(state: State)
fun populateMembers(members: List<PeopleViewModel>)
fun populateRooms(chatRooms: List<ChatRoomViewModel>)
}
\ No newline at end of file
......@@ -16,9 +16,14 @@ import android.support.v7.widget.RecyclerView
import android.view.*
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser
......@@ -62,9 +67,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var isChatRoomReadOnly: Boolean = false
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private lateinit var actionSnackbar: ActionSnackbar
private var citation: String? = null
private var editingMessageId: String? = null
......@@ -102,10 +108,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
setupToolbar(chatRoomName)
presenter.loadMessages(chatRoomId, chatRoomType)
presenter.loadChatRooms()
setupRecyclerView()
setupFab()
setupMessageComposer()
setupSuggestionsView()
setupActionSnackbar()
}
......@@ -174,6 +181,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
recycler_view.scrollToPosition(0)
}
presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true)
}
}
......@@ -245,6 +253,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun populateMembers(members: List<PeopleViewModel>) {
suggestions_view.addItems("@", members)
}
override fun populateRooms(chatRooms: List<ChatRoomViewModel>) {
suggestions_view.addItems("#", chatRooms)
}
override fun copyToClipboard(message: String) {
activity?.apply {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
......@@ -410,14 +426,30 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
}
}
private fun setupSuggestionsView() {
suggestions_view.anchor(text_message)
.bindTokenAdapter(PeopleSuggestionsAdapter())
.bindTokenAdapter(RoomSuggestionsAdapter())
.addSuggestionProviderAction("@") { query ->
if (query.isNotEmpty()) {
presenter.spotlight(query, PEOPLE, true)
}
}
.addSuggestionProviderAction("#") { query ->
if (query.isNotEmpty()) {
presenter.loadChatRooms()
}
}
}
private fun openEmojiKeyboardPopup() {
if (!emojiKeyboardPopup.isShowing()) {
if (!emojiKeyboardPopup.isShowing) {
// If keyboard is visible, simply show the popup
if (emojiKeyboardPopup.isKeyboardOpen) {
emojiKeyboardPopup.showAtBottom()
} else {
// Open the text keyboard first and immediately after that show the emoji popup
text_message.setFocusableInTouchMode(true)
text_message.isFocusableInTouchMode = true
text_message.requestFocus()
emojiKeyboardPopup.showAtBottomPending()
KeyboardHelper.showSoftKeyboard(text_message)
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
class ChatRoomViewModel(text: String,
val fullName: String,
val name: String,
searchList: List<String>) : SuggestionModel(text, searchList, false) {
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.common.model.UserStatus
class PeopleViewModel(val imageUri: String,
text: String,
val username: String,
val name: String,
val status: UserStatus?,
pinned: Boolean = false,
searchList: List<String>) : SuggestionModel(text, searchList, pinned) {
override fun toString(): String {
return "PeopleViewModel(imageUri='$imageUri', username='$username', name='$name', status=$status, pinned=$pinned)"
}
}
\ No newline at end of file
......@@ -185,7 +185,13 @@ class AppModule {
@Provides
@Singleton
fun provideChatRoomsRepository(): ChatRoomsRepository {
fun provideRoomRepository(): RoomRepository {
return MemoryRoomRepository()
}
@Provides
@Singleton
fun provideChatRoomRepository(): ChatRoomsRepository {
return MemoryChatRoomsRepository()
}
......@@ -207,6 +213,12 @@ class AppModule {
return MemoryMessagesRepository()
}
@Provides
@Singleton
fun provideUserRepository(): UsersRepository {
return MemoryUsersRepository()
}
@Provides
@Singleton
fun provideConfiguration(context: Application, client: OkHttpClient): SpannableConfiguration {
......
......@@ -129,20 +129,20 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private fun listenToChanges() {
Observables.combineLatest(text_name.asObservable(),
text_username.asObservable(),
text_email.asObservable(),
text_avatar_url.asObservable()) { text_name, text_username, text_email, text_avatar_url ->
return@combineLatest (text_name.toString() != currentName ||
text_username.toString() !=currentUsername ||
text_username.asObservable(),
text_email.asObservable(),
text_avatar_url.asObservable()) { text_name, text_username, text_email, text_avatar_url ->
return@combineLatest (text_name.toString() != currentName ||
text_username.toString() != currentUsername ||
text_email.toString() != currentEmail ||
(text_avatar_url.toString() != "" && text_avatar_url.toString()!= currentAvatar))
}.subscribe({ isValid->
if (isValid) {
startActionMode()
} else {
finishActionMode()
}
})
(text_avatar_url.toString() != "" && text_avatar_url.toString() != currentAvatar))
}.subscribe({ isValid ->
if (isValid) {
startActionMode()
} else {
finishActionMode()
}
})
}
private fun startActionMode() {
......@@ -154,7 +154,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private fun finishActionMode() = actionMode?.finish()
private fun enableUserInput(value: Boolean) {
text_name.isEnabled = value
text_username.isEnabled = value
text_username.isEnabled = value
text_email.isEnabled = value
text_avatar_url.isEnabled = value
......
......@@ -6,6 +6,14 @@ import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
/**
* Get all ChatRoom objects.
*
* @param url The server url.
*
* @return All the ChatRoom objects.
*/
fun get(url: String) = repository.get(url)
/**
......
......@@ -8,6 +8,7 @@ interface MessagesRepository {
* Get message by its message id.
*
* @param id The id of the message to get.
*
* @return The Message object given by the id or null if message wasn't found.
*/
fun getById(id: String): Message?
......@@ -20,8 +21,19 @@ interface MessagesRepository {
*/
fun getByRoomId(rid: String): List<Message>
/**
* Get most recent messages up to count different users.
*
* @param rid The id of the room the messages are.
* @param count The count last messages to get.
*
* @return List of last count messages.
*/
fun getRecentMessages(rid: String, count: Long): List<Message>
/**
* Get all messages. Use carefully!
*
* @return All messages or an empty list.
*/
fun getAll(): List<Message>
......
package chat.rocket.android.server.domain
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.Room
interface RoomRepository {
/**
* Get all rooms. Use carefully!
*
* @return All rooms or an empty list.
*/
fun getAll(): List<Room>
fun get(query: Query.() -> Unit): List<Room>
/**
* Save a single room object.
*
* @param room The room object to save.
*/
fun save(room: Room)
/**
* Save a list of rooms.
*
* @param roomList The list of rooms to save.
*/
fun saveAll(roomList: List<Room>)
/**
* Removes all rooms.
*/
fun clear()
data class Query(
var id: String? = null,
var name: String? = null,
var fullName: String? = null,
var type: RoomType? = null,
var readonly: Boolean? = null
)
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.common.model.Email
import chat.rocket.common.model.User
import chat.rocket.common.model.UserStatus
interface UsersRepository {
/**
* Get all users. Use carefully!
*
* @return All users or an empty list.
*/
fun getAll(): List<User>
fun get(query: Query.() -> Unit): List<User>
/**
* Save a single user object.
*
* @param user The user object to save.
*/
fun save(user: User)
/**
* Save a list of users.
*
* @param users The list of users to save.
*/
fun saveAll(userList: List<User>)
/**
* Removes all users.
*/
fun clear()
data class Query(
var id: String? = null,
var name: String? = null,
var username: String? = null,
var emails: List<Email>? = null,
var utfOffset: Float? = null,
var status: UserStatus? = null,
var limit: Long = 0L
)
}
\ No newline at end of file
......@@ -15,6 +15,11 @@ class MemoryMessagesRepository : MessagesRepository {
return messages.filter { it.value.roomId == rid }.values.toList()
}
override fun getRecentMessages(rid: String, count: Long): List<Message> {
return getByRoomId(rid).sortedByDescending { it.timestamp }
.distinctBy { it.sender }.take(count.toInt())
}
override fun getAll(): List<Message> = messages.values.toList()
override fun save(message: Message) {
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.RoomRepository.Query
import chat.rocket.core.model.Room
import java.util.concurrent.CopyOnWriteArrayList
class MemoryRoomRepository : RoomRepository {
private val rooms = CopyOnWriteArrayList<Room>()
override fun getAll() = rooms.toList()
override fun get(query: Query.() -> Unit): List<Room> {
val q = Query().apply(query)
return rooms.filter {
with(q) {
if (name != null && it.name?.contains(name!!.toRegex()) == true) return@filter false
if (fullName != null && it.fullName?.contains(fullName!!.toRegex()) == true) return@filter false
if (id != null && id == it.id) return@filter false
if (readonly != null && readonly == it.readonly) return@filter false
if (type != null && type == it.type) return@filter false
return@filter true
}
}
}
override fun save(room: Room) {
rooms.addIfAbsent(room)
}
override fun saveAll(roomList: List<Room>) {
rooms.addAllAbsent(roomList)
}
override fun clear() {
rooms.clear()
}
}
\ No newline at end of file
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.domain.UsersRepository.Query
import chat.rocket.common.model.User
import java.util.concurrent.CopyOnWriteArrayList
class MemoryUsersRepository : UsersRepository {
private val users = CopyOnWriteArrayList<User>()
override fun getAll(): List<User> {
return users.toList()
}
override fun get(query: Query.() -> Unit): List<User> {
val q = Query().apply(query)
return users.filter {
with(q) {
if (name != null && it.name?.contains(name!!.toRegex()) == true) return@filter false
if (username != null && it.username?.contains(username!!.toRegex()) == true) return@filter false
if (id != null && id == it.id) return@filter false
if (status != null && status == it.status) return@filter false
return@filter true
}
}
}
override fun save(user: User) {
users.addIfAbsent(user)
}
override fun saveAll(userList: List<User>) {
users.addAllAbsent(userList)
}
override fun clear() {
this.users.clear()
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.model
abstract class SuggestionModel(val text: String, // This is the text key for searches, must be unique.
val searchList: List<String> = emptyList(), // Where to search for matches.
val pinned: Boolean = false /* If pinned item will have priority to show */) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SuggestionModel) return false
if (text != other.text) return false
return true
}
override fun hashCode(): Int {
return text.hashCode()
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.repository
interface LocalSuggestionProvider {
fun find(prefix: String)
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
interface CompletionStrategy {
fun getItem(prefix: String, position: Int): SuggestionModel
fun autocompleteItems(prefix: String): List<SuggestionModel>
fun addAll(list: List<SuggestionModel>)
fun size(): Int
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.regex
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import java.util.concurrent.CopyOnWriteArrayList
internal class StringMatchingCompletionStrategy : CompletionStrategy {
private val list = CopyOnWriteArrayList<SuggestionModel>()
override fun autocompleteItems(prefix: String): List<SuggestionModel> {
return list.filter {
it.searchList.forEach { word ->
if (word.contains(prefix, ignoreCase = true)) {
return@filter true
}
}
false
}.sortedByDescending { it.pinned }.take(5)
}
override fun addAll(list: List<SuggestionModel>) {
// this.list.removeAll { !it.pinned }
this.list.addAllAbsent(list)
}
override fun getItem(prefix: String, position: Int): SuggestionModel {
return list[position]
}
override fun size(): Int {
return list.size
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.strategy.trie.data.Trie
class TrieCompletionStrategy : CompletionStrategy {
private val items = mutableListOf<SuggestionModel>()
private val trie = Trie()
override fun getItem(prefix: String, position: Int): SuggestionModel {
val item: SuggestionModel
if (prefix.isEmpty()) {
item = items[position]
} else {
item = autocompleteItems(prefix)[position]
}
return item
}
override fun autocompleteItems(prefix: String) = trie.autocompleteItems(prefix)
override fun addAll(list: List<SuggestionModel>) {
items.addAll(list)
list.forEach {
trie.insert(it)
}
}
override fun size() = items.size
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie.data
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
internal class Trie {
private val root = TrieNode(' ')
private var count = 0
fun insert(item: SuggestionModel) {
val sanitizedWord = item.text.trim().toLowerCase()
// Word exists, bail out.
if (search(sanitizedWord)) return
var current = root
sanitizedWord.forEach { ch ->
val child = current.getChild(ch)
if (child == null) {
val node = TrieNode(ch, current)
current.children[ch] = node
current = node
count++
} else {
current = child
}
}
// Set last node as leaf.
if (current != root) {
current.isLeaf = true
current.item = item
}
}
fun search(word: String): Boolean {
val sanitizedWord = word.trim().toLowerCase()
var current = root
sanitizedWord.forEach { ch ->
val child = current.getChild(ch)
if (child == null) {
return false
}
current = child
}
if (current.isLeaf) {
return true
}
return false
}
fun autocomplete(prefix: String): List<String> {
val sanitizedPrefix = prefix.trim().toLowerCase()
var lastNode: TrieNode? = root
sanitizedPrefix.forEach { ch ->
lastNode = lastNode?.getChild(ch)
if (lastNode == null) return emptyList()
}
return lastNode!!.getWords()
}
fun autocompleteItems(prefix: String): List<SuggestionModel> {
val sanitizedPrefix = prefix.trim().toLowerCase()
var lastNode: TrieNode? = root
sanitizedPrefix.forEach { ch ->
lastNode = lastNode?.getChild(ch)
if (lastNode == null) return emptyList()
}
return lastNode!!.getItems()
}
fun getCount() = count
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.strategy.trie.data
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
internal class TrieNode(internal var data: Char,
internal var parent: TrieNode? = null,
internal var isLeaf: Boolean = false,
internal var item: SuggestionModel? = null) {
val children = hashMapOf<Char, TrieNode>()
fun getChild(c: Char): TrieNode? {
children.forEach {
if (it.key == c) return it.value
}
return null
}
fun getWords(): List<String> {
val list = arrayListOf<String>()
if (isLeaf) {
list.add(toString())
}
children.forEach { node ->
node.value.let {
list.addAll(it.getWords())
}
}
return list
}
class X : SuggestionModel("")
fun getItems(): List<SuggestionModel> {
val list = arrayListOf<SuggestionModel>()
if (isLeaf) {
list.add(item!!)
}
children.forEach { node ->
node.value.let {
list.addAll(it.getItems())
}
}
return list
}
override fun toString(): String = if (parent == null) "" else "${parent.toString()}$data"
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.support.v7.widget.RecyclerView
import android.view.View
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
abstract class BaseSuggestionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
abstract fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?)
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.WindowManager
import chat.rocket.android.R
internal class PopupRecyclerView : RecyclerView {
private var displayWidth: Int = 0
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
val wm = context!!.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = wm.defaultDisplay
val size = DisplayMetrics()
display.getMetrics(size)
val screenWidth = size.widthPixels
displayWidth = screenWidth
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
val hSpec = MeasureSpec.makeMeasureSpec(resources.getDimensionPixelSize(
R.dimen.popup_max_height), MeasureSpec.AT_MOST)
val wSpec = MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.EXACTLY)
super.onMeasure(wSpec, hSpec)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l + 40, t, r - 40, b)
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.support.v7.widget.RecyclerView
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.strategy.regex.StringMatchingCompletionStrategy
import java.lang.reflect.Type
import kotlin.properties.Delegates
abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(val token: String) : RecyclerView.Adapter<VH>() {
private val strategy: CompletionStrategy = StringMatchingCompletionStrategy()
private var itemType: Type? = null
private var itemClickListener: ItemClickListener? = null
private var providerExternal: ((query: String) -> Unit)? = null
private var prefix: String by Delegates.observable("", { _, _, _ ->
strategy.autocompleteItems(prefix)
notifyItemRangeChanged(0, 5)
})
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
return getItem(position).text.hashCode().toLong()
}
override fun onBindViewHolder(holder: VH, position: Int) {
holder.bind(getItem(position), itemClickListener)
}
override fun getItemCount() = strategy.autocompleteItems(prefix).size
private fun getItem(position: Int): SuggestionModel {
return strategy.autocompleteItems(prefix)[position]
}
fun autocomplete(prefix: String) {
this.prefix = prefix.toLowerCase().trim()
}
fun addItems(list: List<SuggestionModel>) {
strategy.addAll(list)
// Since we've just added new items we should check for possible new completion suggestions.
strategy.autocompleteItems(prefix)
notifyItemRangeChanged(0, 5)
}
fun setOnClickListener(clickListener: ItemClickListener) {
this.itemClickListener = clickListener
}
fun hasItemClickListener() = itemClickListener != null
fun prefix() = prefix
fun cancel() {
strategy.addAll(emptyList())
strategy.autocompleteItems(prefix)
notifyDataSetChanged()
}
interface ItemClickListener {
fun onClick(item: SuggestionModel)
}
}
\ No newline at end of file
package chat.rocket.android.widget.autocompletion.ui
import android.content.Context
import android.support.transition.Slide
import android.support.transition.TransitionManager
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.widget.EditText
import android.widget.FrameLayout
import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicInteger
/**
* This is a special index that means we're not at an autocompleting state.
*/
private const val NO_STATE_INDEX = 0
class SuggestionsView : FrameLayout, TextWatcher {
private val recyclerView: RecyclerView
// Maps tokens to their respecive adapters.
private val adaptersByToken = hashMapOf<String, SuggestionsAdapter<out BaseSuggestionViewHolder>>()
private val externalProvidersByToken = hashMapOf<String, ((query: String) -> Unit)>()
private val localProvidersByToken = hashMapOf<String, HashMap<String, List<SuggestionModel>>>()
private var editor: WeakReference<EditText>? = null
private var completionStartIndex = AtomicInteger(NO_STATE_INDEX)
companion object {
private val SLIDE_TRANSITION = Slide(Gravity.BOTTOM).setDuration(200)
}
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr, 0) {
recyclerView = RecyclerView(context)
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL,
false)
recyclerView.itemAnimator = DefaultItemAnimator()
recyclerView.layoutManager = layoutManager
recyclerView.visibility = View.GONE
addView(recyclerView)
}
override fun afterTextChanged(s: Editable) {
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// If we have a deletion.
if (after == 0) {
val deleted = s.subSequence(start, start + count).toString()
if (adaptersByToken.containsKey(deleted) && completionStartIndex.get() > NO_STATE_INDEX) {
// We have removed the '@', '#' or any other action token so halt completion.
cancelSuggestions(true)
}
}
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
// If we don't have any adapter bound to any token bail out.
if (adaptersByToken.isEmpty()) return
val new = s.subSequence(start, start + count).toString()
if (adaptersByToken.containsKey(new)) {
swapAdapter(getAdapterForToken(new)!!)
completionStartIndex.compareAndSet(NO_STATE_INDEX, start + 1)
editor?.let {
// Disable keyboard suggestions when autocompleting.
val editText = it.get()
if (editText != null) {
editText.inputType = editText.inputType or InputType.TYPE_TEXT_VARIATION_FILTER
expand()
}
}
}
if (new.startsWith(" ")) {
// just halts the completion execution
cancelSuggestions(false)
return
}
val prefixEndIndex = editor?.get()?.selectionStart ?: NO_STATE_INDEX
if (prefixEndIndex == NO_STATE_INDEX || prefixEndIndex < completionStartIndex.get()) return
val prefix = s.subSequence(completionStartIndex.get(), editor?.get()?.selectionStart ?: completionStartIndex.get()).toString()
recyclerView.adapter?.let {
it as SuggestionsAdapter
// we need to look up only after the '@'
it.autocomplete(prefix)
val cacheMap = localProvidersByToken[it.token]
if (cacheMap != null && cacheMap[prefix] != null) {
it.addItems(cacheMap[prefix]!!)
} else {
// fetch more suggestions from an external source if any
externalProvidersByToken[it.token]?.invoke(prefix)
}
}
}
private fun swapAdapter(adapter: SuggestionsAdapter<*>): SuggestionsView {
recyclerView.adapter = adapter
// Don't override if user set an item click listener already/
if (!adapter.hasItemClickListener()) {
setOnItemClickListener(adapter) {
// set default item click behavior
}
}
return this
}
fun getAdapterForToken(token: String): SuggestionsAdapter<*>? = adaptersByToken.get(token)
fun anchor(editText: EditText): SuggestionsView {
editText.removeTextChangedListener(this)
editText.addTextChangedListener(this)
editor = WeakReference(editText)
return this
}
fun bindTokenAdapter(adapter: SuggestionsAdapter<*>): SuggestionsView {
adaptersByToken.getOrPut(adapter.token, { adapter })
return this
}
fun addItems(token: String, list: List<SuggestionModel>): SuggestionsView {
if (list.isNotEmpty()) {
val adapter = adapter(token)
localProvidersByToken.getOrPut(token, { hashMapOf() })
.put(adapter.prefix(), list)
if (completionStartIndex.get() > NO_STATE_INDEX && adapter.itemCount == 0) expand()
adapter.addItems(list)
}
return this
}
fun setOnItemClickListener(tokenAdapter: SuggestionsAdapter<*>,
clickListener: (item: SuggestionModel) -> Unit): SuggestionsView {
tokenAdapter.setOnClickListener(object : SuggestionsAdapter.ItemClickListener {
override fun onClick(item: SuggestionModel) {
insertSuggestionOnEditor(item)
clickListener.invoke(item)
cancelSuggestions(true)
collapse()
}
})
return this
}
fun addSuggestionProviderAction(token: String, provider: (query: String) -> Unit): SuggestionsView {
externalProvidersByToken.getOrPut(token, { provider })
return this
}
private fun adapter(token: String): SuggestionsAdapter<*> {
return adaptersByToken[token] ?: throw IllegalStateException("no adapter binds to token \"$token\"")
}
private fun cancelSuggestions(haltCompletion: Boolean) {
// Reset completion start index only if we've deleted the token that triggered completion or
// we finished the completion process.
if (haltCompletion) {
completionStartIndex.set(NO_STATE_INDEX)
}
collapse()
// Re-enable keyboard suggestions.
val editText = editor?.get()
if (editText != null) {
editText.inputType = editText.inputType and InputType.TYPE_TEXT_VARIATION_FILTER.inv()
}
}
private fun insertSuggestionOnEditor(item: SuggestionModel) {
editor?.get()?.let {
val suggestionText = item.text
it.text.replace(completionStartIndex.get(), it.selectionStart, "$suggestionText ")
}
}
private fun collapse() {
TransitionManager.beginDelayedTransition(this, SLIDE_TRANSITION)
recyclerView.visibility = View.GONE
}
private fun expand() {
TransitionManager.beginDelayedTransition(this, SLIDE_TRANSITION)
recyclerView.visibility = View.VISIBLE
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="10dp"
android:viewportWidth="10.0"
android:viewportHeight="10.0">
<path
android:pathData="M5,5m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
</vector>
\ No newline at end of file
......@@ -16,7 +16,7 @@
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/text_name"
android:id="@+id/text_username"
style="@style/Authentication.EditText"
android:layout_marginTop="32dp"
android:drawableStart="@drawable/ic_person_black_24dp"
......@@ -37,7 +37,7 @@
android:inputType="text"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_name" />
app:layout_constraintTop_toBottomOf="@+id/text_username" />
<EditText
android:id="@+id/text_password"
......
......@@ -28,8 +28,16 @@
layout="@layout/message_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<chat.rocket.android.widget.autocompletion.ui.SuggestionsView
android:id="@+id/suggestions_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer"
android:background="@color/whitesmoke" />
<include
android:id="@+id/layout_message_composer"
layout="@layout/message_composer"
......@@ -58,15 +66,15 @@
android:id="@+id/connection_status_text"
android:layout_width="match_parent"
android:layout_height="32dp"
android:alpha="0"
android:background="@color/colorPrimary"
android:elevation="4dp"
android:textColor="@color/white"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/white"
android:visibility="gone"
android:alpha="0"
tools:alpha="1"
tools:visibility="visible"
tools:text="connected"/>
tools:text="connected"
tools:visibility="visible" />
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:background="@color/whitesmoke">
<FrameLayout
android:id="@+id/image_avatar_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_avatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="4dp"
app:roundedCornerRadius="3dp"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/image_status"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_gravity="bottom|end"
android:background="@drawable/user_status_white"
android:padding="2dp" />
</FrameLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/image_avatar_container"
android:layout_toRightOf="@id/image_avatar_container"
android:background="@color/whitesmoke">
<TextView
android:id="@+id/text_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/text_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/text_username"
android:layout_toRightOf="@+id/text_username"
android:maxLines="1"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:textColor="@color/gray_material"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
</RelativeLayout>
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:background="@color/whitesmoke">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toEndOf="@id/image_avatar_container"
android:layout_toRightOf="@id/image_avatar_container"
android:background="@color/whitesmoke">
<TextView
android:id="@+id/text_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:maxLines="1"
android:textColor="@color/black"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/text_fullname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/text_name"
android:layout_toRightOf="@+id/text_name"
android:maxLines="1"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:textColor="@color/gray_material"
android:textSize="16sp"
tools:text="@tools:sample/full_names" />
</RelativeLayout>
</RelativeLayout>
\ No newline at end of file
......@@ -36,4 +36,7 @@
<color name="colorEmojiIcon">#FF767676</color>
<color name="default_popup_background_color">#ffffffff</color>
<color name="default_popup_item_color">#ff000000</color>
</resources>
......@@ -25,4 +25,7 @@
<dimen name="padding_mention">4dp</dimen>
<dimen name="radius_mention">6dp</dimen>
<!-- Autocomplete Popup -->
<dimen name="popup_max_height">150dp</dimen>
</resources>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment