Unverified Commit 95a28de4 authored by divyanshu bhargava's avatar divyanshu bhargava Committed by GitHub

Merge pull request #7 from RocketChat/develop-2.x

merge
parents 513931f0 eb73cd48
...@@ -13,8 +13,8 @@ android { ...@@ -13,8 +13,8 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 2009 versionCode 2010
versionName "2.0.0-rc1" versionName "2.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
} }
...@@ -124,6 +124,10 @@ kotlin { ...@@ -124,6 +124,10 @@ kotlin {
} }
} }
androidExtensions {
experimental = true
}
// FIXME - build and install the sdk into the app/libs directory // FIXME - build and install the sdk into the app/libs directory
// We were having some issues with the kapt generated files from the sdk when importing as a module // We were having some issues with the kapt generated files from the sdk when importing as a module
task compileSdk(type:Exec) { task compileSdk(type:Exec) {
......
...@@ -17,10 +17,11 @@ ...@@ -17,10 +17,11 @@
<application <application
android:name=".app.RocketChatApplication" android:name=".app.RocketChatApplication"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_config"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config"> android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"> android:supportsRtl="true">
<activity <activity
...@@ -34,6 +35,19 @@ ...@@ -34,6 +35,19 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" /> <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> </intent-filter>
</activity> </activity>
<activity <activity
......
package chat.rocket.android.authentication.domain.model
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
@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 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.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.OauthHelper import chat.rocket.android.helper.OauthHelper
...@@ -10,6 +11,7 @@ import chat.rocket.android.server.infraestructure.RocketChatClientFactory ...@@ -10,6 +11,7 @@ import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatTwoFactorException import chat.rocket.common.RocketChatTwoFactorException
import chat.rocket.common.model.Token import chat.rocket.common.model.Token
...@@ -24,6 +26,7 @@ import javax.inject.Inject ...@@ -24,6 +26,7 @@ import javax.inject.Inject
private const val TYPE_LOGIN_USER_EMAIL = 0 private const val TYPE_LOGIN_USER_EMAIL = 0
private const val TYPE_LOGIN_CAS = 1 private const val TYPE_LOGIN_CAS = 1
private const val TYPE_LOGIN_OAUTH = 2 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_GITHUB = "github"
private const val SERVICE_NAME_GOOGLE = "google" private const val SERVICE_NAME_GOOGLE = "google"
private const val SERVICE_NAME_LINKEDIN = "linkedin" private const val SERVICE_NAME_LINKEDIN = "linkedin"
...@@ -35,26 +38,31 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -35,26 +38,31 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
private val tokenRepository: TokenRepository, private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor, private val settingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor, serverInteractor: GetCurrentServerInteractor,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val factory: RocketChatClientFactory) 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() // TODO - we should validate the current server when opening the app, and have a nonnull get()
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer) private lateinit var client: RocketChatClient
private val settings: PublicSettings = settingsInteractor.get(currentServer) 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 usernameOrEmail: String
private lateinit var password: String private lateinit var password: String
private lateinit var credentialToken: String private lateinit var credentialToken: String
private lateinit var credentialSecret: String private lateinit var credentialSecret: String
private lateinit var deepLinkUserId: String
private lateinit var deepLinkToken: String
fun setupView() { fun setupView() {
setupConnectionInfo(currentServer)
setupLoginView() setupLoginView()
setupUserRegistrationView() setupUserRegistrationView()
setupCasView() setupCasView()
setupOauthServicesView() setupOauthServicesView()
checkServerInfo() checkServerInfo(currentServer)
} }
fun authenticateWithUserAndPassword(usernameOrEmail: String, password: String) { fun authenticateWithUserAndPassword(usernameOrEmail: String, password: String) {
...@@ -84,6 +92,32 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -84,6 +92,32 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
doAuthentication(TYPE_LOGIN_OAUTH) 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() fun signup() = navigator.toSignUp()
private fun setupLoginView() { private fun setupLoginView() {
...@@ -212,8 +246,16 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -212,8 +246,16 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
TYPE_LOGIN_OAUTH -> { TYPE_LOGIN_OAUTH -> {
client.loginWithOauth(credentialToken, credentialSecret) 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 -> { 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")
} }
} }
} }
......
...@@ -17,6 +17,7 @@ import android.widget.ScrollView ...@@ -17,6 +17,7 @@ import android.widget.ScrollView
import androidx.core.view.postDelayed import androidx.core.view.postDelayed
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.R 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.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
...@@ -27,6 +28,7 @@ import chat.rocket.android.webview.cas.ui.casWebViewIntent ...@@ -27,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_SECRET
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN
import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent
import chat.rocket.common.util.ifNull
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.* import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject import javax.inject.Inject
...@@ -41,14 +43,22 @@ class LoginFragment : Fragment(), LoginView { ...@@ -41,14 +43,22 @@ class LoginFragment : Fragment(), LoginView {
areLoginOptionsNeeded() areLoginOptionsNeeded()
} }
private var isGlobalLayoutListenerSetUp = false private var isGlobalLayoutListenerSetUp = false
private var deepLinkInfo: LoginDeepLinkInfo? = null
companion object { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
deepLinkInfo = arguments?.getParcelable(DEEP_LINK_INFO)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
...@@ -61,7 +71,11 @@ class LoginFragment : Fragment(), LoginView { ...@@ -61,7 +71,11 @@ class LoginFragment : Fragment(), LoginView {
tintEditTextDrawableStart() tintEditTextDrawableStart()
} }
presenter.setupView() deepLinkInfo?.let {
presenter.authenticadeWithDeepLink(it)
}.ifNull {
presenter.setupView()
}
} }
override fun onDestroyView() { override fun onDestroyView() {
......
...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.presentation ...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.presentation
import android.content.Intent import android.content.Intent
import chat.rocket.android.R 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.login.ui.LoginFragment
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.authentication.signup.ui.SignupFragment import chat.rocket.android.authentication.signup.ui.SignupFragment
...@@ -21,6 +22,12 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) { ...@@ -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) { fun toTwoFA(username: String, password: String) {
activity.addFragmentBackStack("TwoFAFragment", R.id.fragment_container) { activity.addFragmentBackStack("TwoFAFragment", R.id.fragment_container) {
TwoFAFragment.newInstance(username, password) TwoFAFragment.newInstance(username, password)
......
package chat.rocket.android.authentication.server.presentation 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.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetAccountsInteractor import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor import chat.rocket.android.server.domain.RefreshSettingsInteractor
...@@ -18,6 +20,12 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -18,6 +20,12 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val getAccountsInteractor: GetAccountsInteractor) { private val getAccountsInteractor: GetAccountsInteractor) {
fun connect(server: String) { fun connect(server: String) {
connectToServer(server) {
navigator.toLogin()
}
}
fun connectToServer(server: String, block: () -> Unit) {
if (!server.isValidUrl()) { if (!server.isValidUrl()) {
view.showInvalidServerUrlMessage() view.showInvalidServerUrlMessage()
} else { } else {
...@@ -33,17 +41,19 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -33,17 +41,19 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
try { try {
refreshSettingsInteractor.refresh(server) refreshSettingsInteractor.refresh(server)
serverInteractor.save(server) serverInteractor.save(server)
navigator.toLogin() block()
} catch (ex: Exception) { } catch (ex: Exception) {
ex.message?.let { view.showMessage(ex)
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally { } finally {
view.hideLoading() 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 ...@@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import chat.rocket.android.R 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.ServerPresenter
import chat.rocket.android.authentication.server.presentation.ServerView import chat.rocket.android.authentication.server.presentation.ServerView
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
...@@ -17,17 +18,26 @@ import javax.inject.Inject ...@@ -17,17 +18,26 @@ import javax.inject.Inject
class ServerFragment : Fragment(), ServerView { class ServerFragment : Fragment(), ServerView {
@Inject lateinit var presenter: ServerPresenter @Inject lateinit var presenter: ServerPresenter
private var deepLinkInfo: LoginDeepLinkInfo? = null
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
text_server_url.isCursorVisible = KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView) text_server_url.isCursorVisible = KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)
} }
companion object { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
deepLinkInfo = arguments?.getParcelable(DEEP_LINK_INFO)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
...@@ -37,6 +47,10 @@ class ServerFragment : Fragment(), ServerView { ...@@ -37,6 +47,10 @@ class ServerFragment : Fragment(), ServerView {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
relative_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener) relative_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
setupOnClickListener() setupOnClickListener()
deepLinkInfo?.let {
presenter.deepLink(it)
}
} }
override fun onDestroyView() { override fun onDestroyView() {
......
...@@ -28,7 +28,7 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -28,7 +28,7 @@ class TwoFAFragment : Fragment(), TwoFAView {
private const val PASSWORD = "password" private const val PASSWORD = "password"
fun newInstance(username: String, password: String) = TwoFAFragment().apply { fun newInstance(username: String, password: String) = TwoFAFragment().apply {
arguments = Bundle(1).apply { arguments = Bundle(2).apply {
putString(USERNAME, username) putString(USERNAME, username)
putString(PASSWORD, password) putString(PASSWORD, password)
} }
......
...@@ -6,10 +6,11 @@ import android.os.Bundle ...@@ -6,10 +6,11 @@ import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R 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.presentation.AuthenticationPresenter
import chat.rocket.android.authentication.server.ui.ServerFragment import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.util.extensions.addFragment import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.launchUI
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
...@@ -30,11 +31,13 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -30,11 +31,13 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
setTheme(R.style.AuthenticationTheme) setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val deepLinkInfo = intent.getLoginDeepLinkInfo()
launch(UI + job) { launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false) 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) { if (!authenticated) {
showServerInput(savedInstanceState) showServerInput(savedInstanceState, deepLinkInfo)
} }
} }
} }
...@@ -49,9 +52,9 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -49,9 +52,9 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector return fragmentDispatchingAndroidInjector
} }
fun showServerInput(savedInstanceState: Bundle?) { fun showServerInput(savedInstanceState: Bundle?, deepLinkInfo: LoginDeepLinkInfo?) {
addFragment("ServerFragment", R.id.fragment_container) { addFragment("ServerFragment", R.id.fragment_container) {
ServerFragment.newInstance() ServerFragment.newInstance(deepLinkInfo)
} }
} }
} }
......
...@@ -57,6 +57,10 @@ class ChatRoomAdapter( ...@@ -57,6 +57,10 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_author_attachment) val view = parent.inflate(R.layout.item_author_attachment)
AuthorAttachmentViewHolder(view, actionsListener, reactionListener) AuthorAttachmentViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.COLOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_color_attachment)
ColorAttachmentViewHolder(view, actionsListener, reactionListener)
}
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
} }
...@@ -97,6 +101,7 @@ class ChatRoomAdapter( ...@@ -97,6 +101,7 @@ class ChatRoomAdapter(
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel) is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel) is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
is AuthorAttachmentViewHolder -> holder.bind(dataSet[position] as AuthorAttachmentViewModel) is AuthorAttachmentViewHolder -> holder.bind(dataSet[position] as AuthorAttachmentViewModel)
is ColorAttachmentViewHolder -> holder.bind(dataSet[position] as ColorAttachmentViewModel)
} }
} }
......
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
...@@ -233,9 +233,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -233,9 +233,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
// if y is positive the keyboard is up else it's down // if y is positive the keyboard is up else it's down
recycler_view.post { recycler_view.post {
if (y > 0 || Math.abs(verticalScrollOffset.get()) >= Math.abs(y)) { if (y > 0 || Math.abs(verticalScrollOffset.get()) >= Math.abs(y)) {
recycler_view.scrollBy(0, y) ui { recycler_view.scrollBy(0, y) }
} else { } else {
recycler_view.scrollBy(0, verticalScrollOffset.get()) ui { recycler_view.scrollBy(0, verticalScrollOffset.get()) }
} }
} }
} }
......
...@@ -22,7 +22,8 @@ interface BaseViewModel<out T> { ...@@ -22,7 +22,8 @@ interface BaseViewModel<out T> {
VIDEO_ATTACHMENT(4), VIDEO_ATTACHMENT(4),
AUDIO_ATTACHMENT(5), AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6), 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
...@@ -5,6 +5,7 @@ import android.content.Context ...@@ -5,6 +5,7 @@ import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.text.Html
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan import android.text.style.StyleSpan
...@@ -106,10 +107,23 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -106,10 +107,23 @@ class ViewModelMapper @Inject constructor(private val context: Context,
is FileAttachment -> mapFileAttachment(message, attachment) is FileAttachment -> mapFileAttachment(message, attachment)
is MessageAttachment -> mapMessageAttachment(message, attachment) is MessageAttachment -> mapMessageAttachment(message, attachment)
is AuthorAttachment -> mapAuthorAttachment(message, attachment) is AuthorAttachment -> mapAuthorAttachment(message, attachment)
is ColorAttachment -> mapColorAttachment(message, attachment)
else -> null 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 { private suspend fun mapAuthorAttachment(message: Message, attachment: AuthorAttachment): AuthorAttachmentViewModel {
return with(attachment) { return with(attachment) {
val content = stripMessageQuotes(message) val content = stripMessageQuotes(message)
......
package chat.rocket.android.core.behaviours package chat.rocket.android.core.behaviours
import android.support.annotation.StringRes import android.support.annotation.StringRes
import chat.rocket.common.util.ifNull
interface MessageView { interface MessageView {
...@@ -14,4 +15,12 @@ interface MessageView { ...@@ -14,4 +15,12 @@ interface MessageView {
fun showMessage(message: String) fun showMessage(message: String)
fun showGenericErrorMessage() fun showGenericErrorMessage()
}
fun MessageView.showMessage(ex: Exception) {
ex.message?.let {
showMessage(it)
}.ifNull {
showGenericErrorMessage()
}
} }
\ No newline at end of file
...@@ -16,6 +16,7 @@ import chat.rocket.android.authentication.infraestructure.SharedPreferencesMulti ...@@ -16,6 +16,7 @@ import chat.rocket.android.authentication.infraestructure.SharedPreferencesMulti
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.chatroom.service.MessageService import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.qualifier.ForFresco 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.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
...@@ -73,7 +74,7 @@ class AppModule { ...@@ -73,7 +74,7 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideRocketChatDatabase(context: Application): RocketChatDatabase { 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 @Provides
...@@ -168,9 +169,14 @@ class AppModule { ...@@ -168,9 +169,14 @@ class AppModule {
} }
@Provides @Provides
fun provideSharedPreferences(context: Application): SharedPreferences { fun provideSharedPreferences(context: Application) =
return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE) context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
}
@Provides
@ForMessages
fun provideMessagesSharedPreferences(context: Application) =
context.getSharedPreferences("messages", Context.MODE_PRIVATE)
@Provides @Provides
@Singleton @Singleton
...@@ -225,8 +231,10 @@ class AppModule { ...@@ -225,8 +231,10 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideMessageRepository(context: Application, moshi: Moshi, currentServerInteractor: GetCurrentServerInteractor): MessagesRepository { fun provideMessageRepository(context: Application,
val preferences = context.getSharedPreferences("messages", Context.MODE_PRIVATE) @ForMessages preferences: SharedPreferences,
moshi: Moshi,
currentServerInteractor: GetCurrentServerInteractor): MessagesRepository {
return SharedPreferencesMessagesRepository(preferences, moshi, currentServerInteractor) return SharedPreferencesMessagesRepository(preferences, moshi, currentServerInteractor)
} }
......
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
...@@ -41,7 +41,7 @@ class MainPresenter @Inject constructor( ...@@ -41,7 +41,7 @@ class MainPresenter @Inject constructor(
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor, getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory managerFactory: ConnectionManagerFactory
) : CheckServerPresenter(strategy, client = factory.create(serverInteractor.get()!!), 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)
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
...@@ -56,7 +56,7 @@ class MainPresenter @Inject constructor( ...@@ -56,7 +56,7 @@ class MainPresenter @Inject constructor(
fun toSettings() = navigator.toSettings() fun toSettings() = navigator.toSettings()
fun loadCurrentInfo() { fun loadCurrentInfo() {
checkServerInfo() checkServerInfo(currentServer)
launchUI(strategy) { launchUI(strategy) {
try { try {
val me = retryIO("me") { val me = retryIO("me") {
......
package chat.rocket.android.server.domain package chat.rocket.android.server.domain
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.core.internal.rest.settings import chat.rocket.core.internal.rest.settings
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.async import kotlinx.coroutines.experimental.async
...@@ -27,7 +28,9 @@ class RefreshSettingsInteractor @Inject constructor(private val factory: RocketC ...@@ -27,7 +28,9 @@ class RefreshSettingsInteractor @Inject constructor(private val factory: RocketC
suspend fun refresh(server: String) { suspend fun refresh(server: String) {
withContext(CommonPool) { withContext(CommonPool) {
factory.create(server).let { client -> factory.create(server).let { client ->
val settings = client.settings(*settingsFilter) val settings = retryIO(description = "settings", times = 5) {
client.settings(*settingsFilter)
}
repository.save(server, settings) repository.save(server, settings)
} }
} }
......
...@@ -3,33 +3,40 @@ package chat.rocket.android.server.presentation ...@@ -3,33 +3,40 @@ package chat.rocket.android.server.presentation
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.authentication.server.presentation.VersionCheckView import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.lifecycle.CancelStrategy 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.VersionInfo
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.serverInfo 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 import timber.log.Timber
abstract class CheckServerPresenter constructor(private val strategy: CancelStrategy, abstract class CheckServerPresenter constructor(private val strategy: CancelStrategy,
private val client: RocketChatClient, private val factory: RocketChatClientFactory,
private val view: VersionCheckView) { private val view: VersionCheckView) {
internal fun checkServerInfo() { private lateinit var currentServer: String
launchUI(strategy) { private val client: RocketChatClient by lazy {
factory.create(currentServer)
}
internal fun checkServerInfo(serverUrl: String): Job {
return launchUI(strategy) {
try { try {
val serverInfo = retryIO(description = "serverInfo", times = 5) { client.serverInfo() } val version = checkServerVersion(serverUrl).await()
val thisServerVersion = serverInfo.version when (version) {
val isRequiredVersion = isRequiredServerVersion(thisServerVersion) is Version.VersionOk -> {
val isRecommendedVersion = isRecommendedServerVersion(thisServerVersion) Timber.i("Your version is nice! (Requires: 0.62.0, Yours: ${version.version})")
if (isRequiredVersion) { }
if (isRecommendedVersion) { is Version.RecommendedVersionWarning -> {
Timber.i("Your version is nice! (Requires: 0.62.0, Yours: $thisServerVersion)") Timber.i("Your server ${version.version} is bellow recommended version ${BuildConfig.RECOMMENDED_SERVER_VERSION}")
} else {
view.alertNotRecommendedVersion() view.alertNotRecommendedVersion()
} }
} else { is Version.OutOfDateError -> {
if (!isRecommendedVersion) { Timber.i("Oops. Looks like your server ${version.version} is out-of-date! Minimum server version required ${BuildConfig.REQUIRED_SERVER_VERSION}!")
view.blockAndAlertNotRequiredVersion() view.blockAndAlertNotRequiredVersion()
Timber.i("Oops. Looks like your server is out-of-date! Minimum server version required ${BuildConfig.REQUIRED_SERVER_VERSION}!")
} }
} }
} catch (ex: Exception) { } catch (ex: Exception) {
...@@ -38,6 +45,26 @@ abstract class CheckServerPresenter constructor(private val strategy: CancelStra ...@@ -38,6 +45,26 @@ abstract class CheckServerPresenter constructor(private val strategy: CancelStra
} }
} }
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)
val isRecommendedVersion = isRecommendedServerVersion(thisServerVersion)
if (isRequiredVersion) {
if (isRecommendedVersion) {
Timber.i("Your version is nice! (Requires: 0.62.0, Yours: $thisServerVersion)")
return@async Version.VersionOk(thisServerVersion)
} else {
return@async Version.RecommendedVersionWarning(thisServerVersion)
}
} else {
return@async Version.OutOfDateError(thisServerVersion)
}
}
}
private fun isRequiredServerVersion(version: String): Boolean { private fun isRequiredServerVersion(version: String): Boolean {
return isMinimumVersion(version, getVersionDistilled(BuildConfig.REQUIRED_SERVER_VERSION)) return isMinimumVersion(version, getVersionDistilled(BuildConfig.REQUIRED_SERVER_VERSION))
} }
...@@ -92,4 +119,10 @@ abstract class CheckServerPresenter constructor(private val strategy: CancelStra ...@@ -92,4 +119,10 @@ abstract class CheckServerPresenter constructor(private val strategy: CancelStra
0 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,53 +8,61 @@ ...@@ -8,53 +8,61 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
tools:context=".profile.ui.ProfileFragment"> tools:context=".profile.ui.ProfileFragment">
<LinearLayout <ScrollView
android:id="@+id/profile_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<include <LinearLayout
android:id="@+id/layout_avatar_profile" android:id="@+id/profile_container"
layout="@layout/avatar_profile" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="32dp" /> android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<include
android:id="@+id/layout_avatar_profile"
layout="@layout/avatar_profile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp" />
<EditText
android:id="@+id/text_name"
style="@style/Profile.EditText"
android:layout_marginTop="32dp"
android:drawableStart="@drawable/ic_person_black_24dp"
android:hint="@string/msg_name"
android:inputType="textCapWords" />
<EditText <EditText
android:id="@+id/text_name" android:id="@+id/text_username"
style="@style/Profile.EditText" style="@style/Profile.EditText"
android:layout_marginTop="32dp" android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_person_black_24dp" android:drawableStart="@drawable/ic_at_black_24dp"
android:hint="@string/msg_name" android:hint="@string/msg_username"
android:inputType="textCapWords" /> android:inputType="text" />
<EditText <EditText
android:id="@+id/text_username" android:id="@+id/text_email"
style="@style/Profile.EditText" style="@style/Profile.EditText"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_at_black_24dp" android:drawableStart="@drawable/ic_email_black_24dp"
android:hint="@string/msg_username" android:hint="@string/msg_email"
android:inputType="text" /> android:inputType="textEmailAddress" />
<EditText <EditText
android:id="@+id/text_email" android:id="@+id/text_avatar_url"
style="@style/Profile.EditText" style="@style/Profile.EditText"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_email_black_24dp" android:drawableStart="@drawable/ic_link_black_24dp"
android:hint="@string/msg_email" android:hint="@string/msg_avatar_url"
android:inputType="textEmailAddress" /> android:inputType="text"
android:layout_marginBottom="16dp"/>
</LinearLayout>
<EditText </ScrollView>
android:id="@+id/text_avatar_url"
style="@style/Profile.EditText"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_link_black_24dp"
android:hint="@string/msg_avatar_url"
android:inputType="text" />
</LinearLayout>
<com.wang.avi.AVLoadingIndicatorView <com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading" android:id="@+id/view_loading"
......
<?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