Commit 3e8ef19b authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Merge branch 'develop' of github.com:RocketChat/Rocket.Chat.Android into new/active-users

parents 81a4bae4 7b7997d5
......@@ -17,10 +17,11 @@
<application
android:name=".app.RocketChatApplication"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config">
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true">
<activity
......@@ -34,6 +35,19 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="auth"
android:scheme="rocketchat" />
<data
android:host="go.rocket.chat"
android:path="/auth"
android:scheme="https" />
</intent-filter>
</activity>
<activity
......@@ -102,13 +116,18 @@
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<service
android:name=".chatroom.service.MessageService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<meta-data
android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
<activity android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme"/>
<activity
android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme" />
</application>
</manifest>
package chat.rocket.android.authentication.domain.model
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
@SuppressLint("ParcelCreator")
@Parcelize
data class LoginDeepLinkInfo(
val url: String,
val userId: String,
val token: String
) : Parcelable
fun Intent.getLoginDeepLinkInfo(): LoginDeepLinkInfo? {
val uri = data
return if (action == Intent.ACTION_VIEW && uri != null && uri.isAuthenticationDeepLink()) {
val host = uri.getQueryParameter("host")
val url = if (host.startsWith("http")) host else "https://$host"
val userId = uri.getQueryParameter("userId")
val token = uri.getQueryParameter("token")
try {
LoginDeepLinkInfo(url, userId, token)
} catch (ex: Exception) {
Timber.d(ex, "Error parsing login deeplink")
null
}
} else null
}
private inline fun Uri.isAuthenticationDeepLink(): Boolean {
if (host == "auth")
return true
else if (host == "go.rocket.chat" && path == "/auth")
return true
return false
}
\ No newline at end of file
package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.OauthHelper
......@@ -10,6 +11,7 @@ import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatTwoFactorException
import chat.rocket.common.model.Token
......@@ -24,6 +26,7 @@ import javax.inject.Inject
private const val TYPE_LOGIN_USER_EMAIL = 0
private const val TYPE_LOGIN_CAS = 1
private const val TYPE_LOGIN_OAUTH = 2
private const val TYPE_LOGIN_DEEP_LINK = 3
private const val SERVICE_NAME_GITHUB = "github"
private const val SERVICE_NAME_GOOGLE = "google"
private const val SERVICE_NAME_LINKEDIN = "linkedin"
......@@ -35,26 +38,31 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor,
private val settingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor,
private val saveAccountInteractor: SaveAccountInteractor,
private val factory: RocketChatClientFactory)
: CheckServerPresenter(strategy, factory.create(serverInteractor.get()!!), view) {
: CheckServerPresenter(strategy, factory, view) {
// TODO - we should validate the current server when opening the app, and have a nonnull get()
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private val settings: PublicSettings = settingsInteractor.get(currentServer)
private lateinit var client: RocketChatClient
private lateinit var settings: PublicSettings
//private val client: RocketChatClient = factory.create(currentServer)
//private val settings: PublicSettings = settingsInteractor.get(currentServer)
private lateinit var usernameOrEmail: String
private lateinit var password: String
private lateinit var credentialToken: String
private lateinit var credentialSecret: String
private lateinit var deepLinkUserId: String
private lateinit var deepLinkToken: String
fun setupView() {
setupConnectionInfo(currentServer)
setupLoginView()
setupUserRegistrationView()
setupCasView()
setupOauthServicesView()
checkServerInfo()
checkServerInfo(currentServer)
}
fun authenticateWithUserAndPassword(usernameOrEmail: String, password: String) {
......@@ -84,6 +92,32 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
doAuthentication(TYPE_LOGIN_OAUTH)
}
fun authenticadeWithDeepLink(deepLinkInfo: LoginDeepLinkInfo) {
val serverUrl = deepLinkInfo.url
setupConnectionInfo(serverUrl)
deepLinkUserId = deepLinkInfo.userId
deepLinkToken = deepLinkInfo.token
tokenRepository.save(serverUrl, Token(deepLinkUserId, deepLinkToken))
launchUI(strategy) {
try {
val version = checkServerVersion(serverUrl).await()
when (version) {
is Version.OutOfDateError -> {
view.blockAndAlertNotRequiredVersion()
}
else -> doAuthentication(TYPE_LOGIN_DEEP_LINK)
}
} catch (ex: Exception) {
Timber.d(ex, "Error performing deep link login")
}
}
}
private fun setupConnectionInfo(serverUrl: String) {
client = factory.create(serverUrl)
settings = settingsInteractor.get(serverUrl)
}
fun signup() = navigator.toSignUp()
private fun setupLoginView() {
......@@ -212,8 +246,16 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
TYPE_LOGIN_OAUTH -> {
client.loginWithOauth(credentialToken, credentialSecret)
}
TYPE_LOGIN_DEEP_LINK -> {
val myself = client.me() // Just checking if the credentials worked.
if (myself.id == deepLinkUserId) {
Token(deepLinkUserId, deepLinkToken)
} else {
throw RocketChatAuthException("Invalid Authentication Deep Link Credentials...")
}
}
else -> {
throw IllegalStateException("Expected TYPE_LOGIN_USER_EMAIL, TYPE_LOGIN_CAS or TYPE_LOGIN_OAUTH")
throw IllegalStateException("Expected TYPE_LOGIN_USER_EMAIL, TYPE_LOGIN_CAS, TYPE_LOGIN_OAUTH or TYPE_LOGIN_DEEP_LINK")
}
}
}
......
......@@ -14,8 +14,10 @@ import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.ImageButton
import android.widget.ScrollView
import androidx.core.view.postDelayed
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.login.presentation.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.KeyboardHelper
......@@ -26,6 +28,7 @@ import chat.rocket.android.webview.cas.ui.casWebViewIntent
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_SECRET
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN
import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent
import chat.rocket.common.util.ifNull
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject
......@@ -40,14 +43,22 @@ class LoginFragment : Fragment(), LoginView {
areLoginOptionsNeeded()
}
private var isGlobalLayoutListenerSetUp = false
private var deepLinkInfo: LoginDeepLinkInfo? = null
companion object {
fun newInstance() = LoginFragment()
private const val DEEP_LINK_INFO = "DeepLinkInfo"
fun newInstance(deepLinkInfo: LoginDeepLinkInfo? = null) = LoginFragment().apply {
arguments = Bundle().apply {
putParcelable(DEEP_LINK_INFO, deepLinkInfo)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
deepLinkInfo = arguments?.getParcelable(DEEP_LINK_INFO)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
......@@ -60,8 +71,12 @@ class LoginFragment : Fragment(), LoginView {
tintEditTextDrawableStart()
}
deepLinkInfo?.let {
presenter.authenticadeWithDeepLink(it)
}.ifNull {
presenter.setupView()
}
}
override fun onDestroyView() {
super.onDestroyView()
......@@ -377,19 +392,23 @@ class LoginFragment : Fragment(), LoginView {
}
private fun showRemainingSocialAccountsView() {
social_accounts_container.postDelayed({
social_accounts_container.postDelayed(300) {
ui {
(0..social_accounts_container.childCount)
.mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
.filter { it.isClickable }
.forEach { ui { it.setVisible(true) }}
}, 1000)
.forEach { it.setVisible(true) }
}
}
}
// Scrolling to the bottom of the screen.
private fun scrollToBottom() {
scroll_view.postDelayed({
ui { scroll_view.fullScroll(ScrollView.FOCUS_DOWN) }
}, 1250)
scroll_view.postDelayed(1250) {
ui {
scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}
}
}
......
......@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.presentation
import android.content.Intent
import chat.rocket.android.R
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.login.ui.LoginFragment
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.authentication.signup.ui.SignupFragment
......@@ -21,6 +22,12 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
}
}
fun toLogin(deepLinkInfo: LoginDeepLinkInfo) {
activity.addFragmentBackStack("LoginFragment", R.id.fragment_container) {
LoginFragment.newInstance(deepLinkInfo)
}
}
fun toTwoFA(username: String, password: String) {
activity.addFragmentBackStack("TwoFAFragment", R.id.fragment_container) {
TwoFAFragment.newInstance(username, password)
......
package chat.rocket.android.authentication.server.presentation
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor
......@@ -16,7 +18,14 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val serverInteractor: SaveCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor) {
fun connect(server: String) {
connectToServer(server) {
navigator.toLogin()
}
}
fun connectToServer(server: String, block: () -> Unit) {
if (!server.isValidUrl()) {
view.showInvalidServerUrlMessage()
} else {
......@@ -32,17 +41,19 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
try {
refreshSettingsInteractor.refresh(server)
serverInteractor.save(server)
navigator.toLogin()
block()
} catch (ex: Exception) {
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showMessage(ex)
} finally {
view.hideLoading()
}
}
}
}
fun deepLink(deepLinkInfo: LoginDeepLinkInfo) {
connectToServer(deepLinkInfo.url) {
navigator.toLogin(deepLinkInfo)
}
}
}
\ No newline at end of file
......@@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import chat.rocket.android.R
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.server.presentation.ServerPresenter
import chat.rocket.android.authentication.server.presentation.ServerView
import chat.rocket.android.helper.KeyboardHelper
......@@ -17,17 +18,26 @@ import javax.inject.Inject
class ServerFragment : Fragment(), ServerView {
@Inject lateinit var presenter: ServerPresenter
private var deepLinkInfo: LoginDeepLinkInfo? = null
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
text_server_url.isCursorVisible = KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)
}
companion object {
fun newInstance() = ServerFragment()
private const val DEEP_LINK_INFO = "DeepLinkInfo"
fun newInstance(deepLinkInfo: LoginDeepLinkInfo?) = ServerFragment().apply {
arguments = Bundle().apply {
putParcelable(DEEP_LINK_INFO, deepLinkInfo)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
deepLinkInfo = arguments?.getParcelable(DEEP_LINK_INFO)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
......@@ -37,6 +47,10 @@ class ServerFragment : Fragment(), ServerView {
super.onViewCreated(view, savedInstanceState)
relative_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
setupOnClickListener()
deepLinkInfo?.let {
presenter.deepLink(it)
}
}
override fun onDestroyView() {
......
......@@ -28,7 +28,7 @@ class TwoFAFragment : Fragment(), TwoFAView {
private const val PASSWORD = "password"
fun newInstance(username: String, password: String) = TwoFAFragment().apply {
arguments = Bundle(1).apply {
arguments = Bundle(2).apply {
putString(USERNAME, username)
putString(PASSWORD, password)
}
......
......@@ -6,10 +6,11 @@ import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.domain.model.getLoginDeepLinkInfo
import chat.rocket.android.authentication.presentation.AuthenticationPresenter
import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.launchUI
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......@@ -30,11 +31,13 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState)
val deepLinkInfo = intent.getLoginDeepLinkInfo()
launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
presenter.loadCredentials(newServer) { authenticated ->
// if we got authenticadeWithDeepLink information, pass true to newServer also
presenter.loadCredentials(newServer || deepLinkInfo != null) { authenticated ->
if (!authenticated) {
showServerInput(savedInstanceState)
showServerInput(savedInstanceState, deepLinkInfo)
}
}
}
......@@ -49,9 +52,9 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector
}
fun showServerInput(savedInstanceState: Bundle?) {
fun showServerInput(savedInstanceState: Bundle?, deepLinkInfo: LoginDeepLinkInfo?) {
addFragment("ServerFragment", R.id.fragment_container) {
ServerFragment.newInstance()
ServerFragment.newInstance(deepLinkInfo)
}
}
}
......
......@@ -57,6 +57,10 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_author_attachment)
AuthorAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.COLOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_color_attachment)
ColorAttachmentViewHolder(view, actionsListener, reactionListener)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
......@@ -97,6 +101,7 @@ class ChatRoomAdapter(
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
is AuthorAttachmentViewHolder -> holder.bind(dataSet[position] as AuthorAttachmentViewModel)
is ColorAttachmentViewHolder -> holder.bind(dataSet[position] as ColorAttachmentViewModel)
}
}
......@@ -117,12 +122,22 @@ class ChatRoomAdapter(
}
fun prependData(dataSet: List<BaseViewModel<*>>) {
val item = dataSet.firstOrNull { newItem ->
val item = dataSet.indexOfFirst { newItem ->
this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1
}
if (item == null) {
if (item == -1) {
this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size)
} else {
dataSet.forEach { item ->
val index = this.dataSet.indexOfFirst {
item.messageId == it.messageId && item.viewType == it.viewType
}
if (index > -1) {
this.dataSet[index] = item
notifyItemChanged(index)
}
}
}
}
......
package chat.rocket.android.chatroom.adapter
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.ColorAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_color_attachment.view.*
class ColorAttachmentViewHolder(itemView: View,
listener: BaseViewHolder.ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ColorAttachmentViewModel>(itemView, listener, reactionListener) {
val drawable: Drawable? = ContextCompat.getDrawable(itemView.context,
R.drawable.quote_vertical_bar)
init {
with(itemView) {
setupActionMenu(attachment_text)
setupActionMenu(color_attachment_container)
attachment_text.movementMethod = LinkMovementMethod()
}
}
override fun bindViews(data: ColorAttachmentViewModel) {
with(itemView) {
drawable?.let {
quote_bar.background = drawable.mutate().apply { setTint(data.color) }
attachment_text.text = data.text
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.graphics.Color
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
......@@ -29,6 +30,9 @@ class MessageViewHolder(
text_sender.text = data.senderName
text_content.text = data.content
image_avatar.setImageURI(data.avatar)
text_content.setTextColor(
if (data.isTemporary) Color.GRAY else Color.BLACK
)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.di
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.module.AppModule
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class MessageServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideMessageService(): MessageService
}
\ No newline at end of file
......@@ -12,6 +12,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewMo
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.username
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state
......@@ -20,6 +21,7 @@ import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.SimpleUser
import chat.rocket.common.model.UserStatus
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
......@@ -52,7 +54,8 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val roomsRepository: RoomRepository,
private val localRepository: LocalRepository,
factory: ConnectionManagerFactory,
private val mapper: ViewModelMapper) {
private val mapper: ViewModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor) {
private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer)
private val client = manager.client
......@@ -70,14 +73,18 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
launchUI(strategy) {
view.showLoading()
try {
val messages =
retryIO(description = "messages chatRoom: $chatRoomId, type: $chatRoomType, offset: $offset") {
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
if (offset == 0L) {
val localMessages = messagesRepository.getByRoomId(chatRoomId)
val oldMessages = mapper.map(localMessages)
if (oldMessages.isNotEmpty()) {
view.showMessages(oldMessages)
loadMissingMessages()
} else {
loadAndShowMessages(chatRoomId, chatRoomType, offset)
}
} else {
loadAndShowMessages(chatRoomId, chatRoomType, offset)
}
messagesRepository.saveAll(messages)
val messagesViewModels = mapper.map(messages)
view.showMessages(messagesViewModels)
// TODO: For now we are marking the room as read if we can get the messages (I mean, no exception occurs)
// but should mark only when the user see the first unread message.
......@@ -85,7 +92,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
subscribeMessages(chatRoomId)
} catch (ex: Exception) {
ex.printStackTrace()
Timber.e(ex)
ex.message?.let {
view.showMessage(it)
}.ifNull {
......@@ -101,28 +108,58 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
}
private suspend fun loadAndShowMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
val messages =
retryIO(description = "messages chatRoom: $chatRoomId, type: $chatRoomType, offset: $offset") {
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
}
messagesRepository.saveAll(messages)
val allMessages = mapper.map(messages)
view.showMessages(allMessages)
}
fun sendMessage(chatRoomId: String, text: String, messageId: String?) {
launchUI(strategy) {
view.disableSendMessageButton()
try {
// ignore message for now, will receive it on the stream
val message = retryIO {
if (messageId == null) {
val id = UUID.randomUUID().toString()
val message = if (messageId == null) {
val username = localRepository.username()
val newMessage = Message(
id = id,
roomId = chatRoomId,
message = text,
timestamp = Instant.now().toEpochMilli(),
sender = SimpleUser(null, username, username),
attachments = null,
avatar = currentServer.avatarUrl(username!!),
channels = null,
editedAt = null,
editedBy = null,
groupable = false,
parseUrls = false,
pinned = false,
mentions = emptyList(),
reactions = null,
senderAlias = null,
type = null,
updatedAt = null,
urls = null,
isTemporary = true
)
messagesRepository.save(newMessage)
view.showNewMessage(mapper.map(newMessage))
client.sendMessage(id, chatRoomId, text)
} else {
client.updateMessage(chatRoomId, messageId, text)
}
}
view.enableSendMessageButton(false)
view.enableSendMessageButton()
} catch (ex: Exception) {
Timber.d(ex, "Error sending message...")
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.enableSendMessageButton(true)
jobSchedulerInteractor.scheduleSendingMessages()
} finally {
view.enableSendMessageButton()
}
}
}
......@@ -189,6 +226,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
if (state is State.Connected) {
jobSchedulerInteractor.scheduleSendingMessages()
loadMissingMessages()
}
}
......@@ -563,12 +601,13 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
// failed, command is not valid so post it
sendMessage(roomId, text, null)
}
view.enableSendMessageButton(false)
}
} catch (ex: RocketChatException) {
Timber.e(ex)
// command is not valid, post it
sendMessage(roomId, text, null)
} finally {
view.enableSendMessageButton()
}
}
}
......
......@@ -92,10 +92,8 @@ interface ChatRoomView : LoadingView, MessageView {
/**
* Enables the send message button.
*
* @param sendFailed Whether the sent message has failed.
*/
fun enableSendMessageButton(sendFailed: Boolean)
fun enableSendMessageButton()
/**
* Clears the message composition.
......
package chat.rocket.android.chatroom.service
import android.app.job.JobParameters
import android.app.job.JobService
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.model.Message
import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
class MessageService : JobService() {
@Inject
lateinit var factory: ConnectionManagerFactory
@Inject
lateinit var currentServerRepository: CurrentServerRepository
@Inject
lateinit var messageRepository: MessagesRepository
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onStopJob(params: JobParameters?): Boolean {
return false
}
override fun onStartJob(params: JobParameters?): Boolean {
launch(CommonPool) {
val currentServer = currentServerRepository.get()
if (currentServer != null) {
retrySendingMessages(params, currentServer)
jobFinished(params, false)
}
}
return true
}
private suspend fun retrySendingMessages(params: JobParameters?, currentServer: String) {
val temporaryMessages = messageRepository.getAllUnsent()
.sortedWith(compareBy(Message::timestamp))
if (temporaryMessages.isNotEmpty()) {
val connectionManager = factory.create(currentServer)
val client = connectionManager.client
temporaryMessages.forEach { message ->
try {
client.sendMessage(
message = message.message,
messageId = message.id,
roomId = message.roomId,
avatar = message.avatar,
attachments = message.attachments,
alias = message.senderAlias
)
messageRepository.save(message.copy(isTemporary = false))
Timber.d("Sent scheduled message given by id: ${message.id}")
} catch (ex: RocketChatException) {
Timber.e(ex)
if (ex.message?.contains("E11000", true) == true) {
// XXX: Temporary solution. We need proper error codes from the api.
messageRepository.save(message.copy(isTemporary = false))
}
jobFinished(params, true)
}
}
}
}
companion object {
const val RETRY_SEND_MESSAGE_ID = 1
}
}
\ No newline at end of file
......@@ -18,7 +18,6 @@ import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.*
import androidx.core.content.systemService
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.*
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
......@@ -70,8 +69,10 @@ private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val BUNDLE_CHAT_ROOM_IS_SUBSCRIBED = "chat_room_is_subscribed"
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
@Inject lateinit var presenter: ChatRoomPresenter
@Inject lateinit var parser: MessageParser
@Inject
lateinit var presenter: ChatRoomPresenter
@Inject
lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
......@@ -232,9 +233,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
// if y is positive the keyboard is up else it's down
recycler_view.post {
if (y > 0 || Math.abs(verticalScrollOffset.get()) >= Math.abs(y)) {
recycler_view.scrollBy(0, y)
ui { recycler_view.scrollBy(0, y) }
} else {
recycler_view.scrollBy(0, verticalScrollOffset.get())
ui { recycler_view.scrollBy(0, verticalScrollOffset.get()) }
}
}
}
......@@ -316,15 +317,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun enableSendMessageButton(sendFailed: Boolean) {
override fun enableSendMessageButton() {
ui {
button_send.isEnabled = true
text_message.isEnabled = true
if (!sendFailed) {
clearMessageComposition()
}
}
}
override fun clearMessageComposition() {
ui {
......@@ -408,7 +408,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun copyToClipboard(message: String) {
ui {
val clipboard: ClipboardManager = it.systemService()
val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText("", message)
}
}
......@@ -589,6 +589,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
var textMessage = citation ?: ""
textMessage += text_message.textContent
sendMessage(textMessage)
clearMessageComposition()
}
button_show_attachment_options.setOnClickListener {
......
......@@ -13,7 +13,8 @@ data class AudioAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
......
......@@ -15,7 +15,8 @@ data class AuthorAttachmentViewModel(
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseAttachmentViewModel<AuthorAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUTHOR_ATTACHMENT.viewType
......
......@@ -12,6 +12,7 @@ interface BaseViewModel<out T> {
var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>?
var preview: Message?
var isTemporary: Boolean
enum class ViewType(val viewType: Int) {
MESSAGE(0),
......@@ -21,7 +22,8 @@ interface BaseViewModel<out T> {
VIDEO_ATTACHMENT(4),
AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7)
AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8)
}
}
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.ColorAttachment
data class ColorAttachmentViewModel(
override val attachmentUrl: String,
val id: Long,
val color: Int,
val text: CharSequence,
override val message: Message,
override val rawData: ColorAttachment,
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseAttachmentViewModel<ColorAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.COLOR_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_color_attachment
}
\ No newline at end of file
......@@ -13,7 +13,8 @@ data class ImageAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
......@@ -14,7 +14,8 @@ data class MessageAttachmentViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
......
......@@ -15,7 +15,8 @@ data class MessageViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
var isFirstUnread: Boolean
var isFirstUnread: Boolean,
override var isTemporary: Boolean = false
) : BaseMessageViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType
......
......@@ -14,7 +14,8 @@ data class UrlPreviewViewModel(
val thumbUrl: String?,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Url> {
override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
......@@ -13,7 +13,8 @@ data class VideoAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
......@@ -5,6 +5,7 @@ import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.support.v4.content.ContextCompat
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
......@@ -106,10 +107,23 @@ class ViewModelMapper @Inject constructor(private val context: Context,
is FileAttachment -> mapFileAttachment(message, attachment)
is MessageAttachment -> mapMessageAttachment(message, attachment)
is AuthorAttachment -> mapAuthorAttachment(message, attachment)
is ColorAttachment -> mapColorAttachment(message, attachment)
else -> null
}
}
private suspend fun mapColorAttachment(message: Message, attachment: ColorAttachment): BaseViewModel<*>? {
return with(attachment) {
val content = stripMessageQuotes(message)
val id = attachmentId(message, attachment)
ColorAttachmentViewModel(attachmentUrl = url, id = id, color = color.color,
text = text, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message))
}
}
private suspend fun mapAuthorAttachment(message: Message, attachment: AuthorAttachment): AuthorAttachmentViewModel {
return with(attachment) {
val content = stripMessageQuotes(message)
......@@ -212,12 +226,13 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val time = getTime(message.timestamp)
val avatar = getUserAvatar(message)
val preview = mapMessagePreview(message)
val isTemp = message.isTemporary ?: false
val content = getContent(stripMessageQuotes(message))
MessageViewModel(message = stripMessageQuotes(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, reactions = getReactions(message),
isFirstUnread = false, preview = preview)
isFirstUnread = false, preview = preview, isTemporary = isTemp)
}
private suspend fun mapMessagePreview(message: Message): Message {
......
......@@ -42,6 +42,7 @@ class ChatRoomsPresenter @Inject constructor(
private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val viewModelMapper: ViewModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor,
settingsRepository: SettingsRepository,
factory: ConnectionManagerFactory
) {
......@@ -121,7 +122,8 @@ class ChatRoomsPresenter @Inject constructor(
private fun usersToChatRooms(users: List<User>): List<ChatRoom> {
return users.map {
ChatRoom(id = it.id,
ChatRoom(
id = it.id,
type = RoomType.DIRECT_MESSAGE,
user = SimpleUser(username = it.username, name = it.name, id = null),
status = getActiveUserByUsername(it.name!!)?.status,
......@@ -148,7 +150,8 @@ class ChatRoomsPresenter @Inject constructor(
private fun roomsToChatRooms(rooms: List<Room>): List<ChatRoom> {
return rooms.map {
ChatRoom(id = it.id,
ChatRoom(
id = it.id,
type = it.type,
user = it.user,
status = getActiveUserByUsername(it.name!!)?.status,
......@@ -254,6 +257,7 @@ class ChatRoomsPresenter @Inject constructor(
view.showConnectionState(state)
}
if (state is State.Connected) {
jobSchedulerInteractor.scheduleSendingMessages()
reloadRooms()
updateChatRooms()
}
......@@ -339,7 +343,8 @@ class ChatRoomsPresenter @Inject constructor(
val chatRooms = getChatRoomsInteractor.getAll(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == room.id }
chatRoom?.apply {
val newRoom = ChatRoom(id = room.id,
val newRoom = ChatRoom(
id = room.id,
type = room.type,
user = room.user ?: user,
status = getActiveUserByUsername(room.name!!)?.status,
......@@ -360,7 +365,8 @@ class ChatRoomsPresenter @Inject constructor(
userMenstions = userMenstions,
groupMentions = groupMentions,
lastMessage = room.lastMessage,
client = client)
client = client
)
removeRoom(room.id, chatRooms)
chatRooms.add(newRoom)
saveChatRoomsInteractor.save(currentServer, sortRooms(chatRooms))
......@@ -373,7 +379,8 @@ class ChatRoomsPresenter @Inject constructor(
val chatRooms = getChatRoomsInteractor.getAll(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId }
chatRoom?.apply {
val newRoom = ChatRoom(id = subscription.roomId,
val newRoom = ChatRoom(
id = subscription.roomId,
type = subscription.type,
user = subscription.user ?: user,
status = getActiveUserByUsername(subscription.name)?.status,
......@@ -394,7 +401,8 @@ class ChatRoomsPresenter @Inject constructor(
userMenstions = subscription.userMentions,
groupMentions = subscription.groupMentions,
lastMessage = lastMessage,
client = client)
client = client
)
removeRoom(subscription.roomId, chatRooms)
chatRooms.add(newRoom)
saveChatRoomsInteractor.save(currentServer, sortRooms(chatRooms))
......
package chat.rocket.android.core.behaviours
import android.support.annotation.StringRes
import chat.rocket.common.util.ifNull
interface MessageView {
......@@ -15,3 +16,11 @@ interface MessageView {
fun showGenericErrorMessage()
}
fun MessageView.showMessage(ex: Exception) {
ex.message?.let {
showMessage(it)
}.ifNull {
showGenericErrorMessage()
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ package chat.rocket.android.dagger
import android.app.Application
import chat.rocket.android.app.RocketChatApplication
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.module.ActivityBuilder
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.dagger.module.ReceiverBuilder
......@@ -29,6 +30,8 @@ interface AppComponent {
fun inject(service: FirebaseTokenService)
fun inject(service: MessageService)
/*@Component.Builder
abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/
}
......@@ -2,16 +2,20 @@ package chat.rocket.android.dagger.module
import android.app.Application
import android.app.NotificationManager
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.arch.persistence.room.Room
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.systemService
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.dagger.qualifier.ForMessages
import chat.rocket.android.helper.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
......@@ -20,11 +24,39 @@ import chat.rocket.android.push.GroupedPush
import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.*
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.ChatRoomsRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetPermissionsInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.JobSchedulerInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.infraestructure.JobSchedulerInteractorImpl
import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository
import chat.rocket.android.server.infraestructure.MemoryRoomRepository
import chat.rocket.android.server.infraestructure.MemoryUsersRepository
import chat.rocket.android.server.infraestructure.ServerDao
import chat.rocket.android.server.infraestructure.SharedPreferencesAccountsRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesMessagesRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesSettingsRepository
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.AttachmentAdapterFactory
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig
......@@ -64,7 +96,7 @@ class AppModule {
@Provides
@Singleton
fun provideRocketChatDatabase(context: Application): RocketChatDatabase {
return Room.databaseBuilder(context, RocketChatDatabase::class.java, "rocketchat-db").build()
return Room.databaseBuilder(context.applicationContext, RocketChatDatabase::class.java, "rocketchat-db").build()
}
@Provides
......@@ -114,7 +146,7 @@ class AppModule {
@Provides
@ForFresco
@Singleton
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
fun provideFrescoAuthInterceptor(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
return FrescoAuthInterceptor(tokenRepository, currentServerInteractor)
}
......@@ -159,9 +191,14 @@ class AppModule {
}
@Provides
fun provideSharedPreferences(context: Application): SharedPreferences {
return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
}
fun provideSharedPreferences(context: Application) =
context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
@Provides
@ForMessages
fun provideMessagesSharedPreferences(context: Application) =
context.getSharedPreferences("messages", Context.MODE_PRIVATE)
@Provides
@Singleton
......@@ -201,10 +238,25 @@ class AppModule {
@Provides
@Singleton
fun provideMoshi(): Moshi {
fun provideMoshi(
logger: PlatformLogger,
currentServerInteractor: GetCurrentServerInteractor
): Moshi {
val url = currentServerInteractor.get() ?: ""
return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE)
.add(AttachmentAdapterFactory(Logger(logger, url)))
.add(
java.lang.Long::class.java,
ISO8601Date::class.java,
TimestampAdapter(CalendarISO8601Converter())
)
.add(
Long::class.java,
ISO8601Date::class.java,
TimestampAdapter(CalendarISO8601Converter())
)
.build()
}
......@@ -216,8 +268,10 @@ class AppModule {
@Provides
@Singleton
fun provideMessageRepository(): MessagesRepository {
return MemoryMessagesRepository()
fun provideMessageRepository(@ForMessages preferences: SharedPreferences,
moshi: Moshi,
currentServerInteractor: GetCurrentServerInteractor): MessagesRepository {
return SharedPreferencesMessagesRepository(preferences, moshi, currentServerInteractor)
}
@Provides
......@@ -260,7 +314,8 @@ class AppModule {
SharedPreferencesAccountsRepository(preferences, moshi)
@Provides
fun provideNotificationManager(context: Context): NotificationManager = context.systemService()
fun provideNotificationManager(context: Application) =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Provides
@Singleton
......@@ -269,7 +324,7 @@ class AppModule {
@Provides
@Singleton
fun providePushManager(
context: Context,
context: Application,
groupedPushes: GroupedPush,
manager: NotificationManager,
moshi: Moshi,
......@@ -277,4 +332,22 @@ class AppModule {
getSettingsInteractor: GetSettingsInteractor): PushManager {
return PushManager(groupedPushes, manager, moshi, getAccountInteractor, getSettingsInteractor, context)
}
@Provides
fun provideJobScheduler(context: Application): JobScheduler {
return context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
}
@Provides
fun provideSendMessageJob(context: Application): JobInfo {
return JobInfo.Builder(MessageService.RETRY_SEND_MESSAGE_ID,
ComponentName(context, MessageService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build()
}
@Provides
fun provideJobSchedulerInteractor(jobScheduler: JobScheduler, jobInfo: JobInfo): JobSchedulerInteractor {
return JobSchedulerInteractorImpl(jobScheduler, jobInfo)
}
}
\ No newline at end of file
package chat.rocket.android.dagger.module
import chat.rocket.android.chatroom.di.MessageServiceProvider
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.push.FirebaseTokenService
import chat.rocket.android.push.GcmListenerService
import chat.rocket.android.push.di.FirebaseTokenServiceProvider
......@@ -14,4 +16,7 @@ import dagger.android.ContributesAndroidInjector
@ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class])
abstract fun bindGcmListenerService(): GcmListenerService
@ContributesAndroidInjector(modules = [MessageServiceProvider::class])
abstract fun bindMessageService(): MessageService
}
\ No newline at end of file
package chat.rocket.android.dagger.qualifier
import javax.inject.Qualifier
/**
* Created by luciofm on 4/14/18.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ForMessages
\ No newline at end of file
......@@ -24,4 +24,5 @@ interface LocalRepository {
}
}
fun LocalRepository.checkIfMyself(username: String) = get(LocalRepository.CURRENT_USERNAME_KEY) == username
\ No newline at end of file
fun LocalRepository.checkIfMyself(username: String) = username() == username
fun LocalRepository.username() = get(LocalRepository.CURRENT_USERNAME_KEY)
\ No newline at end of file
......@@ -23,9 +23,7 @@ import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
......@@ -39,11 +37,11 @@ class MainPresenter @Inject constructor(
private val navHeaderMapper: NavHeaderViewModelMapper,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInterector: RemoveAccountInterector,
private val removeAccountInteractor: RemoveAccountInteractor,
private val factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory
) : CheckServerPresenter(strategy, client = factory.create(serverInteractor.get()!!), view = view) {
) : CheckServerPresenter(strategy, factory, view = view) {
private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer)
......@@ -58,7 +56,7 @@ class MainPresenter @Inject constructor(
fun toSettings() = navigator.toSettings()
fun loadCurrentInfo() {
checkServerInfo()
checkServerInfo(currentServer)
launchUI(strategy) {
try {
val me = retryIO("me") {
......@@ -105,7 +103,7 @@ class MainPresenter @Inject constructor(
try {
disconnect()
removeAccountInterector.remove(currentServer)
removeAccountInteractor.remove(currentServer)
tokenRepository.remove(currentServer)
navigator.toNewServer()
} catch (ex: Exception) {
......
package chat.rocket.android.profile.presentation
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
......@@ -36,11 +37,7 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView,
myself.emails?.get(0)?.address!!
)
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showMessage(exception)
} finally {
view.hideLoading()
}
......
......@@ -12,6 +12,7 @@ import chat.rocket.android.profile.presentation.ProfilePresenter
import chat.rocket.android.profile.presentation.ProfileView
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.avatar_profile.*
......@@ -25,6 +26,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private lateinit var currentEmail: String
private lateinit var currentAvatar: String
private var actionMode: ActionMode? = null
private val disposables = CompositeDisposable()
companion object {
fun newInstance() = ProfileFragment()
......@@ -48,7 +50,13 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
presenter.loadUserProfile()
}
override fun onDestroyView() {
disposables.clear()
super.onDestroyView()
}
override fun showProfile(avatarUrl: String, name: String, username: String, email: String) {
ui {
image_avatar.setImageURI(avatarUrl)
text_name.textContent = name
......@@ -65,6 +73,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
listenToChanges()
}
}
override fun showProfileUpdateSuccessfullyMessage() {
showMessage(getString(R.string.msg_profile_update_successfully))
......@@ -72,23 +81,31 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
override fun showLoading() {
enableUserInput(false)
ui {
view_loading.setVisible(true)
}
}
override fun hideLoading() {
ui {
if (view_loading != null) {
view_loading.setVisible(false)
}
}
enableUserInput(true)
}
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......@@ -136,7 +153,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
}
private fun listenToChanges() {
Observables.combineLatest(text_name.asObservable(),
disposables.add(Observables.combineLatest(text_name.asObservable(),
text_username.asObservable(),
text_email.asObservable(),
text_avatar_url.asObservable()) { text_name, text_username, text_email, text_avatar_url ->
......@@ -144,7 +161,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_username.toString() != currentUsername ||
text_email.toString() != currentEmail ||
(text_avatar_url.toString() != "" && text_avatar_url.toString() != currentAvatar))
}.subscribe({ isValid ->
}.subscribe { isValid ->
if (isValid) {
startActionMode()
} else {
......@@ -162,9 +179,11 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private fun finishActionMode() = actionMode?.finish()
private fun enableUserInput(value: Boolean) {
ui {
text_username.isEnabled = value
text_username.isEnabled = value
text_email.isEnabled = value
text_avatar_url.isEnabled = value
}
}
}
......@@ -4,6 +4,7 @@ import chat.rocket.android.R
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.registerPushToken
import com.google.android.gms.gcm.GoogleCloudMessaging
......@@ -32,6 +33,7 @@ class FirebaseTokenService : FirebaseInstanceIdService() {
override fun onTokenRefresh() {
//TODO: We need to use the Cordova Project gcm_sender_id since it's the one configured on RC
// default push gateway. We should register this project's own project sender id into it.
try {
val gcmToken = InstanceID.getInstance(this)
.getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
val currentServer = getCurrentServerInteractor.get()
......@@ -43,12 +45,15 @@ class FirebaseTokenService : FirebaseInstanceIdService() {
launch {
try {
Timber.d("Registering push token: $gcmToken for ${client.url}")
client.registerPushToken(gcmToken)
retryIO("register push token") { client.registerPushToken(gcmToken) }
} catch (ex: RocketChatException) {
Timber.e(ex)
Timber.e(ex, "Error registering push token")
}
}
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error refreshing Firebase TOKEN")
}
}
}
\ No newline at end of file
package chat.rocket.android.server.domain
interface JobSchedulerInteractor {
/**
* Schedule job to retry previously failed sending messages.
*/
fun scheduleSendingMessages()
}
\ No newline at end of file
......@@ -11,7 +11,7 @@ interface MessagesRepository {
*
* @return The Message object given by the id or null if message wasn't found.
*/
fun getById(id: String): Message?
suspend fun getById(id: String): Message?
/**
* Get all messages from the current room id.
......@@ -19,7 +19,7 @@ interface MessagesRepository {
* @param rid The room id.
* @return A list of Message objects for the room with given room id or an empty list.
*/
fun getByRoomId(rid: String): List<Message>
suspend fun getByRoomId(rid: String): List<Message>
/**
* Get most recent messages up to count different users.
......@@ -29,43 +29,47 @@ interface MessagesRepository {
*
* @return List of last count messages.
*/
fun getRecentMessages(rid: String, count: Long): List<Message>
suspend fun getRecentMessages(rid: String, count: Long): List<Message>
/**
* Get all messages. Use carefully!
*
* @return All messages or an empty list.
*/
fun getAll(): List<Message>
suspend fun getAll(): List<Message>
/**
* Save a single message object.
*
* @param The message object to saveAll.
*/
fun save(message: Message)
suspend fun save(message: Message)
/**
* Save a list of messages.
*/
fun saveAll(newMessages: List<Message>)
suspend fun saveAll(newMessages: List<Message>)
/**
* Removes all messages.
*/
fun clear()
suspend fun clear()
/**
* Remove message by id.
*
* @param id The id of the message to remove.
*/
fun removeById(id: String)
suspend fun removeById(id: String)
/**
* Remove all messages from a given room.
*
* @param rid The room id where messages are to be removed.
*/
fun removeByRoomId(rid: String)
suspend fun removeByRoomId(rid: String)
suspend fun getAllUnsent(): List<Message>
suspend fun getUnsentByRoomId(roomId: String): List<Message>
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.core.internal.rest.settings
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.async
......@@ -27,7 +28,9 @@ class RefreshSettingsInteractor @Inject constructor(private val factory: RocketC
suspend fun refresh(server: String) {
withContext(CommonPool) {
factory.create(server).let { client ->
val settings = client.settings(*settingsFilter)
val settings = retryIO(description = "settings", times = 5) {
client.settings(*settingsFilter)
}
repository.save(server, settings)
}
}
......
......@@ -2,7 +2,7 @@ package chat.rocket.android.server.domain
import javax.inject.Inject
class RemoveAccountInterector @Inject constructor(val repository: AccountsRepository) {
class RemoveAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun remove(serverUrl: String) {
repository.remove(serverUrl)
}
......
package chat.rocket.android.server.infraestructure
import android.app.job.JobInfo
import android.app.job.JobScheduler
import chat.rocket.android.server.domain.JobSchedulerInteractor
import timber.log.Timber
import javax.inject.Inject
/**
* Uses the Android framework [JobScheduler].
*/
class JobSchedulerInteractorImpl @Inject constructor(
private val jobScheduler: JobScheduler,
private val jobInfo: JobInfo
) : JobSchedulerInteractor {
override fun scheduleSendingMessages() {
Timber.d("Scheduling unsent messages to send...")
jobScheduler.schedule(jobInfo)
}
}
\ No newline at end of file
......@@ -2,45 +2,67 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.model.Message
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
class MemoryMessagesRepository : MessagesRepository {
private val messages: HashMap<String, Message> = HashMap()
override fun getById(id: String): Message? {
return messages[id]
override suspend fun getById(id: String): Message? = withContext(CommonPool) {
return@withContext messages[id]
}
override fun getByRoomId(rid: String): List<Message> {
return messages.filter { it.value.roomId == rid }.values.toList()
override suspend fun getByRoomId(rid: String): List<Message> = withContext(CommonPool) {
return@withContext messages.filter { it.value.roomId == rid }.values.toList()
}
override fun getRecentMessages(rid: String, count: Long): List<Message> {
return getByRoomId(rid).sortedByDescending { it.timestamp }
override suspend fun getRecentMessages(rid: String, count: Long): List<Message> = withContext(CommonPool) {
return@withContext getByRoomId(rid).sortedByDescending { it.timestamp }
.distinctBy { it.sender }.take(count.toInt())
}
override fun getAll(): List<Message> = messages.values.toList()
override suspend fun getAll(): List<Message> = withContext(CommonPool) {
return@withContext messages.values.toList()
}
override suspend fun getUnsentByRoomId(roomId: String): List<Message> = withContext(CommonPool) {
val allByRoomId = getByRoomId(roomId)
if (allByRoomId.isEmpty()) {
return@withContext emptyList<Message>()
}
return@withContext allByRoomId.filter { it.isTemporary ?: false && it.roomId == roomId }
}
override fun save(message: Message) {
override suspend fun getAllUnsent(): List<Message> = withContext(CommonPool) {
val all = getAll()
if (all.isEmpty()) {
return@withContext emptyList<Message>()
}
return@withContext all.filter { it.isTemporary ?: false }
}
override suspend fun save(message: Message) = withContext(CommonPool) {
messages[message.id] = message
}
override fun saveAll(newMessages: List<Message>) {
override suspend fun saveAll(newMessages: List<Message>) = withContext(CommonPool) {
for (msg in newMessages) {
messages[msg.id] = msg
}
}
override fun clear() {
override suspend fun clear() = withContext(CommonPool) {
messages.clear()
}
override fun removeById(id: String) {
override suspend fun removeById(id: String) {
withContext(CommonPool) {
messages.remove(id)
}
}
override fun removeByRoomId(rid: String) {
override suspend fun removeByRoomId(rid: String) = withContext(CommonPool) {
val roomMessages = messages.filter { it.value.roomId == rid }.values
roomMessages.forEach {
messages.remove(it.roomId)
......
package chat.rocket.android.server.infraestructure
import android.content.SharedPreferences
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.model.Message
import com.squareup.moshi.Moshi
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
class SharedPreferencesMessagesRepository(
private val prefs: SharedPreferences,
private val moshi: Moshi,
private val currentServerInteractor: GetCurrentServerInteractor
) : MessagesRepository {
override suspend fun getById(id: String): Message? = withContext(CommonPool) {
currentServerInteractor.get()?.also { server ->
if (prefs.all.values.isEmpty()) {
return@withContext null
}
val adapter = moshi.adapter<Message>(Message::class.java)
val values = prefs.all.entries.filter { it.key.startsWith(server) }
.map { it.value } as Collection<String>
return@withContext values.map { adapter.fromJson(it) }.firstOrNull { it?.id == id }
}
return@withContext null
}
override suspend fun getByRoomId(rid: String): List<Message> = withContext(CommonPool) {
currentServerInteractor.get()?.also { server ->
val adapter = moshi.adapter<Message>(Message::class.java)
if (prefs.all.values.isEmpty()) {
return@withContext emptyList<Message>()
}
val values = prefs.all.entries.filter { it.key.startsWith(server) }
.map { it.value } as Collection<String>
return@withContext values.mapNotNull { adapter.fromJson(it) }.filter {
it.roomId == rid
}.toList().sortedWith(compareBy(Message::timestamp)).reversed()
}
return@withContext emptyList<Message>()
}
override suspend fun getRecentMessages(rid: String, count: Long): List<Message> = withContext(CommonPool) {
return@withContext getByRoomId(rid).sortedByDescending { it.timestamp }
.distinctBy { it.sender }.take(count.toInt())
}
override suspend fun getAll(): List<Message> = withContext(CommonPool) {
val adapter = moshi.adapter<Message>(Message::class.java)
if (prefs.all.values.isEmpty()) {
return@withContext emptyList<Message>()
}
currentServerInteractor.get()?.also { server ->
val values = prefs.all.entries.filter { it.key.startsWith(server) }
.map { it.value } as Collection<String>
return@withContext values.mapNotNull { adapter.fromJson(it) }
}
return@withContext emptyList<Message>()
}
override suspend fun getAllUnsent(): List<Message> = withContext(CommonPool) {
if (prefs.all.values.isEmpty()) {
return@withContext emptyList<Message>()
}
currentServerInteractor.get()?.also { server ->
val values = prefs.all.entries.filter { it.key.startsWith(server) }
.map { it.value } as Collection<String>
val adapter = moshi.adapter<Message>(Message::class.java)
return@withContext values.mapNotNull { adapter.fromJson(it) }
.filter { it.isTemporary ?: false }
}
return@withContext emptyList<Message>()
}
override suspend fun getUnsentByRoomId(roomId: String): List<Message> = withContext(CommonPool) {
val allByRoomId = getByRoomId(roomId)
if (allByRoomId.isEmpty()) {
return@withContext emptyList<Message>()
}
return@withContext allByRoomId.filter { it.isTemporary ?: false }
}
override suspend fun save(message: Message) {
withContext(CommonPool) {
currentServerInteractor.get()?.also {
val adapter = moshi.adapter<Message>(Message::class.java)
prefs.edit().putString("${it}_${message.id}", adapter.toJson(message)).apply()
}
}
}
override suspend fun saveAll(newMessages: List<Message>) {
withContext(CommonPool) {
currentServerInteractor.get()?.also {
val adapter = moshi.adapter<Message>(Message::class.java)
val editor = prefs.edit()
for (msg in newMessages) {
editor.putString("${it}_${msg.id}", adapter.toJson(msg))
}
editor.apply()
}
}
}
override suspend fun clear() = withContext(CommonPool) {
prefs.edit().clear().apply()
}
override suspend fun removeById(id: String) {
withContext(CommonPool) {
currentServerInteractor.get()?.also {
prefs.edit().putString("${it}_$id", null).apply()
}
}
}
override suspend fun removeByRoomId(rid: String) {
withContext(CommonPool) {
currentServerInteractor.get()?.also { server ->
val adapter = moshi.adapter<Message>(Message::class.java)
val editor = prefs.edit()
prefs.all.entries.forEach {
val value = it.value
if (value is String) {
val message = adapter.fromJson(value)
if (message?.roomId == rid) {
editor.putString("${server}_${message.id}", null)
}
}
}
editor.apply()
}
}
}
}
\ No newline at end of file
......@@ -3,19 +3,51 @@ package chat.rocket.android.server.presentation
import chat.rocket.android.BuildConfig
import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.VersionInfo
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.serverInfo
import kotlinx.coroutines.experimental.Deferred
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.async
import timber.log.Timber
abstract class CheckServerPresenter constructor(private val strategy: CancelStrategy,
private val client: RocketChatClient,
private val factory: RocketChatClientFactory,
private val view: VersionCheckView) {
internal fun checkServerInfo() {
launchUI(strategy) {
private lateinit var currentServer: String
private val client: RocketChatClient by lazy {
factory.create(currentServer)
}
internal fun checkServerInfo(serverUrl: String): Job {
return launchUI(strategy) {
try {
val version = checkServerVersion(serverUrl).await()
when (version) {
is Version.VersionOk -> {
Timber.i("Your version is nice! (Requires: 0.62.0, Yours: ${version.version})")
}
is Version.RecommendedVersionWarning -> {
Timber.i("Your server ${version.version} is bellow recommended version ${BuildConfig.RECOMMENDED_SERVER_VERSION}")
view.alertNotRecommendedVersion()
}
is Version.OutOfDateError -> {
Timber.i("Oops. Looks like your server ${version.version} is out-of-date! Minimum server version required ${BuildConfig.REQUIRED_SERVER_VERSION}!")
view.blockAndAlertNotRequiredVersion()
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error getting server info")
}
}
}
internal fun checkServerVersion(serverUrl: String): Deferred<Version> {
currentServer = serverUrl
return async {
val serverInfo = retryIO(description = "serverInfo", times = 5) { client.serverInfo() }
val thisServerVersion = serverInfo.version
val isRequiredVersion = isRequiredServerVersion(thisServerVersion)
......@@ -23,17 +55,12 @@ abstract class CheckServerPresenter constructor(private val strategy: CancelStra
if (isRequiredVersion) {
if (isRecommendedVersion) {
Timber.i("Your version is nice! (Requires: 0.62.0, Yours: $thisServerVersion)")
return@async Version.VersionOk(thisServerVersion)
} else {
view.alertNotRecommendedVersion()
return@async Version.RecommendedVersionWarning(thisServerVersion)
}
} else {
if (!isRecommendedVersion) {
view.blockAndAlertNotRequiredVersion()
Timber.i("Oops. Looks like your server is out-of-date! Minimum server version required ${BuildConfig.REQUIRED_SERVER_VERSION}!")
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error getting server info")
return@async Version.OutOfDateError(thisServerVersion)
}
}
}
......@@ -92,4 +119,10 @@ abstract class CheckServerPresenter constructor(private val strategy: CancelStra
0
}
}
sealed class Version(val version: String) {
data class VersionOk(private val currentVersion: String) : Version(currentVersion)
data class RecommendedVersionWarning(private val currentVersion: String) : Version(currentVersion)
data class OutOfDateError(private val currentVersion: String) : Version(currentVersion)
}
}
\ No newline at end of file
......@@ -8,13 +8,18 @@
android:focusableInTouchMode="true"
tools:context=".profile.ui.ProfileFragment">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/profile_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
android:visibility="gone"
tools:visibility="visible">
<include
android:id="@+id/layout_avatar_profile"
......@@ -53,9 +58,12 @@
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_link_black_24dp"
android:hint="@string/msg_avatar_url"
android:inputType="text" />
android:inputType="text"
android:layout_marginBottom="16dp"/>
</LinearLayout>
</ScrollView>
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:id="@+id/color_attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="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:paddingTop="@dimen/message_item_top_and_bottom_padding">
<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:layout_marginStart="56dp"
android:background="@drawable/quote_vertical_bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/recycler_view_reactions"/>
<TextView
android:id="@+id/attachment_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:autoLink="web"
app:layout_constraintStart_toEndOf="@id/quote_bar"
app:layout_constraintEnd_toEndOf="parent"
tools:text="#5571 - User profile from SSO must not have password change option" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/attachment_text" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content xmlns:tools="http://schemas.android.com/tools">
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="messages.xml"
tools:ignore="FullBackupContent" />
<exclude domain="file" path="instant-run"
tools:ignore="FullBackupContent" />
</full-backup-content>
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