Unverified Commit 212b870f authored by divyanshu bhargava's avatar divyanshu bhargava Committed by GitHub

Merge pull request #40 from RocketChat/develop

merge
parents 98b0b8f6 a56f09ba
...@@ -108,7 +108,6 @@ dependencies { ...@@ -108,7 +108,6 @@ dependencies {
implementation libraries.timber implementation libraries.timber
implementation libraries.threeTenABP implementation libraries.threeTenABP
implementation libraries.rxBinding
implementation libraries.fresco implementation libraries.fresco
api libraries.frescoOkHttp api libraries.frescoOkHttp
......
...@@ -139,6 +139,11 @@ class ChatRoomAdapter( ...@@ -139,6 +139,11 @@ class ChatRoomAdapter(
} }
} }
fun clearData() {
dataSet.clear()
notifyDataSetChanged()
}
fun appendData(dataSet: List<BaseUiModel<*>>) { fun appendData(dataSet: List<BaseUiModel<*>>) {
val previousDataSetSize = this.dataSet.size val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(dataSet) this.dataSet.addAll(dataSet)
......
package chat.rocket.android.chatroom.presentation package chat.rocket.android.chatroom.presentation
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.AutoCompleteType import chat.rocket.android.chatroom.adapter.AutoCompleteType
...@@ -64,6 +63,7 @@ import chat.rocket.core.internal.rest.unstarMessage ...@@ -64,6 +63,7 @@ import chat.rocket.core.internal.rest.unstarMessage
import chat.rocket.core.internal.rest.updateMessage import chat.rocket.core.internal.rest.updateMessage
import chat.rocket.core.internal.rest.uploadFile import chat.rocket.core.internal.rest.uploadFile
import chat.rocket.core.internal.rest.favorite import chat.rocket.core.internal.rest.favorite
import chat.rocket.core.internal.rest.searchMessages
import chat.rocket.core.model.ChatRoomRole import chat.rocket.core.model.ChatRoomRole
import chat.rocket.core.model.Command import chat.rocket.core.model.Command
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
...@@ -158,7 +158,12 @@ class ChatRoomPresenter @Inject constructor( ...@@ -158,7 +158,12 @@ class ChatRoomPresenter @Inject constructor(
} ?: false } ?: false
} }
fun loadMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) { fun loadMessages(
chatRoomId: String,
chatRoomType: String,
offset: Long = 0,
clearDataSet: Boolean = false
) {
this.chatRoomId = chatRoomId this.chatRoomId = chatRoomId
this.chatRoomType = chatRoomType this.chatRoomType = chatRoomType
launchUI(strategy) { launchUI(strategy) {
...@@ -173,13 +178,13 @@ class ChatRoomPresenter @Inject constructor( ...@@ -173,13 +178,13 @@ class ChatRoomPresenter @Inject constructor(
) )
) )
if (oldMessages.isNotEmpty()) { if (oldMessages.isNotEmpty()) {
view.showMessages(oldMessages) view.showMessages(oldMessages, clearDataSet)
loadMissingMessages() loadMissingMessages()
} else { } else {
loadAndShowMessages(chatRoomId, chatRoomType, offset) loadAndShowMessages(chatRoomId, chatRoomType, offset, clearDataSet)
} }
} else { } else {
loadAndShowMessages(chatRoomId, chatRoomType, offset) loadAndShowMessages(chatRoomId, chatRoomType, offset, clearDataSet)
} }
// TODO: For now we are marking the room as read if we can get the messages (I mean, no exception occurs) // TODO: For now we are marking the room as read if we can get the messages (I mean, no exception occurs)
...@@ -206,21 +211,49 @@ class ChatRoomPresenter @Inject constructor( ...@@ -206,21 +211,49 @@ class ChatRoomPresenter @Inject constructor(
private suspend fun loadAndShowMessages( private suspend fun loadAndShowMessages(
chatRoomId: String, chatRoomId: String,
chatRoomType: String, chatRoomType: String,
offset: Long = 0 offset: Long = 0,
clearDataSet: Boolean
) { ) {
val messages = val messages =
retryIO(description = "messages chatRoom: $chatRoomId, type: $chatRoomType, offset: $offset") { retryIO("loadAndShowMessages($chatRoomId, $chatRoomType, $offset") {
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
} }
messagesRepository.saveAll(messages) messagesRepository.saveAll(messages)
view.showMessages( view.showMessages(
mapper.map(messages, RoomUiModel( mapper.map(
roles = chatRoles, messages,
isBroadcast = chatIsBroadcast, isRoom = true RoomUiModel(roles = chatRoles, isBroadcast = chatIsBroadcast, isRoom = true)
)) ),
clearDataSet
) )
} }
fun searchMessages(chatRoomId: String, searchText: String) {
launchUI(strategy) {
try {
view.showLoading()
val messages = retryIO("searchMessages($chatRoomId, $searchText)") {
client.searchMessages(chatRoomId, searchText).result
}
view.showSearchedMessages(
mapper.map(
messages,
RoomUiModel(chatRoles, chatIsBroadcast, true)
)
)
} catch (ex: Exception) {
Timber.e(ex)
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
}
}
fun sendMessage(chatRoomId: String, text: String, messageId: String?) { fun sendMessage(chatRoomId: String, text: String, messageId: String?) {
launchUI(strategy) { launchUI(strategy) {
try { try {
......
package chat.rocket.android.chatroom.presentation package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.chatroom.uimodel.BaseUiModel import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
...@@ -23,8 +22,16 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -23,8 +22,16 @@ interface ChatRoomView : LoadingView, MessageView {
* Shows the chat room messages. * Shows the chat room messages.
* *
* @param dataSet The data set to show. * @param dataSet The data set to show.
* @param clearDataSet If true it will clear the previous data set.
*/ */
fun showMessages(dataSet: List<BaseUiModel<*>>) fun showMessages(dataSet: List<BaseUiModel<*>>, clearDataSet: Boolean)
/**
* Shows the chat room messages in the basis of a search term.
*
* @param dataSet The data set to show.
*/
fun showSearchedMessages(dataSet: List<BaseUiModel<*>>)
/** /**
* Send a message to a chat room. * Send a message to a chat room.
......
package chat.rocket.android.chatroom.ui
import android.content.Context
import android.view.Menu
import android.view.MenuItem
import android.widget.EditText
import androidx.appcompat.widget.SearchView
import androidx.core.content.res.ResourcesCompat
import chat.rocket.android.R
import chat.rocket.android.util.extension.onQueryTextListener
import chat.rocket.common.model.RoomType
internal fun ChatRoomFragment.setupMenu(menu: Menu) {
setupSearchMessageMenuItem(menu, requireContext())
setupFavoriteMenuItem(menu)
if (chatRoomType != RoomType.DIRECT_MESSAGE) {
menu.add(
Menu.NONE,
MENU_ACTION_MEMBER,
Menu.NONE,
R.string.title_members_list
)
menu.add(
Menu.NONE,
MENU_ACTION_MENTIONS,
Menu.NONE,
R.string.msg_mentions
)
}
menu.add(
Menu.NONE,
MENU_ACTION_PINNED_MESSAGES,
Menu.NONE,
R.string.title_pinned_messages
)
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_MESSAGES,
Menu.NONE,
R.string.title_favorite_messages
)
menu.add(
Menu.NONE,
MENU_ACTION_FILES,
Menu.NONE,
R.string.title_files
)
}
internal fun ChatRoomFragment.setOnMenuItemClickListener(item: MenuItem) {
when (item.itemId) {
MENU_ACTION_FAVORITE_UNFAVORITE_CHAT -> presenter.toggleFavoriteChatRoom(
chatRoomId,
isFavorite
)
MENU_ACTION_MEMBER -> presenter.toMembersList(chatRoomId)
MENU_ACTION_MENTIONS -> presenter.toMentions(chatRoomId)
MENU_ACTION_PINNED_MESSAGES -> presenter.toPinnedMessageList(chatRoomId)
MENU_ACTION_FAVORITE_MESSAGES -> presenter.toFavoriteMessageList(chatRoomId)
MENU_ACTION_FILES -> presenter.toFileList(chatRoomId)
}
}
private fun ChatRoomFragment.setupSearchMessageMenuItem(menu: Menu, context: Context) {
val searchItem = menu.add(
Menu.NONE,
Menu.NONE,
Menu.NONE,
R.string.title_search_message
).setActionView(SearchView(context))
.setIcon(R.drawable.ic_search_white_24dp)
.setShowAsActionFlags(
MenuItem.SHOW_AS_ACTION_IF_ROOM or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
)
(searchItem?.actionView as? SearchView)?.let {
// TODO: Check why we need to stylize the search text programmatically instead of by defining it in the styles.xml (ChatRoom.SearchView)
stylizeSearchView(it, context)
setupSearchViewTextListener(it)
if (it.isIconified) {
isSearchTermQueried = false
}
}
}
private fun stylizeSearchView(searchView: SearchView, context: Context) {
val searchText = searchView.findViewById<EditText>(androidx.appcompat.R.id.search_src_text)
searchText.setTextColor(ResourcesCompat.getColor(context.resources, R.color.color_white, null))
searchText.setHintTextColor(
ResourcesCompat.getColor(context.resources, R.color.color_white, null)
)
}
private fun ChatRoomFragment.setupSearchViewTextListener(searchView: SearchView) {
searchView.onQueryTextListener {
// TODO: We use isSearchTermQueried to avoid querying when the search view is expanded but the user doesn't start typing. Check for a native solution.
if (it.isEmpty() && isSearchTermQueried) {
presenter.loadMessages(chatRoomId, chatRoomType, clearDataSet = true)
} else if (it.isNotEmpty()){
presenter.searchMessages(chatRoomId, it)
isSearchTermQueried = true
}
}
}
private fun ChatRoomFragment.setupFavoriteMenuItem(menu: Menu) {
if (isFavorite) {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_UNFAVORITE_CHAT,
Menu.NONE,
R.string.title_unfavorite_chat
).setIcon(R.drawable.ic_star_yellow_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} else {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_UNFAVORITE_CHAT,
Menu.NONE,
R.string.title_favorite_chat
).setIcon(R.drawable.ic_star_border_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
}
\ No newline at end of file
...@@ -46,15 +46,19 @@ class ChatRoomsFragmentModule { ...@@ -46,15 +46,19 @@ class ChatRoomsFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideRocketChatClient(factory: RocketChatClientFactory, fun provideRocketChatClient(
@Named("currentServer") currentServer: String): RocketChatClient { factory: RocketChatClientFactory,
@Named("currentServer") currentServer: String
): RocketChatClient {
return factory.create(currentServer) return factory.create(currentServer)
} }
@Provides @Provides
@PerFragment @PerFragment
fun provideDatabaseManager(factory: DatabaseManagerFactory, fun provideDatabaseManager(
@Named("currentServer") currentServer: String): DatabaseManager { factory: DatabaseManagerFactory,
@Named("currentServer") currentServer: String
): DatabaseManager {
return factory.create(currentServer) return factory.create(currentServer)
} }
...@@ -64,31 +68,39 @@ class ChatRoomsFragmentModule { ...@@ -64,31 +68,39 @@ class ChatRoomsFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideConnectionManager(factory: ConnectionManagerFactory, fun provideConnectionManager(
@Named("currentServer") currentServer: String): ConnectionManager { factory: ConnectionManagerFactory,
@Named("currentServer") currentServer: String
): ConnectionManager {
return factory.create(currentServer) return factory.create(currentServer)
} }
@Provides @Provides
@PerFragment @PerFragment
fun provideFetchChatRoomsInteractor(client: RocketChatClient, fun provideFetchChatRoomsInteractor(
dbManager: DatabaseManager): FetchChatRoomsInteractor { client: RocketChatClient,
dbManager: DatabaseManager
): FetchChatRoomsInteractor {
return FetchChatRoomsInteractor(client, dbManager) return FetchChatRoomsInteractor(client, dbManager)
} }
@Provides @Provides
@PerFragment @PerFragment
fun providePublicSettings(repository: SettingsRepository, fun providePublicSettings(
@Named("currentServer") currentServer: String): PublicSettings { repository: SettingsRepository,
@Named("currentServer") currentServer: String
): PublicSettings {
return repository.get(currentServer) return repository.get(currentServer)
} }
@Provides @Provides
@PerFragment @PerFragment
fun provideRoomMapper(context: Application, fun provideRoomMapper(
repository: SettingsRepository, context: Application,
localRepository: LocalRepository, repository: SettingsRepository,
@Named("currentServer") serverUrl: String): RoomUiModelMapper { localRepository: LocalRepository,
@Named("currentServer") serverUrl: String
): RoomUiModelMapper {
return RoomUiModelMapper(context, repository.get(serverUrl), localRepository, serverUrl) return RoomUiModelMapper(context, repository.get(serverUrl), localRepository, serverUrl)
} }
} }
\ No newline at end of file
...@@ -32,6 +32,7 @@ import chat.rocket.android.db.DatabaseManager ...@@ -32,6 +32,7 @@ import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.helper.ChatRoomsSortOrder import chat.rocket.android.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants import chat.rocket.android.helper.Constants
import chat.rocket.android.helper.SharedPreferenceHelper import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.util.extension.onQueryTextListener
import chat.rocket.android.util.extensions.fadeIn import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
...@@ -164,15 +165,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -164,15 +165,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
searchView = searchItem?.actionView as? SearchView searchView = searchItem?.actionView as? SearchView
searchView?.setIconifiedByDefault(false) searchView?.setIconifiedByDefault(false)
searchView?.maxWidth = Integer.MAX_VALUE searchView?.maxWidth = Integer.MAX_VALUE
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { searchView?.onQueryTextListener { queryChatRoomsByName(it) }
override fun onQueryTextSubmit(query: String?): Boolean {
return queryChatRoomsByName(query)
}
override fun onQueryTextChange(newText: String?): Boolean {
return queryChatRoomsByName(newText)
}
})
val expandListener = object : MenuItem.OnActionExpandListener { val expandListener = object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
...@@ -297,25 +290,25 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -297,25 +290,25 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
private fun showConnectionState(state: State) { private fun showConnectionState(state: State) {
Timber.d("Got new state: $state") Timber.d("Got new state: $state")
ui { ui {
connection_status_text.fadeIn() text_connection_status.fadeIn()
handler.removeCallbacks(dismissStatus) handler.removeCallbacks(dismissStatus)
when (state) { when (state) {
is State.Connected -> { is State.Connected -> {
connection_status_text.text = getString(R.string.status_connected) text_connection_status.text = getString(R.string.status_connected)
handler.postDelayed(dismissStatus, 2000) handler.postDelayed(dismissStatus, 2000)
} }
is State.Disconnected -> connection_status_text.text = getString(R.string.status_disconnected) is State.Disconnected -> text_connection_status.text = getString(R.string.status_disconnected)
is State.Connecting -> connection_status_text.text = getString(R.string.status_connecting) is State.Connecting -> text_connection_status.text = getString(R.string.status_connecting)
is State.Authenticating -> connection_status_text.text = getString(R.string.status_authenticating) is State.Authenticating -> text_connection_status.text = getString(R.string.status_authenticating)
is State.Disconnecting -> connection_status_text.text = getString(R.string.status_disconnecting) is State.Disconnecting -> text_connection_status.text = getString(R.string.status_disconnecting)
is State.Waiting -> connection_status_text.text = getString(R.string.status_waiting, state.seconds) is State.Waiting -> text_connection_status.text = getString(R.string.status_waiting, state.seconds)
} }
} }
} }
private val dismissStatus = { private val dismissStatus = {
if (connection_status_text != null) { if (text_connection_status != null) {
connection_status_text.fadeOut() text_connection_status.fadeOut()
} }
} }
...@@ -331,4 +324,4 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -331,4 +324,4 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
return true return true
} }
} }
\ No newline at end of file
...@@ -20,7 +20,7 @@ import chat.rocket.android.createchannel.presentation.CreateChannelView ...@@ -20,7 +20,7 @@ import chat.rocket.android.createchannel.presentation.CreateChannelView
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.members.adapter.MembersAdapter import chat.rocket.android.members.adapter.MembersAdapter
import chat.rocket.android.members.uimodel.MemberUiModel import chat.rocket.android.members.uimodel.MemberUiModel
import chat.rocket.android.util.extensions.asObservable import chat.rocket.android.util.extension.asObservable
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui import chat.rocket.android.util.extensions.ui
...@@ -221,7 +221,6 @@ class CreateChannelFragment : Fragment(), CreateChannelView, ActionMode.Callback ...@@ -221,7 +221,6 @@ class CreateChannelFragment : Fragment(), CreateChannelView, ActionMode.Callback
LinearLayoutManager(context, RecyclerView.VERTICAL, false) LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(it, DividerItemDecoration.HORIZONTAL)) recycler_view.addItemDecoration(DividerItemDecoration(it, DividerItemDecoration.HORIZONTAL))
recycler_view.adapter = adapter recycler_view.adapter = adapter
} }
} }
......
...@@ -247,6 +247,7 @@ class AppModule { ...@@ -247,6 +247,7 @@ class AppModule {
val res = context.resources val res = context.resources
return SpannableConfiguration.builder(context) return SpannableConfiguration.builder(context)
.theme(SpannableTheme.builder() .theme(SpannableTheme.builder()
.blockMargin(0)
.linkColor(res.getColor(R.color.colorAccent)) .linkColor(res.getColor(R.color.colorAccent))
.build()) .build())
.build() .build()
......
...@@ -10,6 +10,7 @@ import androidx.core.content.res.ResourcesCompat ...@@ -10,6 +10,7 @@ import androidx.core.content.res.ResourcesCompat
import android.text.Spanned import android.text.Spanned
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.text.style.ReplacementSpan import android.text.style.ReplacementSpan
import android.text.style.StyleSpan
import android.util.Patterns import android.util.Patterns
import android.view.View import android.view.View
import chat.rocket.android.R import chat.rocket.android.R
...@@ -23,6 +24,10 @@ import chat.rocket.common.model.SimpleUser ...@@ -23,6 +24,10 @@ import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import org.commonmark.node.AbstractVisitor import org.commonmark.node.AbstractVisitor
import org.commonmark.node.Document import org.commonmark.node.Document
import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.Text import org.commonmark.node.Text
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder import ru.noties.markwon.SpannableBuilder
...@@ -59,7 +64,7 @@ class MessageParser @Inject constructor( ...@@ -59,7 +64,7 @@ class MessageParser @Inject constructor(
val builder = SpannableBuilder() val builder = SpannableBuilder()
val content = EmojiRepository.shortnameToUnicode(text, true) val content = EmojiRepository.shortnameToUnicode(text, true)
val parentNode = parser.parse(toLenientMarkdown(content)) val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(SpannableMarkdownVisitor(configuration, builder)) parentNode.accept(MarkdownVisitor(configuration, builder))
parentNode.accept(LinkVisitor(builder)) parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(configuration, builder)) parentNode.accept(EmojiVisitor(configuration, builder))
message.mentions?.let { message.mentions?.let {
...@@ -138,6 +143,37 @@ class MessageParser @Inject constructor( ...@@ -138,6 +143,37 @@ class MessageParser @Inject constructor(
} }
} }
class MarkdownVisitor(
configuration: SpannableConfiguration,
val builder: SpannableBuilder
) : SpannableMarkdownVisitor(configuration, builder) {
/**
* NOOP
*/
override fun visit(orderedList: OrderedList) {
var number = orderedList.startNumber
val delimiter = orderedList.delimiter
var node: Node? = orderedList.firstChild
while (node != null) {
if (node is ListItem) {
newLine()
builder.append("$number$delimiter ")
super.visit(node.firstChild as Paragraph)
newLine()
}
number++
node = node.next
}
}
private fun newLine() {
if (builder.length() > 0 && '\n' != builder.lastChar()) {
builder.append('\n')
}
}
}
class LinkVisitor(private val builder: SpannableBuilder) : AbstractVisitor() { class LinkVisitor(private val builder: SpannableBuilder) : AbstractVisitor() {
override fun visit(text: Text) { override fun visit(text: Text) {
......
...@@ -84,7 +84,7 @@ object OauthHelper { ...@@ -84,7 +84,7 @@ object OauthHelper {
* @return The Facebook Oauth URL. * @return The Facebook Oauth URL.
*/ */
fun getFacebookOauthUrl(clientId: String, serverUrl: String, state: String): String { fun getFacebookOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://facebook.com/v2.9/dialog/oauth" + return "https://facebook.com/v2.9/dialog/oauth" +
"?client_id=$clientId" + "?client_id=$clientId" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/facebook?close" + "&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/facebook?close" +
"&state=$state" + "&state=$state" +
...@@ -113,12 +113,18 @@ object OauthHelper { ...@@ -113,12 +113,18 @@ object OauthHelper {
state: String, state: String,
scope: String scope: String
): String { ): String {
return host + (authorizePath +
authorizePath +
"?client_id=$clientId" + "?client_id=$clientId" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/$serviceName" + "&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/$serviceName" +
"&state=$state" + "&state=$state" +
"&scope=$scope" + "&scope=$scope" +
"&response_type=code" "&response_type=code"
).let {
return if (it.contains(host)) {
it
} else {
host + it
}
}
} }
} }
\ No newline at end of file
...@@ -38,20 +38,20 @@ import timber.log.Timber ...@@ -38,20 +38,20 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MainPresenter @Inject constructor( class MainPresenter @Inject constructor(
private val view: MainView, private val view: MainView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: MainNavigator, private val navigator: MainNavigator,
private val tokenRepository: TokenRepository, private val tokenRepository: TokenRepository,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val navHeaderMapper: NavHeaderUiModelMapper, private val navHeaderMapper: NavHeaderUiModelMapper,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInteractor: RemoveAccountInteractor, private val removeAccountInteractor: RemoveAccountInteractor,
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
dbManagerFactory: DatabaseManagerFactory, dbManagerFactory: DatabaseManagerFactory,
getSettingsInteractor: GetSettingsInteractor, getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory managerFactory: ConnectionManagerFactory
) : CheckServerPresenter(strategy, factory, view = view) { ) : CheckServerPresenter(strategy, factory, view = view) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer) private val manager = managerFactory.create(currentServer)
...@@ -69,16 +69,34 @@ class MainPresenter @Inject constructor( ...@@ -69,16 +69,34 @@ class MainPresenter @Inject constructor(
fun toCreateChannel() = navigator.toCreateChannel() fun toCreateChannel() = navigator.toCreateChannel()
fun loadServerAccounts() {
launchUI(strategy) {
try {
view.setupServerAccountList(getAccountsInteractor.get())
} catch (ex: Exception) {
when (ex) {
is RocketChatAuthException -> logout()
else -> {
Timber.d(ex, "Error loading serve accounts")
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
}
}
}
fun loadCurrentInfo() { fun loadCurrentInfo() {
checkServerInfo(currentServer) checkServerInfo(currentServer)
launchUI(strategy) { launchUI(strategy) {
try { try {
val me = retryIO("me") { val me = retryIO("me") { client.me() }
client.me()
}
val model = navHeaderMapper.mapToUiModel(me) val model = navHeaderMapper.mapToUiModel(me)
saveAccount(model) saveAccount(model)
view.setupNavHeader(model, getAccountsInteractor.get()) view.setupUserAccountInfo(model)
} catch (ex: Exception) { } catch (ex: Exception) {
when (ex) { when (ex) {
is RocketChatAuthException -> { is RocketChatAuthException -> {
...@@ -208,8 +226,6 @@ class MainPresenter @Inject constructor( ...@@ -208,8 +226,6 @@ class MainPresenter @Inject constructor(
} }
} }
private suspend fun updateMyself(myself: Myself) { private fun updateMyself(myself: Myself) =
val model = navHeaderMapper.mapToUiModel(myself) view.setupUserAccountInfo(navHeaderMapper.mapToUiModel(myself))
view.setupNavHeader(model, getAccountsInteractor.get())
}
} }
\ No newline at end of file
...@@ -16,17 +16,24 @@ interface MainView : MessageView, VersionCheckView { ...@@ -16,17 +16,24 @@ interface MainView : MessageView, VersionCheckView {
fun showUserStatus(userStatus: UserStatus) fun showUserStatus(userStatus: UserStatus)
/** /**
* Setups the navigation header. * Setups the user account info (displayed in the nav. header)
* *
* @param uiModel The [NavHeaderUiModel]. * @param uiModel The [NavHeaderUiModel].
* @param accounts The list of accounts.
*/ */
fun setupNavHeader(uiModel: NavHeaderUiModel, accounts: List<Account>) fun setupUserAccountInfo(uiModel: NavHeaderUiModel)
/**
* Setups the server account list.
*
* @param serverAccountList The list of server accounts.
*/
fun setupServerAccountList(serverAccountList: List<Account>)
fun closeServerSelection() fun closeServerSelection()
fun invalidateToken(token: String) fun invalidateToken(token: String)
fun showProgress() fun showProgress()
fun hideProgress() fun hideProgress()
} }
\ No newline at end of file
...@@ -10,7 +10,6 @@ import androidx.appcompat.app.AppCompatActivity ...@@ -10,7 +10,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import android.view.Gravity import android.view.Gravity
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
...@@ -56,6 +55,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -56,6 +55,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
private var expanded = false private var expanded = false
private val headerLayout by lazy { view_navigation.getHeaderView(0) } private val headerLayout by lazy { view_navigation.getHeaderView(0) }
private var chatRoomId: String? = null private var chatRoomId: String? = null
private var progressDialog : ProgressDialog? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this) AndroidInjection.inject(this)
...@@ -75,6 +75,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -75,6 +75,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID) chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
presenter.connect() presenter.connect()
presenter.loadServerAccounts()
presenter.loadCurrentInfo() presenter.loadCurrentInfo()
setupToolbar() setupToolbar()
setupNavigationView() setupNavigationView()
...@@ -119,8 +120,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -119,8 +120,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
} }
} }
override fun setupNavHeader(uiModel: NavHeaderUiModel, accounts: List<Account>) { override fun setupUserAccountInfo(uiModel: NavHeaderUiModel) {
Timber.d("Setting up nav header: $uiModel")
with(headerLayout) { with(headerLayout) {
with(uiModel) { with(uiModel) {
if (userStatus != null) { if (userStatus != null) {
...@@ -139,10 +139,43 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -139,10 +139,43 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
} }
text_server_url.text = uiModel.serverUrl text_server_url.text = uiModel.serverUrl
} }
setupAccountsList(headerLayout, accounts)
} }
} }
override fun setupServerAccountList(serverAccountList: List<Account>) {
accounts_list.layoutManager = LinearLayoutManager(this)
accounts_list.adapter = AccountsAdapter(serverAccountList, object : Selector {
override fun onStatusSelected(userStatus: UserStatus) {
presenter.changeDefaultStatus(userStatus)
}
override fun onAccountSelected(serverUrl: String) {
presenter.changeServer(serverUrl)
}
override fun onAddedAccountSelected() {
presenter.addNewServer()
}
})
headerLayout.account_container.setOnClickListener {
it.image_account_expand.rotateBy(180f)
if (expanded) {
accounts_list.fadeOut()
} else {
accounts_list.fadeIn()
}
expanded = !expanded
}
headerLayout.image_avatar.setOnClickListener {
view_navigation.menu.findItem(R.id.action_update_profile).isChecked = true
presenter.toUserProfile()
drawer_layout.closeDrawer(Gravity.START)
}
}
override fun closeServerSelection() { override fun closeServerSelection() {
view_navigation.getHeaderView(0).account_container.performClick() view_navigation.getHeaderView(0).account_container.performClick()
} }
...@@ -174,9 +207,8 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -174,9 +207,8 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
.show() .show()
} }
override fun invalidateToken(token: String) { override fun invalidateToken(token: String) =
FirebaseInstanceId.getInstance().deleteToken(token, FirebaseMessaging.INSTANCE_ID_SCOPE) FirebaseInstanceId.getInstance().deleteToken(token, FirebaseMessaging.INSTANCE_ID_SCOPE)
}
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) = showToast(resId)
...@@ -221,57 +253,14 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, ...@@ -221,57 +253,14 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
} }
} }
private fun setupAccountsList(header: View, accounts: List<Account>) { fun getDrawerLayout(): DrawerLayout = drawer_layout
accounts_list.layoutManager = LinearLayoutManager(this)
accounts_list.adapter = AccountsAdapter(accounts, object : Selector {
override fun onStatusSelected(userStatus: UserStatus) {
presenter.changeDefaultStatus(userStatus)
}
override fun onAccountSelected(serverUrl: String) {
presenter.changeServer(serverUrl)
}
override fun onAddedAccountSelected() {
presenter.addNewServer()
}
})
header.account_container.setOnClickListener {
header.image_account_expand.rotateBy(180f)
if (expanded) {
accounts_list.fadeOut()
} else {
accounts_list.fadeIn()
}
expanded = !expanded
}
header.image_avatar.setOnClickListener {
view_navigation.menu.findItem(R.id.action_update_profile).isChecked = true
presenter.toUserProfile()
drawer_layout.closeDrawer(Gravity.START)
}
}
fun getDrawerLayout(): DrawerLayout {
return drawer_layout
}
fun openDrawer() { fun openDrawer() = drawer_layout.openDrawer(Gravity.START)
drawer_layout.openDrawer(Gravity.START)
}
fun closeDrawer() { fun closeDrawer() = drawer_layout.closeDrawer(Gravity.START)
drawer_layout.closeDrawer(Gravity.START)
}
fun setCheckedNavDrawerItem(@IdRes item: Int) { fun setCheckedNavDrawerItem(@IdRes item: Int) = view_navigation.setCheckedItem(item)
view_navigation.setCheckedItem(item)
}
private var progressDialog : ProgressDialog? = null
override fun showProgress() { override fun showProgress() {
progressDialog = ProgressDialog.show(this, getString(R.string.app_name), getString(R.string.msg_log_out), true, false) progressDialog = ProgressDialog.show(this, getString(R.string.app_name), getString(R.string.msg_log_out), true, false)
} }
......
...@@ -11,6 +11,7 @@ import chat.rocket.android.R ...@@ -11,6 +11,7 @@ import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.presentation.ProfilePresenter import chat.rocket.android.profile.presentation.ProfilePresenter
import chat.rocket.android.profile.presentation.ProfileView import chat.rocket.android.profile.presentation.ProfileView
import chat.rocket.android.util.extension.asObservable
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
......
...@@ -9,7 +9,7 @@ import chat.rocket.android.settings.password.presentation.PasswordPresenter ...@@ -9,7 +9,7 @@ import chat.rocket.android.settings.password.presentation.PasswordPresenter
import chat.rocket.android.settings.password.presentation.PasswordView import chat.rocket.android.settings.password.presentation.PasswordView
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import chat.rocket.android.util.extensions.asObservable import chat.rocket.android.util.extension.asObservable
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui import chat.rocket.android.util.extensions.ui
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
......
...@@ -5,10 +5,8 @@ import android.text.Spanned ...@@ -5,10 +5,8 @@ import android.text.Spanned
import android.text.TextUtils import android.text.TextUtils
import android.util.Base64 import android.util.Base64
import android.util.Patterns import android.util.Patterns
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.emoji.EmojiParser import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiTypefaceSpan
import org.json.JSONObject import org.json.JSONObject
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import java.net.URLDecoder import java.net.URLDecoder
...@@ -21,21 +19,6 @@ fun String.ifEmpty(value: String): String { ...@@ -21,21 +19,6 @@ fun String.ifEmpty(value: String): String {
return this return this
} }
fun CharSequence.ifEmpty(value: String): CharSequence {
if (isEmpty()) {
return value
}
return this
}
fun EditText.erase() {
this.text.clear()
val spans = this.text.getSpans(0, text.length, EmojiTypefaceSpan::class.java)
spans.forEach {
text.removeSpan(it)
}
}
fun String.isEmail(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches() fun String.isEmail(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun String.encodeToBase64(): String { fun String.encodeToBase64(): String {
...@@ -94,9 +77,3 @@ var TextView.content: CharSequence? ...@@ -94,9 +77,3 @@ var TextView.content: CharSequence?
Markwon.scheduleDrawables(this) Markwon.scheduleDrawables(this)
Markwon.scheduleTableRows(this) Markwon.scheduleTableRows(this)
} }
var TextView.spanned: CharSequence?
get() = text
set(value) {
text = spanned
}
\ No newline at end of file
...@@ -11,7 +11,6 @@ import kotlinx.coroutines.experimental.launch ...@@ -11,7 +11,6 @@ import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
import kotlin.coroutines.experimental.CoroutineContext import kotlin.coroutines.experimental.CoroutineContext
class WrappedLiveData<Source, Output>( class WrappedLiveData<Source, Output>(
private val runContext: CoroutineContext = CommonPool, private val runContext: CoroutineContext = CommonPool,
private val source: LiveData<Source>, private val source: LiveData<Source>,
......
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true" android:autoMirrored="true"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:viewportWidth="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFF" android:fillColor="#FFFFFF"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" /> android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector> </vector>
\ No newline at end of file
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<solid android:color="@color/colorAccent" /> <solid android:color="@color/colorAccent" />
<size <size
android:width="20dp" android:width="24dp"
android:height="20dp" /> android:height="24dp" />
</shape> </shape>
\ No newline at end of file
...@@ -12,9 +12,9 @@ ...@@ -12,9 +12,9 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/text_typing_status" app:layout_constraintBottom_toTopOf="@id/text_typing_status"
app:layout_constraintTop_toBottomOf="@+id/text_connection_status"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent">
app:layout_constraintTop_toTopOf="parent">
<include <include
android:id="@+id/layout_message_list" android:id="@+id/layout_message_list"
...@@ -117,7 +117,7 @@ ...@@ -117,7 +117,7 @@
app:layout_constraintBottom_toTopOf="@id/layout_message_composer" /> app:layout_constraintBottom_toTopOf="@id/layout_message_composer" />
<TextView <TextView
android:id="@+id/connection_status_text" android:id="@+id/text_connection_status"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="32dp" android:layout_height="32dp"
android:alpha="0" android:alpha="0"
...@@ -127,6 +127,7 @@ ...@@ -127,6 +127,7 @@
android:textAppearance="@style/TextAppearance.AppCompat.Body2" android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/colorWhite" android:textColor="@color/colorWhite"
android:visibility="gone" android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
tools:alpha="1" tools:alpha="1"
tools:text="connected" tools:text="connected"
tools:visibility="visible" /> tools:visibility="visible" />
...@@ -144,4 +145,4 @@ ...@@ -144,4 +145,4 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
android:layout_below="@+id/text_connection_status"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
...@@ -31,7 +32,7 @@ ...@@ -31,7 +32,7 @@
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <TextView
android:id="@+id/connection_status_text" android:id="@+id/text_connection_status"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="32dp" android:layout_height="32dp"
android:alpha="0" android:alpha="0"
...@@ -55,4 +56,4 @@ ...@@ -55,4 +56,4 @@
android:textSize="20sp" android:textSize="20sp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
</RelativeLayout> </RelativeLayout>
\ No newline at end of file
...@@ -8,10 +8,10 @@ ...@@ -8,10 +8,10 @@
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding" android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding"> android:paddingTop="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding">
<LinearLayout <LinearLayout
android:id="@+id/new_messages_notif" android:id="@+id/new_messages_notif"
...@@ -19,8 +19,8 @@ ...@@ -19,8 +19,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone" android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"> tools:visibility="visible">
...@@ -56,63 +56,67 @@ ...@@ -56,63 +56,67 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif" /> app:layout_constraintTop_toBottomOf="@+id/new_messages_notif" />
<LinearLayout <TextView
android:id="@+id/top_container" android:id="@+id/text_sender"
android:layout_width="0dp" style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:orientation="horizontal" app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/layout_avatar" app:layout_constraintStart_toEndOf="@+id/layout_avatar"
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif"> app:layout_constraintTop_toBottomOf="@+id/new_messages_notif"
tools:text="Ronald Perkins" />
<TextView <TextView
android:id="@+id/text_sender" android:id="@+id/text_message_time"
style="@style/Sender.Name.TextView" style="@style/Timestamp.TextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="Ronald Perkins" /> android:layout_marginStart="4dp"
app:layout_constraintBaseline_toBaselineOf="@+id/text_sender"
<TextView app:layout_constraintHorizontal_bias="0.5"
android:id="@+id/text_message_time" app:layout_constraintStart_toEndOf="@+id/text_sender"
style="@style/Timestamp.TextView" tools:text="11:45 PM" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
tools:text="11:45 PM" />
<TextView <TextView
android:id="@+id/text_edit_indicator" android:id="@+id/text_edit_indicator"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="4dp"
android:text="@string/msg_edited" android:text="@string/msg_edited"
android:textStyle="italic" android:textStyle="italic"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> app:layout_constraintBaseline_toBaselineOf="@+id/text_sender"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/text_message_time"
tools:visibility="visible" />
<ImageView <ImageView
android:id="@+id/image_star_indicator" android:id="@+id/image_star_indicator"
android:layout_width="14dp" android:layout_width="14dp"
android:layout_height="14dp" android:layout_height="14dp"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginStart="8dp" android:layout_marginStart="4dp"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:src="@drawable/ic_action_message_star_24dp" android:src="@drawable/ic_action_message_star_24dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> app:layout_constraintBottom_toBottomOf="@+id/text_sender"
</LinearLayout> app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/text_edit_indicator"
app:layout_constraintTop_toTopOf="@+id/text_sender"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/text_content" android:id="@+id/text_content"
style="@style/Message.TextView" style="@style/Message.TextView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
app:layout_constraintStart_toStartOf="@+id/top_container" android:layout_marginBottom="2dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_container" android:layout_marginEnd="4dp"
android:textDirection="locale" app:layout_constraintStart_toStartOf="@+id/text_sender"
app:layout_constraintTop_toBottomOf="@+id/text_sender"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" /> tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<include <include
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
...@@ -10,40 +11,33 @@ ...@@ -10,40 +11,33 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:scrollbars="vertical" /> android:scrollbars="vertical" />
<RelativeLayout <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/button_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:visibility="invisible"
android:layout_alignParentBottom="true" android:layout_margin="16dp"
app:layout_anchor="@id/recycler_view" android:src="@drawable/ic_arrow_downward_24dp"
app:layout_anchorGravity="bottom|end"> android:theme="@style/Theme.AppCompat"
android:tint="@color/actionMenuColor"
app:backgroundTint="@color/colorWhite"
tools:visibility="visible"
android:layout_gravity="end|bottom" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <TextView
android:id="@+id/button_fab" android:id="@+id/text_count"
android:layout_alignParentEnd="true" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:elevation="6dp"
android:visibility="invisible" android:gravity="center"
android:layout_margin="16dp" android:textColor="@color/colorWhite"
android:src="@drawable/ic_arrow_downward_24dp" android:visibility="invisible"
android:theme="@style/Theme.AppCompat" android:textSize="@dimen/message_time_text_size"
android:tint="@color/actionMenuColor" android:background="@drawable/round_textview"
app:backgroundTint="@color/colorWhite" tools:visibility="visible"
app:fabSize="mini" /> tools:text="1"
android:layout_gravity="center_vertical"
<TextView app:layout_anchor="@id/button_fab"
android:id="@+id/text_count" app:layout_anchorGravity="end" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="7dp"
android:padding="2dp"
android:gravity="center"
android:layout_margin="10dp"
android:textColor="@color/colorWhite"
android:visibility="invisible"
android:layout_alignParentEnd="true"
android:textSize="@dimen/message_time_text_size"
android:background="@drawable/round_textview" />
</RelativeLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<item <item
android:id="@+id/action_search" android:id="@+id/action_search"
android:icon="@drawable/ic_search_white_24px" android:icon="@drawable/ic_search_white_24dp"
android:title="@string/action_search" android:title="@string/action_search"
app:actionViewClass="androidx.appcompat.widget.SearchView" app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" /> app:showAsAction="ifRoom|collapseActionView" />
......
...@@ -191,6 +191,10 @@ ...@@ -191,6 +191,10 @@
// TODO: Add proper translation. // TODO: Add proper translation.
<string name="permission_starring_not_allowed">Starring is not allowed</string> <string name="permission_starring_not_allowed">Starring is not allowed</string>
<!-- Search message -->
<!-- TODO Add proper translation-->
<string name="title_search_message">Search message</string>
<!-- Favorite/Unfavorite chat room --> <!-- Favorite/Unfavorite chat room -->
<!-- TODO Add proper translation--> <!-- TODO Add proper translation-->
<string name="title_favorite_chat">Favorite chat</string> <string name="title_favorite_chat">Favorite chat</string>
...@@ -263,6 +267,7 @@ ...@@ -263,6 +267,7 @@
<!-- Emoji message--> <!-- Emoji message-->
<string name="msg_no_recent_emoji">Sin emojis recientes</string> <string name="msg_no_recent_emoji">Sin emojis recientes</string>
<string name="alert_title_default_skin_tone">Tono de piel predeterminado</string>
<!-- Sorting and grouping--> <!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Ordenar</string> <string name="menu_chatroom_sort">Ordenar</string>
...@@ -284,5 +289,5 @@ ...@@ -284,5 +289,5 @@
<string name="notif_action_reply_hint">RESPUESTA</string> <string name="notif_action_reply_hint">RESPUESTA</string>
<string name="notif_error_sending">La respuesta ha fallado. Inténtalo de nuevo.</string> <string name="notif_error_sending">La respuesta ha fallado. Inténtalo de nuevo.</string>
<string name="notif_success_sending">Mensaje enviado a %1$s!</string> <string name="notif_success_sending">Mensaje enviado a %1$s!</string>
<string name="msg_log_out">Saliendo de tu cuenta...</string> <string name="msg_log_out">Saliendo de tu cuenta</string>
</resources> </resources>
...@@ -193,6 +193,10 @@ ...@@ -193,6 +193,10 @@
// TODO: Add proper translation. // TODO: Add proper translation.
<string name="permission_starring_not_allowed">Starring is not allowed</string> <string name="permission_starring_not_allowed">Starring is not allowed</string>
<!-- Search message -->
<!-- TODO Add proper translation-->
<string name="title_search_message">Search message</string>
<!-- Favorite/Unfavorite chat room --> <!-- Favorite/Unfavorite chat room -->
<!-- TODO Add proper translation--> <!-- TODO Add proper translation-->
<string name="title_favorite_chat">Favorite chat</string> <string name="title_favorite_chat">Favorite chat</string>
...@@ -265,6 +269,7 @@ ...@@ -265,6 +269,7 @@
<!-- Emoji message--> <!-- Emoji message-->
<string name="msg_no_recent_emoji">Aucun emoji récent</string> <string name="msg_no_recent_emoji">Aucun emoji récent</string>
<string name="alert_title_default_skin_tone">Tonalité de peau par défaut</string>
<!-- Sorting and grouping--> <!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Trier</string> <string name="menu_chatroom_sort">Trier</string>
...@@ -286,5 +291,6 @@ ...@@ -286,5 +291,6 @@
<string name="notif_action_reply_hint">RÉPONDRE</string> <string name="notif_action_reply_hint">RÉPONDRE</string>
<string name="notif_error_sending">La réponse a échoué. Veuillez réessayer.</string> <string name="notif_error_sending">La réponse a échoué. Veuillez réessayer.</string>
<string name="notif_success_sending">Message envoyé à %1$s!</string> <string name="notif_success_sending">Message envoyé à %1$s!</string>
<string name="msg_log_out">Logging out...</string> <!-- TODO Add proper translation-->
<string name="msg_log_out">Logging out…</string>
</resources> </resources>
...@@ -174,6 +174,10 @@ ...@@ -174,6 +174,10 @@
<string name="permission_pinning_not_allowed">पिनि करने की अनुमति नहीं है</string> <string name="permission_pinning_not_allowed">पिनि करने की अनुमति नहीं है</string>
<string name="permission_starring_not_allowed">तारांकित की अनुमति नहीं है</string> <string name="permission_starring_not_allowed">तारांकित की अनुमति नहीं है</string>
<!-- Search message -->
<!-- TODO Add proper translation-->
<string name="title_search_message">Search message</string>
<!-- Favorite/Unfavorite chat room --> <!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">पसंदीदा चैट</string> <string name="title_favorite_chat">पसंदीदा चैट</string>
<string name="title_unfavorite_chat">नापसंद चैट</string> <string name="title_unfavorite_chat">नापसंद चैट</string>
...@@ -242,6 +246,7 @@ ...@@ -242,6 +246,7 @@
<!-- Emoji message--> <!-- Emoji message-->
<string name="msg_no_recent_emoji"> कोई नया इमोजी नहीं</string> <string name="msg_no_recent_emoji"> कोई नया इमोजी नहीं</string>
<string name="alert_title_default_skin_tone">डिफ़ॉल्ट त्वचा टोन</string>
<!-- Sorting and grouping--> <!-- Sorting and grouping-->
<string name="menu_chatroom_sort">क्रम</string> <string name="menu_chatroom_sort">क्रम</string>
...@@ -263,5 +268,6 @@ ...@@ -263,5 +268,6 @@
<string name="notif_action_reply_hint">जवाब</string> <string name="notif_action_reply_hint">जवाब</string>
<string name="notif_error_sending">उत्तर विफल हुआ है। कृपया फिर से प्रयास करें।</string> <string name="notif_error_sending">उत्तर विफल हुआ है। कृपया फिर से प्रयास करें।</string>
<string name="notif_success_sending">संदेश भेजा गया %1$s!</string> <string name="notif_success_sending">संदेश भेजा गया %1$s!</string>
<string name="msg_log_out">Logging out...</string> <!-- TODO Add proper translation-->
<string name="msg_log_out">Logging out…</string>
</resources> </resources>
\ No newline at end of file
...@@ -155,7 +155,6 @@ ...@@ -155,7 +155,6 @@
// TODO:Add proper translation. // TODO:Add proper translation.
<string name="message_credentials_saved_successfully">Credentials saved successfully</string> <string name="message_credentials_saved_successfully">Credentials saved successfully</string>
<!-- Message actions --> <!-- Message actions -->
<string name="action_msg_reply">Responder</string> <string name="action_msg_reply">Responder</string>
<string name="action_msg_edit">Editar</string> <string name="action_msg_edit">Editar</string>
...@@ -176,6 +175,9 @@ ...@@ -176,6 +175,9 @@
<string name="permission_pinning_not_allowed">Pinagem não permitida</string> <string name="permission_pinning_not_allowed">Pinagem não permitida</string>
<string name="permission_starring_not_allowed">Favoritar não permitido</string> <string name="permission_starring_not_allowed">Favoritar não permitido</string>
<!-- Search message -->
<string name="title_search_message">Procurar mensagem</string>
<!-- Favorite/Unfavorite chat room --> <!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Favoritar canal</string> <string name="title_favorite_chat">Favoritar canal</string>
<string name="title_unfavorite_chat">Desfavoritar canal</string> <string name="title_unfavorite_chat">Desfavoritar canal</string>
...@@ -244,6 +246,7 @@ ...@@ -244,6 +246,7 @@
<!-- Emoji message--> <!-- Emoji message-->
<string name="msg_no_recent_emoji">Nenhum emoji recente</string> <string name="msg_no_recent_emoji">Nenhum emoji recente</string>
<string name="alert_title_default_skin_tone">Tom de pele padrão</string>
<!-- Sorting and grouping--> <!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Ordenar</string> <string name="menu_chatroom_sort">Ordenar</string>
...@@ -265,5 +268,5 @@ ...@@ -265,5 +268,5 @@
<string name="notif_action_reply_hint">RESPONDER</string> <string name="notif_action_reply_hint">RESPONDER</string>
<string name="notif_error_sending">Falha ao enviar a mensagem.</string> <string name="notif_error_sending">Falha ao enviar a mensagem.</string>
<string name="notif_success_sending">Mensagem enviada para %1$s!</string> <string name="notif_success_sending">Mensagem enviada para %1$s!</string>
<string name="msg_log_out">Deslogando...</string> <string name="msg_log_out">Deslogando</string>
</resources> </resources>
...@@ -171,6 +171,9 @@ ...@@ -171,6 +171,9 @@
<string name="permission_pinning_not_allowed">Прикрепление запрещено</string> <string name="permission_pinning_not_allowed">Прикрепление запрещено</string>
<string name="permission_starring_not_allowed">Отмечивание запрещено</string> <string name="permission_starring_not_allowed">Отмечивание запрещено</string>
<!-- Search message -->
<string name="title_search_message">Поиск сообщения</string>
<!-- Favorite/Unfavorite chat room --> <!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Добавить чат в избранное</string> <string name="title_favorite_chat">Добавить чат в избранное</string>
<string name="title_unfavorite_chat">Удалить чат из избранного</string> <string name="title_unfavorite_chat">Удалить чат из избранного</string>
...@@ -239,6 +242,7 @@ ...@@ -239,6 +242,7 @@
<!-- Emoji message--> <!-- Emoji message-->
<string name="msg_no_recent_emoji">Нет недавно используемых emoji</string> <string name="msg_no_recent_emoji">Нет недавно используемых emoji</string>
<string name="alert_title_default_skin_tone">По умолчанию тон кожи</string>
<!-- Sorting and grouping--> <!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Сортировать</string> <string name="menu_chatroom_sort">Сортировать</string>
...@@ -260,5 +264,5 @@ ...@@ -260,5 +264,5 @@
<string name="notif_action_reply_hint">ОТВЕТИТЬ</string> <string name="notif_action_reply_hint">ОТВЕТИТЬ</string>
<string name="notif_error_sending">Ошибка ответа. Пожалуйста, попробуйте еще раз.</string> <string name="notif_error_sending">Ошибка ответа. Пожалуйста, попробуйте еще раз.</string>
<string name="notif_success_sending">Сообщение отправлено %1$s!</string> <string name="notif_success_sending">Сообщение отправлено %1$s!</string>
<string name="msg_log_out">Logging out...</string> <string name="msg_log_out">Выход…</string>
</resources> </resources>
...@@ -175,6 +175,9 @@ ...@@ -175,6 +175,9 @@
<string name="permission_pinning_not_allowed">Pinning is not allowed</string> <string name="permission_pinning_not_allowed">Pinning is not allowed</string>
<string name="permission_starring_not_allowed">Starring is not allowed</string> <string name="permission_starring_not_allowed">Starring is not allowed</string>
<!-- Search message -->
<string name="title_search_message">Search message</string>
<!-- Favorite/Unfavorite chat room --> <!-- Favorite/Unfavorite chat room -->
<string name="title_favorite_chat">Favorite chat</string> <string name="title_favorite_chat">Favorite chat</string>
<string name="title_unfavorite_chat">Unfavorite chat</string> <string name="title_unfavorite_chat">Unfavorite chat</string>
...@@ -243,6 +246,7 @@ ...@@ -243,6 +246,7 @@
<!-- Emoji message--> <!-- Emoji message-->
<string name="msg_no_recent_emoji">No recent emojis</string> <string name="msg_no_recent_emoji">No recent emojis</string>
<string name="alert_title_default_skin_tone">Default skin tone</string>
<!-- Sorting and grouping--> <!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Sort</string> <string name="menu_chatroom_sort">Sort</string>
......
...@@ -9,10 +9,8 @@ ...@@ -9,10 +9,8 @@
<item name="android:statusBarColor">@color/colorPrimaryDark</item> <item name="android:statusBarColor">@color/colorPrimaryDark</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
</style>
<style name="ToolbarTheme" parent="Base.Widget.AppCompat.Toolbar"> <item name="searchViewStyle">@style/ChatRoom.SearchView</item>
<item name="android:colorAccent">@color/colorLightTheme</item>
</style> </style>
<style name="AuthenticationTheme" parent="Theme.AppCompat.NoActionBar"> <style name="AuthenticationTheme" parent="Theme.AppCompat.NoActionBar">
...@@ -75,6 +73,13 @@ ...@@ -75,6 +73,13 @@
<item name="android:paddingStart">@dimen/edit_text_margin</item> <item name="android:paddingStart">@dimen/edit_text_margin</item>
</style> </style>
<style name="ChatRoom.SearchView" parent="Widget.AppCompat.SearchView">
<item name="queryHint">@string/title_search_message</item>
<item name="searchIcon">@drawable/ic_search_white_24dp</item>
<item name="searchHintIcon">@drawable/ic_search_white_24dp</item>
<item name="closeIcon">@drawable/ic_close_white_24dp</item>
</style>
<style name="ChatRooms.Header" parent="TextAppearance.AppCompat.Headline"> <style name="ChatRooms.Header" parent="TextAppearance.AppCompat.Headline">
<item name="android:textSize">16sp</item> <item name="android:textSize">16sp</item>
</style> </style>
......
ext { ext {
versions = [ versions = [
// For project configuration
java : JavaVersion.VERSION_1_8, java : JavaVersion.VERSION_1_8,
compileSdk : 28, compileSdk : 28,
targetSdk : 28, targetSdk : 28,
minSdk : 21, minSdk : 21,
buildTools : '28.0.0-rc2', buildTools : '28.0.0-rc2',
dokka : '0.9.16',
// For app
kotlin : '1.2.51', kotlin : '1.2.51',
coroutine : '0.23.1', coroutine : '0.23.1',
dokka : '0.9.16',
// Main dependencies appCompat : '1.0.0-beta01',
appCompat : '1.0.0-alpha1', recyclerview : '1.0.0-beta01',
recyclerview : '1.0.0-alpha1', constraintLayout : '2.0.0-alpha1',
material : '1.0.0-alpha1', cardview : '1.0.0-beta01',
cardview : '1.0.0-alpha1', browser : '1.0.0-beta01',
browser : '1.0.0-alpha1', androidKtx : '1.0.0-beta01',
constraintLayout : '1.1.0',
androidKtx : '1.0.0-alpha1',
dagger : '2.16', dagger : '2.16',
exoPlayer : '2.6.0',
playServices : '15.0.0',
firebase : '15.0.0', firebase : '15.0.0',
playServices : '15.0.0',
exoPlayer : '2.6.0',
flexbox : '0.3.2',
material : '1.0.0-alpha1',
room : '2.0.0-alpha1', room : '2.0.0-alpha1',
lifecycle : '2.0.0-beta01', lifecycle : '2.0.0-beta01',
rxKotlin : '2.2.0', rxKotlin : '2.2.0',
rxAndroid : '2.0.2', rxAndroid : '2.0.2',
moshi : '1.6.0', moshi : '1.6.0',
okhttp : '3.10.0', okhttp : '3.10.0',
timber : '4.7.0', timber : '4.7.0',
threeTenABP : '1.0.5', threeTenABP : '1.0.5',
rxBinding : '2.0.0', rxBinding : '2.0.0',
fresco : '1.9.0', fresco : '1.9.0',
kotshi : '1.0.2', kotshi : '1.0.2',
frescoImageViewer : '0.5.1', frescoImageViewer : '0.5.1',
markwon : '1.0.3', markwon : '1.0.6',
aVLoadingIndicatorView: '2.1.3', aVLoadingIndicatorView: '2.1.3',
flexbox : '0.3.2',
// For wearable
wear : '2.3.0',
playServicesWearable : '15.0.1',
supportWearable : '26.1.0',
// For testing // For testing
junit : '4.12', junit : '4.12',
truth : '0.36', truth : '0.36',
espresso : '3.1.0-alpha2', espresso : '3.1.0-alpha2',
mockito : '2.10.0', mockito : '2.10.0'
//For wearable
wear : '2.3.0',
playServicesWearable : '15.0.1',
supportWearable : '26.1.0'
] ]
libraries = [ libraries = [
kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jre8:${versions.kotlin}", kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jre8:${versions.kotlin}",
...@@ -55,12 +65,9 @@ ext { ...@@ -55,12 +65,9 @@ ext {
appCompat : "androidx.appcompat:appcompat:${versions.appCompat}", appCompat : "androidx.appcompat:appcompat:${versions.appCompat}",
recyclerview : "androidx.recyclerview:recyclerview:${versions.recyclerview}", recyclerview : "androidx.recyclerview:recyclerview:${versions.recyclerview}",
material : "com.google.android.material:material:${versions.material}",
constraintlayout : "androidx.constraintlayout:constraintlayout:${versions.constraintLayout}", constraintlayout : "androidx.constraintlayout:constraintlayout:${versions.constraintLayout}",
cardview : "androidx.cardview:cardview:${versions.cardview}", cardview : "androidx.cardview:cardview:${versions.cardview}",
flexbox : "com.google.android:flexbox:${versions.flexbox}",
browser : "androidx.browser:browser:${versions.browser}", browser : "androidx.browser:browser:${versions.browser}",
androidKtx : "androidx.core:core-ktx:${versions.androidKtx}", androidKtx : "androidx.core:core-ktx:${versions.androidKtx}",
dagger : "com.google.dagger:dagger:${versions.dagger}", dagger : "com.google.dagger:dagger:${versions.dagger}",
...@@ -70,6 +77,8 @@ ext { ...@@ -70,6 +77,8 @@ ext {
fcm : "com.google.firebase:firebase-messaging:${versions.firebase}", fcm : "com.google.firebase:firebase-messaging:${versions.firebase}",
playServicesAuth : "com.google.android.gms:play-services-auth:${versions.playServices}", playServicesAuth : "com.google.android.gms:play-services-auth:${versions.playServices}",
exoPlayer : "com.google.android.exoplayer:exoplayer:${versions.exoPlayer}", exoPlayer : "com.google.android.exoplayer:exoplayer:${versions.exoPlayer}",
flexbox : "com.google.android:flexbox:${versions.flexbox}",
material : "com.google.android.material:material:${versions.material}",
room : "androidx.room:room-runtime:${versions.room}", room : "androidx.room:room-runtime:${versions.room}",
roomProcessor : "androidx.room:room-compiler:${versions.room}", roomProcessor : "androidx.room:room-compiler:${versions.room}",
...@@ -103,7 +112,7 @@ ext { ...@@ -103,7 +112,7 @@ ext {
aVLoadingIndicatorView: "com.wang.avi:library:${versions.aVLoadingIndicatorView}", aVLoadingIndicatorView: "com.wang.avi:library:${versions.aVLoadingIndicatorView}",
//For the wear app // For wearable
wearable : "com.google.android.support:wearable:${versions.wear}", wearable : "com.google.android.support:wearable:${versions.wear}",
playServicesWearable : "com.google.android.gms:play-services-wearable:${versions.playServicesWearable}", playServicesWearable : "com.google.android.gms:play-services-wearable:${versions.playServicesWearable}",
percentLayout : "com.android.support:percent:${versions.supportWearable}", percentLayout : "com.android.support:percent:${versions.supportWearable}",
......
...@@ -29,6 +29,8 @@ dependencies { ...@@ -29,6 +29,8 @@ dependencies {
implementation libraries.androidKtx implementation libraries.androidKtx
implementation libraries.appCompat implementation libraries.appCompat
implementation libraries.kotlin implementation libraries.kotlin
implementation libraries.coroutines
implementation libraries.coroutinesAndroid
implementation libraries.constraintlayout implementation libraries.constraintlayout
implementation libraries.recyclerview implementation libraries.recyclerview
implementation libraries.material implementation libraries.material
......
...@@ -22,7 +22,7 @@ class ComposerEditText : AppCompatEditText { ...@@ -22,7 +22,7 @@ class ComposerEditText : AppCompatEditText {
override fun dispatchKeyEventPreIme(event: KeyEvent): Boolean { override fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK) { if (event.keyCode == KeyEvent.KEYCODE_BACK) {
val state = getKeyDispatcherState() val state = keyDispatcherState
if (state != null) { if (state != null) {
if (event.action == KeyEvent.ACTION_DOWN) { if (event.action == KeyEvent.ACTION_DOWN) {
state.startTracking(event, this) state.startTracking(event, this)
......
...@@ -6,5 +6,7 @@ data class Emoji( ...@@ -6,5 +6,7 @@ data class Emoji(
val unicode: String, val unicode: String,
val keywords: List<String>, val keywords: List<String>,
val category: String, val category: String,
val count: Int = 0 val count: Int = 0,
val siblings: MutableCollection<Emoji> = mutableListOf(),
val fitzpatrick: Fitzpatrick = Fitzpatrick.Default
) )
\ No newline at end of file
package chat.rocket.android.emoji package chat.rocket.android.emoji
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
...@@ -9,23 +10,32 @@ import android.view.View ...@@ -9,23 +10,32 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.graphics.drawable.DrawableCompat
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
class EmojiKeyboardPopup( class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow(context, view) {
context: Context,
view: View
) : OverKeyboardPopupWindow(context, view) {
private lateinit var viewPager: ViewPager private lateinit var viewPager: ViewPager
private lateinit var tabLayout: TabLayout private lateinit var tabLayout: TabLayout
private lateinit var searchView: View private lateinit var searchView: View
private lateinit var backspaceView: View private lateinit var backspaceView: View
private lateinit var parentContainer: ViewGroup private lateinit var parentContainer: ViewGroup
private lateinit var changeColorView: View
private lateinit var adapter: EmojiPagerAdapter
var listener: EmojiKeyboardListener? = null var listener: EmojiKeyboardListener? = null
@SuppressLint("InflateParams")
override fun onCreateView(inflater: LayoutInflater): View { override fun onCreateView(inflater: LayoutInflater): View {
val view = inflater.inflate(R.layout.emoji_keyboard, null) val view = inflater.inflate(R.layout.emoji_keyboard, null)
parentContainer = view.findViewById(R.id.emoji_keyboard_container) parentContainer = view.findViewById(R.id.emoji_keyboard_container)
...@@ -33,6 +43,7 @@ class EmojiKeyboardPopup( ...@@ -33,6 +43,7 @@ class EmojiKeyboardPopup(
searchView = view.findViewById(R.id.emoji_search) searchView = view.findViewById(R.id.emoji_search)
backspaceView = view.findViewById(R.id.emoji_backspace) backspaceView = view.findViewById(R.id.emoji_backspace)
tabLayout = view.findViewById(R.id.tabs) tabLayout = view.findViewById(R.id.tabs)
changeColorView = view.findViewById(R.id.color_change_view)
tabLayout.setupWithViewPager(viewPager) tabLayout.setupWithViewPager(viewPager)
return view return view
} }
...@@ -44,11 +55,97 @@ class EmojiKeyboardPopup( ...@@ -44,11 +55,97 @@ class EmojiKeyboardPopup(
private fun setupBottomBar() { private fun setupBottomBar() {
searchView.setOnClickListener { searchView.setOnClickListener {
//TODO: search not yet implemented
} }
backspaceView.setOnClickListener { backspaceView.setOnClickListener {
listener?.onNonEmojiKeyPressed(KeyEvent.KEYCODE_BACK) listener?.onNonEmojiKeyPressed(KeyEvent.KEYCODE_BACK)
} }
changeColorView.setOnClickListener {
showSkinToneChooser()
}
val sharedPreferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
sharedPreferences.getString(PREF_EMOJI_SKIN_TONE, "")?.let {
changeSkinTone(Fitzpatrick.valueOf(it))
}
}
private fun showSkinToneChooser() {
val view = LayoutInflater.from(context).inflate(R.layout.color_select_popup, null)
val dialog = AlertDialog.Builder(context, R.style.Dialog)
.setView(view)
.setTitle(context.getString(R.string.alert_title_default_skin_tone))
.setCancelable(true)
.create()
view.findViewById<TextView>(R.id.default_tone_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.Default)
}
view.findViewById<TextView>(R.id.light_tone_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.LightTone)
}
view.findViewById<TextView>(R.id.medium_light_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumLightTone)
}
view.findViewById<TextView>(R.id.medium_tone_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumTone)
}
view.findViewById<TextView>(R.id.medium_dark_tone_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.MediumDarkTone)
}
view.findViewById<TextView>(R.id.dark_tone_text).also {
it.text = EmojiParser.parse(it.text)
}.setOnClickListener {
dialog.dismiss()
changeSkinTone(Fitzpatrick.DarkTone)
}
dialog.show()
}
private fun changeSkinTone(tone: Fitzpatrick) {
val drawable = ContextCompat.getDrawable(context, R.drawable.color_change_circle)!!
val wrappedDrawable = DrawableCompat.wrap(drawable)
DrawableCompat.setTint(wrappedDrawable, getFitzpatrickColor(tone))
(changeColorView as ImageView).setImageDrawable(wrappedDrawable)
adapter.setFitzpatrick(tone)
}
@ColorInt
private fun getFitzpatrickColor(tone: Fitzpatrick): Int {
val sharedPreferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
sharedPreferences.edit {
putString(PREF_EMOJI_SKIN_TONE, tone.type)
}
return when (tone) {
Fitzpatrick.Default -> ContextCompat.getColor(context, R.color.tone_default)
Fitzpatrick.LightTone -> ContextCompat.getColor(context, R.color.tone_light)
Fitzpatrick.MediumLightTone -> ContextCompat.getColor(context, R.color.tone_medium_light)
Fitzpatrick.MediumTone -> ContextCompat.getColor(context, R.color.tone_medium)
Fitzpatrick.MediumDarkTone -> ContextCompat.getColor(context, R.color.tone_medium_dark)
Fitzpatrick.DarkTone -> ContextCompat.getColor(context, R.color.tone_dark)
}
} }
private fun setupViewPager() { private fun setupViewPager() {
...@@ -64,12 +161,14 @@ class EmojiKeyboardPopup( ...@@ -64,12 +161,14 @@ class EmojiKeyboardPopup(
} }
} }
viewPager.adapter = CategoryPagerAdapter(object : EmojiKeyboardListener { adapter = EmojiPagerAdapter(object : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) { override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji) EmojiRepository.addToRecents(emoji)
callback.onEmojiAdded(emoji) callback.onEmojiAdded(emoji)
} }
}) })
viewPager.offscreenPageLimit = EmojiCategory.values().size
viewPager.adapter = adapter
for (category in EmojiCategory.values()) { for (category in EmojiCategory.values()) {
val tab = tabLayout.getTabAt(category.ordinal) val tab = tabLayout.getTabAt(category.ordinal)
...@@ -79,14 +178,18 @@ class EmojiKeyboardPopup( ...@@ -79,14 +178,18 @@ class EmojiKeyboardPopup(
textView.setImageResource(category.resourceIcon()) textView.setImageResource(category.resourceIcon())
} }
val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else val currentTab = if (EmojiRepository.getRecents().isEmpty()) {
EmojiCategory.PEOPLE.ordinal
} else {
EmojiCategory.RECENTS.ordinal EmojiCategory.RECENTS.ordinal
}
viewPager.currentItem = currentTab viewPager.currentItem = currentTab
} }
} }
class EmojiTextWatcher(private val editor: EditText) : TextWatcher { class EmojiTextWatcher(private val editor: EditText) : TextWatcher {
@Volatile private var emojiToRemove = mutableListOf<EmojiTypefaceSpan>() @Volatile
private var emojiToRemove = mutableListOf<EmojiTypefaceSpan>()
override fun afterTextChanged(s: Editable) { override fun afterTextChanged(s: Editable) {
val message = editor.editableText val message = editor.editableText
...@@ -128,8 +231,4 @@ class EmojiKeyboardPopup( ...@@ -128,8 +231,4 @@ class EmojiKeyboardPopup(
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
} }
} }
companion object {
const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
}
} }
\ No newline at end of file
package chat.rocket.android.emoji package chat.rocket.android.emoji
import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned import android.text.Spanned
...@@ -11,11 +12,12 @@ class EmojiParser { ...@@ -11,11 +12,12 @@ class EmojiParser {
* Spannable. * Spannable.
* *
* @param text The text to parse * @param text The text to parse
* @param factory Optional. A [Spannable.Factory] instance to reuse when creating [Spannable].
* @return A rendered Spannable containing any supported emoji. * @return A rendered Spannable containing any supported emoji.
*/ */
fun parse(text: CharSequence): CharSequence { fun parse(text: CharSequence, factory: Spannable.Factory? = null): CharSequence {
val unicodedText = EmojiRepository.shortnameToUnicode(text, true) val unicodedText = EmojiRepository.shortnameToUnicode(text, true)
val spannable = SpannableString.valueOf(unicodedText) val spannable = factory?.newSpannable(unicodedText) ?: SpannableString.valueOf(unicodedText)
val typeface = EmojiRepository.cachedTypeface val typeface = EmojiRepository.cachedTypeface
// Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font. // Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font.
val length = spannable.length val length = spannable.length
......
...@@ -3,18 +3,23 @@ package chat.rocket.android.emoji ...@@ -3,18 +3,23 @@ package chat.rocket.android.emoji
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import com.google.android.material.tabs.TabLayout
import androidx.viewpager.widget.ViewPager
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.EmojiPagerAdapter
import chat.rocket.android.emoji.internal.PREF_EMOJI_SKIN_TONE
import kotlinx.android.synthetic.main.emoji_picker.* import kotlinx.android.synthetic.main.emoji_picker.*
class EmojiPickerPopup(context: Context) : Dialog(context) { class EmojiPickerPopup(context: Context) : Dialog(context) {
var listener: EmojiKeyboardListener? = null var listener: EmojiKeyboardListener? = null
private lateinit var adapter: EmojiPagerAdapter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -35,7 +40,7 @@ class EmojiPickerPopup(context: Context) : Dialog(context) { ...@@ -35,7 +40,7 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
} }
private fun setupViewPager() { private fun setupViewPager() {
pager_categories.adapter = CategoryPagerAdapter(object : EmojiKeyboardListener { adapter = EmojiPagerAdapter(object : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) { override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji) EmojiRepository.addToRecents(emoji)
dismiss() dismiss()
...@@ -43,6 +48,14 @@ class EmojiPickerPopup(context: Context) : Dialog(context) { ...@@ -43,6 +48,14 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
} }
}) })
val sharedPreferences = context.getSharedPreferences("emoji", Context.MODE_PRIVATE)
sharedPreferences.getString(PREF_EMOJI_SKIN_TONE, "")?.let {
changeSkinTone(Fitzpatrick.valueOf(it))
}
pager_categories.adapter = adapter
pager_categories.offscreenPageLimit = EmojiCategory.values().size
for (category in EmojiCategory.values()) { for (category in EmojiCategory.values()) {
val tab = tabs.getTabAt(category.ordinal) val tab = tabs.getTabAt(category.ordinal)
val tabView = LayoutInflater.from(context).inflate(R.layout.emoji_picker_tab, null) val tabView = LayoutInflater.from(context).inflate(R.layout.emoji_picker_tab, null)
...@@ -51,8 +64,15 @@ class EmojiPickerPopup(context: Context) : Dialog(context) { ...@@ -51,8 +64,15 @@ class EmojiPickerPopup(context: Context) : Dialog(context) {
textView.setImageResource(category.resourceIcon()) textView.setImageResource(category.resourceIcon())
} }
val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else val currentTab = if (EmojiRepository.getRecents().isEmpty()) {
EmojiCategory.PEOPLE.ordinal
} else {
EmojiCategory.RECENTS.ordinal EmojiCategory.RECENTS.ordinal
}
pager_categories.currentItem = currentTab pager_categories.currentItem = currentTab
} }
private fun changeSkinTone(tone: Fitzpatrick) {
adapter.setFitzpatrick(tone)
}
} }
\ No newline at end of file
...@@ -3,6 +3,12 @@ package chat.rocket.android.emoji ...@@ -3,6 +3,12 @@ package chat.rocket.android.emoji
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Typeface import android.graphics.Typeface
import android.os.SystemClock
import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.emoji.internal.PREF_EMOJI_RECENTS
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.experimental.yield
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.BufferedReader import java.io.BufferedReader
...@@ -10,8 +16,11 @@ import java.io.InputStream ...@@ -10,8 +16,11 @@ import java.io.InputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.coroutines.experimental.buildSequence
object EmojiRepository { object EmojiRepository {
private val FITZPATRICK_REGEX = "(.*)_(tone[0-9]):".toRegex(RegexOption.IGNORE_CASE)
private val shortNameToUnicode = HashMap<String, String>() private val shortNameToUnicode = HashMap<String, String>()
private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):") private val SHORTNAME_PATTERN = Pattern.compile(":([-+\\w]+):")
private val ALL_EMOJIS = mutableListOf<Emoji>() private val ALL_EMOJIS = mutableListOf<Emoji>()
...@@ -24,9 +33,9 @@ object EmojiRepository { ...@@ -24,9 +33,9 @@ object EmojiRepository {
cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf") cachedTypeface = Typeface.createFromAsset(context.assets, "fonts/emojione-android.ttf")
val stream = context.assets.open(path) val stream = context.assets.open(path)
val emojis = loadEmojis(stream) val emojis = loadEmojis(stream)
emojis.forEach { emojis.forEach { emoji ->
val unicodeIntList = mutableListOf<Int>() val unicodeIntList = mutableListOf<Int>()
it.unicode.split("-").forEach { emoji.unicode.split("-").forEach {
val value = it.toInt(16) val value = it.toInt(16)
if (value >= 0x10000) { if (value >= 0x10000) {
val surrogatePair = calculateSurrogatePairs(value) val surrogatePair = calculateSurrogatePairs(value)
...@@ -38,20 +47,40 @@ object EmojiRepository { ...@@ -38,20 +47,40 @@ object EmojiRepository {
} }
val unicodeIntArray = unicodeIntList.toIntArray() val unicodeIntArray = unicodeIntList.toIntArray()
val unicode = String(unicodeIntArray, 0, unicodeIntArray.size) val unicode = String(unicodeIntArray, 0, unicodeIntArray.size)
ALL_EMOJIS.add(it.copy(unicode = unicode)) val emojiWithUnicode = emoji.copy(unicode = unicode)
if (hasFitzpatrick(emoji.shortname)) {
val matchResult = FITZPATRICK_REGEX.find(emoji.shortname)
val prefix = matchResult!!.groupValues[1] + ":"
val fitzpatrick = Fitzpatrick.valueOf(matchResult.groupValues[2])
val defaultEmoji = ALL_EMOJIS.firstOrNull { it.shortname == prefix }
val emojiWithFitzpatrick = emojiWithUnicode.copy(fitzpatrick = fitzpatrick)
if (defaultEmoji != null) {
defaultEmoji.siblings.add(emojiWithFitzpatrick)
} else {
// This emoji doesn't have a default tone, ie. :man_in_business_suit_levitating_tone1:
// In this case, the default emoji becomes the first toned one.
ALL_EMOJIS.add(emojiWithFitzpatrick)
}
} else {
ALL_EMOJIS.add(emojiWithUnicode)
}
shortNameToUnicode.apply { shortNameToUnicode.apply {
put(it.shortname, unicode) put(emoji.shortname, unicode)
it.shortnameAlternates.forEach { alternate -> put(alternate, unicode) } emoji.shortnameAlternates.forEach { alternate -> put(alternate, unicode) }
} }
} }
} }
private fun hasFitzpatrick(shortname: String): Boolean {
return FITZPATRICK_REGEX matches shortname
}
/** /**
* Get all loaded emojis as list of Emoji objects. * Get all loaded emojis as list of Emoji objects.
* *
* @return All emojis for all categories. * @return All emojis for all categories.
*/ */
fun getAll() = ALL_EMOJIS internal fun getAll() = ALL_EMOJIS
/** /**
* Get all emojis for a given category. * Get all emojis for a given category.
...@@ -60,10 +89,19 @@ object EmojiRepository { ...@@ -60,10 +89,19 @@ object EmojiRepository {
* *
* @return All emoji from specified category * @return All emoji from specified category
*/ */
fun getEmojisByCategory(category: EmojiCategory): List<Emoji> { internal fun getEmojisByCategory(category: EmojiCategory): List<Emoji> {
return ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() } return ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() }
} }
internal fun getEmojiSequenceByCategory(category: EmojiCategory): Sequence<Emoji> {
val list = ALL_EMOJIS.filter { it.category.toLowerCase() == category.name.toLowerCase() }
return buildSequence{
list.forEach {
yield(it)
}
}
}
/** /**
* Get the emoji given by a specified shortname. Returns null if can't find any. * Get the emoji given by a specified shortname. Returns null if can't find any.
* *
...@@ -71,21 +109,21 @@ object EmojiRepository { ...@@ -71,21 +109,21 @@ object EmojiRepository {
* *
* @return Emoji given by shortname or null * @return Emoji given by shortname or null
*/ */
fun getEmojiByShortname(shortname: String) = ALL_EMOJIS.firstOrNull { it.shortname == shortname } internal fun getEmojiByShortname(shortname: String) = ALL_EMOJIS.firstOrNull { it.shortname == shortname }
/** /**
* Add an emoji to the Recents category. * Add an emoji to the Recents category.
*/ */
fun addToRecents(emoji: Emoji) { internal fun addToRecents(emoji: Emoji) {
val emojiShortname = emoji.shortname val emojiShortname = emoji.shortname
val recentsJson = JSONObject(preferences.getString(EmojiKeyboardPopup.PREF_EMOJI_RECENTS, "{}")) val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}"))
if (recentsJson.has(emojiShortname)) { if (recentsJson.has(emojiShortname)) {
val useCount = recentsJson.getInt(emojiShortname) val useCount = recentsJson.getInt(emojiShortname)
recentsJson.put(emojiShortname, useCount + 1) recentsJson.put(emojiShortname, useCount + 1)
} else { } else {
recentsJson.put(emojiShortname, 1) recentsJson.put(emojiShortname, 1)
} }
preferences.edit().putString(EmojiKeyboardPopup.PREF_EMOJI_RECENTS, recentsJson.toString()).apply() preferences.edit().putString(PREF_EMOJI_RECENTS, recentsJson.toString()).apply()
} }
/** /**
...@@ -93,9 +131,9 @@ object EmojiRepository { ...@@ -93,9 +131,9 @@ object EmojiRepository {
* *
* @return All recent emojis ordered by usage. * @return All recent emojis ordered by usage.
*/ */
fun getRecents(): List<Emoji> { internal fun getRecents(): List<Emoji> {
val list = mutableListOf<Emoji>() val list = mutableListOf<Emoji>()
val recentsJson = JSONObject(preferences.getString(EmojiKeyboardPopup.PREF_EMOJI_RECENTS, "{}")) val recentsJson = JSONObject(preferences.getString(PREF_EMOJI_RECENTS, "{}"))
for (shortname in recentsJson.keys()) { for (shortname in recentsJson.keys()) {
val emoji = getEmojiByShortname(shortname) val emoji = getEmojiByShortname(shortname)
emoji?.let { emoji?.let {
......
...@@ -16,23 +16,6 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac ...@@ -16,23 +16,6 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac
} }
private fun applyCustomTypeFace(paint: Paint, tf: Typeface) { private fun applyCustomTypeFace(paint: Paint, tf: Typeface) {
val oldStyle: Int
val old = paint.typeface
if (old == null) {
oldStyle = 0
} else {
oldStyle = old.style
}
val fake = oldStyle and tf.style.inv()
if (fake and Typeface.BOLD != 0) {
paint.isFakeBoldText = true
}
if (fake and Typeface.ITALIC != 0) {
paint.textSkewX = -0.25f
}
paint.typeface = tf paint.typeface = tf
} }
} }
\ No newline at end of file
package chat.rocket.android.emoji
/**
* Taken the Fitzpatrick scale as reference and adapted to be used with emojione.
*
* @see <a href="https://en.wikipedia.org/wiki/Fitzpatrick_scale">https://en.wikipedia.org/wiki/Fitzpatrick_scale</a>
*/
sealed class Fitzpatrick(val type: String) {
object Default: Fitzpatrick("")
object LightTone: Fitzpatrick("tone1")
object MediumLightTone: Fitzpatrick("tone2")
object MediumTone: Fitzpatrick("tone3")
object MediumDarkTone: Fitzpatrick("tone4")
object DarkTone: Fitzpatrick("tone5")
companion object {
fun valueOf(type: String): Fitzpatrick {
return when(type) {
"" -> Default
"tone1" -> LightTone
"tone2" -> MediumLightTone
"tone3" -> MediumTone
"tone4" -> MediumDarkTone
"tone5" -> DarkTone
else -> throw IllegalArgumentException("Fitzpatrick type '$type' is invalid")
}
}
}
}
\ No newline at end of file
package chat.rocket.android.emoji package chat.rocket.android.emoji.internal
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned import android.text.Spanned
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.EmojiTypefaceSpan
import chat.rocket.android.emoji.R
enum class EmojiCategory { internal enum class EmojiCategory {
RECENTS { RECENTS {
override fun resourceIcon() = R.drawable.ic_emoji_recents override fun resourceIcon() = R.drawable.ic_emoji_recents
......
package chat.rocket.android.emoji package chat.rocket.android.emoji.internal
import android.text.Spannable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.PagerAdapter import androidx.viewpager.widget.PagerAdapter
import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiKeyboardListener
import chat.rocket.android.emoji.EmojiParser
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.Fitzpatrick
import chat.rocket.android.emoji.R
import kotlinx.android.synthetic.main.emoji_category_layout.view.* import kotlinx.android.synthetic.main.emoji_category_layout.view.*
import java.util.* import kotlinx.android.synthetic.main.emoji_row_item.view.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener) : PagerAdapter() {
internal class EmojiPagerAdapter(private val listener: EmojiKeyboardListener) : PagerAdapter() {
private val adapters = hashMapOf<EmojiCategory, EmojiAdapter>()
private var fitzpatrick: Fitzpatrick = Fitzpatrick.Default
override fun isViewFromObject(view: View, obj: Any): Boolean { override fun isViewFromObject(view: View, obj: Any): Boolean {
return view == obj return view == obj
...@@ -23,21 +36,28 @@ internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener) ...@@ -23,21 +36,28 @@ internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener)
.inflate(R.layout.emoji_category_layout, container, false) .inflate(R.layout.emoji_category_layout, container, false)
with(view) { with(view) {
val layoutManager = GridLayoutManager(context, 8) val layoutManager = GridLayoutManager(context, 8)
val adapter = EmojiAdapter(layoutManager.spanCount, listener)
val category = EmojiCategory.values()[position] val category = EmojiCategory.values()[position]
val emojis = if (category != EmojiCategory.RECENTS) {
EmojiRepository.getEmojisByCategory(category)
} else {
EmojiRepository.getRecents()
}
val recentEmojiSize = EmojiRepository.getRecents().size
text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0
adapter.addEmojis(emojis)
emoji_recycler_view.layoutManager = layoutManager emoji_recycler_view.layoutManager = layoutManager
emoji_recycler_view.itemAnimator = DefaultItemAnimator() emoji_recycler_view.setRecycledViewPool(RecyclerView.RecycledViewPool())
emoji_recycler_view.adapter = adapter
emoji_recycler_view.isNestedScrollingEnabled = false
container.addView(view) container.addView(view)
launch(UI) {
val emojis = if (category != EmojiCategory.RECENTS) {
EmojiRepository.getEmojiSequenceByCategory(category)
} else {
sequenceOf(*EmojiRepository.getRecents().toTypedArray())
}
val recentEmojiSize = EmojiRepository.getRecents().size
text_no_recent_emoji.isVisible = category == EmojiCategory.RECENTS && recentEmojiSize == 0
if (adapters[category] == null) {
val adapter = EmojiAdapter(listener = listener)
emoji_recycler_view.adapter = adapter
adapters[category] = adapter
adapter.addEmojisFromSequence(emojis)
}
adapters[category]!!.setFitzpatrick(fitzpatrick)
}
} }
return view return view
} }
...@@ -50,25 +70,58 @@ internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener) ...@@ -50,25 +70,58 @@ internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener)
override fun getPageTitle(position: Int) = EmojiCategory.values()[position].textIcon() override fun getPageTitle(position: Int) = EmojiCategory.values()[position].textIcon()
override fun getItemPosition(`object`: Any): Int {
return POSITION_NONE
}
fun setFitzpatrick(fitzpatrick: Fitzpatrick) {
this.fitzpatrick = fitzpatrick
for (entry in adapters.entries) {
if (entry.key != EmojiCategory.RECENTS) {
entry.value.setFitzpatrick(fitzpatrick)
}
}
}
class EmojiAdapter( class EmojiAdapter(
private val spanCount: Int, private var fitzpatrick: Fitzpatrick = Fitzpatrick.Default,
private val listener: EmojiKeyboardListener private val listener: EmojiKeyboardListener
) : RecyclerView.Adapter<EmojiRowViewHolder>() { ) : RecyclerView.Adapter<EmojiRowViewHolder>() {
private var emojis = Collections.emptyList<Emoji>() private val emojis = mutableListOf<Emoji>()
fun addEmojis(emojis: List<Emoji>) { fun addEmojis(emojis: List<Emoji>) {
this.emojis = emojis this.emojis.clear()
notifyItemRangeInserted(0, emojis.size) this.emojis.addAll(emojis)
notifyDataSetChanged()
}
suspend fun addEmojisFromSequence(emojiSequence: Sequence<Emoji>) {
withContext(CommonPool) {
emojiSequence.forEachIndexed { index, emoji ->
withContext(UI) {
emojis.add(emoji)
notifyItemInserted(index)
}
}
}
}
fun setFitzpatrick(fitzpatrick: Fitzpatrick) {
this.fitzpatrick = fitzpatrick
notifyDataSetChanged()
} }
override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) { override fun onBindViewHolder(holder: EmojiRowViewHolder, position: Int) {
holder.bind(emojis[position]) val emoji = emojis[position]
holder.bind(
emoji.siblings.find { it.fitzpatrick == fitzpatrick } ?: emoji
)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiRowViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiRowViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.emoji_row_item, parent, false) val view = LayoutInflater.from(parent.context).inflate(R.layout.emoji_row_item, parent, false)
return EmojiRowViewHolder(view, itemCount, spanCount, listener) return EmojiRowViewHolder(view, listener)
} }
override fun getItemCount(): Int = emojis.size override fun getItemCount(): Int = emojis.size
...@@ -76,25 +129,30 @@ internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener) ...@@ -76,25 +129,30 @@ internal class CategoryPagerAdapter(private val listener: EmojiKeyboardListener)
class EmojiRowViewHolder( class EmojiRowViewHolder(
itemView: View, itemView: View,
private val itemCount: Int,
private val spanCount: Int,
private val listener: EmojiKeyboardListener private val listener: EmojiKeyboardListener
) : RecyclerView.ViewHolder(itemView) { ) : RecyclerView.ViewHolder(itemView) {
private val emojiView: TextView = itemView.findViewById(R.id.emoji)
fun bind(emoji: Emoji) { fun bind(emoji: Emoji) {
val context = itemView.context with(itemView) {
emojiView.text = EmojiParser.parse(emoji.unicode) val parsedUnicode = unicodeCache[emoji.unicode]
val remainder = itemCount % spanCount emoji_view.setSpannableFactory(spannableFactory)
val lastLineItemCount = if (remainder == 0) spanCount else remainder emoji_view.text = if (parsedUnicode == null) {
val paddingBottom = context.resources.getDimensionPixelSize(R.dimen.picker_padding_bottom) EmojiParser.parse(emoji.unicode, spannableFactory).let {
if (adapterPosition >= itemCount - lastLineItemCount) { unicodeCache[emoji.unicode] = it
itemView.setPadding(0, 0, 0, paddingBottom) it
} }
itemView.setOnClickListener { } else {
listener.onEmojiAdded(emoji) parsedUnicode
}
itemView.setOnClickListener {
listener.onEmojiAdded(emoji)
}
} }
} }
companion object {
private val spannableFactory = Spannable.Factory()
private val unicodeCache = mutableMapOf<CharSequence, CharSequence>()
}
} }
} }
\ No newline at end of file
package chat.rocket.android.emoji.internal
internal const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
internal const val PREF_EMOJI_SKIN_TONE = "PREF_EMOJI_SKIN_TONE"
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/tone_default" />
<size
android:width="24dp"
android:height="24dp" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/picker_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorWhite"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabBackground="@color/whitesmoke"
app:tabGravity="fill"
app:tabMode="scrollable" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/pager_categories"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:background="@color/colorWhite" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/default_tone_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="👌"
android:textSize="32sp"
android:tint="@color/tone_default"
app:layout_constraintEnd_toStartOf="@+id/light_tone_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle" />
<TextView
android:id="@+id/light_tone_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:text="👌🏻"
android:textSize="32sp"
android:tint="@color/tone_light"
app:layout_constraintEnd_toStartOf="@+id/medium_light_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/default_tone_text"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
<TextView
android:id="@+id/medium_light_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="👌🏼"
android:textSize="32sp"
android:tint="@color/tone_medium_light"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/light_tone_text"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
<TextView
android:id="@+id/medium_tone_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="👌🏽"
android:textSize="32sp"
android:tint="@color/tone_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/medium_dark_tone_text"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/default_tone_text"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
<TextView
android:id="@+id/medium_dark_tone_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="16dp"
android:text="👌🏾"
android:textSize="32sp"
android:tint="@color/tone_medium_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/dark_tone_text"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/medium_tone_text"
app:layout_constraintTop_toBottomOf="@+id/light_tone_text"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
<TextView
android:id="@+id/dark_tone_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="👌🏿"
android:textSize="32sp"
android:tint="@color/tone_dark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/medium_dark_tone_text"
app:layout_constraintTop_toBottomOf="@+id/medium_light_text"
app:srcCompat="@drawable/color_change_circle"
tools:text="👌" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emoji_keyboard_container" android:id="@+id/emoji_keyboard_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
android:background="@color/colorWhite"> android:background="@color/colorWhite">
<View <View
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/emoji_actions_container" android:id="@+id/emoji_actions_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
...@@ -36,29 +36,40 @@ ...@@ -36,29 +36,40 @@
app:layout_constraintStart_toStartOf="parent"> app:layout_constraintStart_toStartOf="parent">
<ImageView <ImageView
android:id="@+id/emoji_search" android:id="@+id/color_change_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:padding="8dp"
android:layout_centerVertical="true" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/color_change_circle" />
<ImageView
android:id="@+id/emoji_search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/whitesmoke" android:background="@color/whitesmoke"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:padding="8dp" android:padding="8dp"
android:src="@drawable/ic_search_gray_24px" android:src="@drawable/ic_search_gray_24px"
android:visibility="invisible" /> android:visibility="invisible"
app:layout_constraintEnd_toStartOf="@+id/emoji_backspace"
app:layout_constraintStart_toEndOf="@+id/color_change_view" />
<ImageView <ImageView
android:id="@+id/emoji_backspace" android:id="@+id/emoji_backspace"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@color/whitesmoke" android:background="@color/whitesmoke"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:padding="8dp" android:padding="8dp"
android:src="@drawable/ic_backspace_gray_24dp" /> app:layout_constraintBottom_toBottomOf="parent"
</RelativeLayout> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_backspace_gray_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:id="@+id/emoji_view"
android:layout_height="wrap_content" style="@style/TextAppearance.AppCompat.Title"
android:layout_gravity="center" android:layout_width="48dp"
android:layout_height="48dp"
android:foreground="?selectableItemBackground" android:foreground="?selectableItemBackground"
android:gravity="center"> android:textColor="#000000"
android:textSize="26sp"
<TextView tools:text="😀" />
android:id="@+id/emoji"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textColor="#000000"
android:textSize="26sp"
tools:text="😀" />
</FrameLayout>
\ No newline at end of file
...@@ -7,4 +7,12 @@ ...@@ -7,4 +7,12 @@
<color name="colorEmojiIcon">#FF767676</color> <color name="colorEmojiIcon">#FF767676</color>
<color name="colorDividerMessageComposer">#D8D8D8</color> <color name="colorDividerMessageComposer">#D8D8D8</color>
<!-- Skin tone colors -->
<color name="tone_default">#fdcb45</color>
<color name="tone_light">#fcd6b4</color>
<color name="tone_medium_light">#ecc0a1</color>
<color name="tone_medium">#ba8b6f</color>
<color name="tone_medium_dark">#926b4f</color>
<color name="tone_dark">#614833</color>
</resources> </resources>
<resources> <resources>
<string name="msg_no_recent_emoji">No recent emoji</string> <string name="msg_no_recent_emoji">No recent emoji</string>
<string name="alert_title_default_skin_tone">Default skin tone</string>
</resources> </resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Dialog" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="android:windowMinWidthMajor">80%</item>
<item name="android:windowMinWidthMinor">80%</item>
</style>
</resources>
\ No newline at end of file
...@@ -29,6 +29,10 @@ dependencies { ...@@ -29,6 +29,10 @@ dependencies {
implementation libraries.coroutines implementation libraries.coroutines
implementation libraries.coroutinesAndroid implementation libraries.coroutinesAndroid
implementation libraries.appCompat
implementation libraries.rxBinding
// TODO This is a dependency from the core module, but the util module are unable to get that dependencies. Check why it is occurring since transitive is enable by default // TODO This is a dependency from the core module, but the util module are unable to get that dependencies. Check why it is occurring since transitive is enable by default
implementation libraries.lifecycleExtensions implementation libraries.lifecycleExtensions
kapt libraries.lifecycleCompiler kapt libraries.lifecycleCompiler
......
package chat.rocket.android.util.extensions package chat.rocket.android.util.extension
import android.widget.EditText import android.widget.EditText
import com.jakewharton.rxbinding2.widget.RxTextView import com.jakewharton.rxbinding2.widget.RxTextView
......
package chat.rocket.android.util.extension
import androidx.appcompat.widget.SearchView
fun SearchView.onQueryTextListener(queryListener: (String) -> Unit) {
return this.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
queryListener(query)
return true
}
override fun onQueryTextChange(newText: String): Boolean {
queryListener(newText)
return true
}
})
}
\ 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