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 @@ ...@@ -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
...@@ -102,13 +116,18 @@ ...@@ -102,13 +116,18 @@
<action android:name="com.google.android.c2dm.intent.RECEIVE" /> <action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".chatroom.service.MessageService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<meta-data <meta-data
android:name="io.fabric.ApiKey" android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" /> android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
<activity android:name=".settings.about.ui.AboutActivity" <activity
android:theme="@style/AppTheme"/> android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme" />
</application> </application>
</manifest> </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 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")
} }
} }
} }
......
...@@ -14,8 +14,10 @@ import android.view.ViewGroup ...@@ -14,8 +14,10 @@ import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ScrollView import android.widget.ScrollView
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
...@@ -26,6 +28,7 @@ import chat.rocket.android.webview.cas.ui.casWebViewIntent ...@@ -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_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
...@@ -40,14 +43,22 @@ class LoginFragment : Fragment(), LoginView { ...@@ -40,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? =
...@@ -60,7 +71,11 @@ class LoginFragment : Fragment(), LoginView { ...@@ -60,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() {
...@@ -377,19 +392,23 @@ class LoginFragment : Fragment(), LoginView { ...@@ -377,19 +392,23 @@ class LoginFragment : Fragment(), LoginView {
} }
private fun showRemainingSocialAccountsView() { private fun showRemainingSocialAccountsView() {
social_accounts_container.postDelayed({ social_accounts_container.postDelayed(300) {
(0..social_accounts_container.childCount) ui {
.mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton } (0..social_accounts_container.childCount)
.filter { it.isClickable } .mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
.forEach { ui { it.setVisible(true) }} .filter { it.isClickable }
}, 1000) .forEach { it.setVisible(true) }
}
}
} }
// Scrolling to the bottom of the screen. // Scrolling to the bottom of the screen.
private fun scrollToBottom() { private fun scrollToBottom() {
scroll_view.postDelayed({ scroll_view.postDelayed(1250) {
ui { scroll_view.fullScroll(ScrollView.FOCUS_DOWN) } ui {
}, 1250) scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}
}
} }
......
...@@ -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
...@@ -16,7 +18,14 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -16,7 +18,14 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val serverInteractor: SaveCurrentServerInteractor, private val serverInteractor: SaveCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor, private val refreshSettingsInteractor: RefreshSettingsInteractor,
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 {
...@@ -32,17 +41,19 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -32,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)
} }
} }
...@@ -117,12 +122,22 @@ class ChatRoomAdapter( ...@@ -117,12 +122,22 @@ class ChatRoomAdapter(
} }
fun prependData(dataSet: List<BaseViewModel<*>>) { 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 this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1
} }
if (item == null) { if (item == -1) {
this.dataSet.addAll(0, dataSet) this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size) 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 package chat.rocket.android.chatroom.adapter
import android.graphics.Color
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.MessageViewModel
...@@ -29,6 +30,9 @@ class MessageViewHolder( ...@@ -29,6 +30,9 @@ class MessageViewHolder(
text_sender.text = data.senderName text_sender.text = data.senderName
text_content.text = data.content text_content.text = data.content
image_avatar.setImageURI(data.avatar) 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
...@@ -92,10 +92,8 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -92,10 +92,8 @@ interface ChatRoomView : LoadingView, MessageView {
/** /**
* Enables the send message button. * Enables the send message button.
*
* @param sendFailed Whether the sent message has failed.
*/ */
fun enableSendMessageButton(sendFailed: Boolean) fun enableSendMessageButton()
/** /**
* Clears the message composition. * 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 ...@@ -18,7 +18,6 @@ import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.* import android.view.*
import androidx.core.content.systemService
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.* import chat.rocket.android.chatroom.adapter.*
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
...@@ -70,8 +69,10 @@ private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen" ...@@ -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" private const val BUNDLE_CHAT_ROOM_IS_SUBSCRIBED = "chat_room_is_subscribed"
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener { class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
@Inject lateinit var presenter: ChatRoomPresenter @Inject
@Inject lateinit var parser: MessageParser lateinit var presenter: ChatRoomPresenter
@Inject
lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter private lateinit var adapter: ChatRoomAdapter
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
...@@ -207,7 +208,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -207,7 +208,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (recycler_view.adapter == null) { if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter, adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter,
reactionListener = this@ChatRoomFragment) reactionListener = this@ChatRoomFragment)
recycler_view.adapter = adapter recycler_view.adapter = adapter
if (dataSet.size >= 30) { if (dataSet.size >= 30) {
recycler_view.addOnScrollListener(endlessRecyclerViewScrollListener) recycler_view.addOnScrollListener(endlessRecyclerViewScrollListener)
...@@ -232,9 +233,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -232,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()) }
} }
} }
} }
...@@ -316,16 +317,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -316,16 +317,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun enableSendMessageButton(sendFailed: Boolean) { override fun enableSendMessageButton() {
ui { ui {
button_send.isEnabled = true button_send.isEnabled = true
text_message.isEnabled = true text_message.isEnabled = true
if (!sendFailed) { clearMessageComposition()
clearMessageComposition()
}
} }
} }
override fun clearMessageComposition() { override fun clearMessageComposition() {
ui { ui {
citation = null citation = null
...@@ -408,7 +408,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -408,7 +408,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun copyToClipboard(message: String) { override fun copyToClipboard(message: String) {
ui { ui {
val clipboard: ClipboardManager = it.systemService() val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText("", message) clipboard.primaryClip = ClipData.newPlainText("", message)
} }
} }
...@@ -469,10 +469,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -469,10 +469,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun showFileSelection(filter: Array<String>) { override fun showFileSelection(filter: Array<String>) {
ui { ui {
if (ContextCompat.checkSelfPermission(it, Manifest.permission.READ_EXTERNAL_STORAGE) if (ContextCompat.checkSelfPermission(it, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) { != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(it, ActivityCompat.requestPermissions(it,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
1) 1)
} else { } else {
val intent = Intent(Intent.ACTION_GET_CONTENT) val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*" intent.type = "*/*"
...@@ -536,7 +536,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -536,7 +536,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
recycler_view.layoutManager = linearLayoutManager recycler_view.layoutManager = linearLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
endlessRecyclerViewScrollListener = object : endlessRecyclerViewScrollListener = object :
EndlessRecyclerViewScrollListener(recycler_view.layoutManager as LinearLayoutManager) { EndlessRecyclerViewScrollListener(recycler_view.layoutManager as LinearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) { override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
presenter.loadMessages(chatRoomId, chatRoomType, page * 30L) presenter.loadMessages(chatRoomId, chatRoomType, page * 30L)
} }
...@@ -589,6 +589,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -589,6 +589,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
var textMessage = citation ?: "" var textMessage = citation ?: ""
textMessage += text_message.textContent textMessage += text_message.textContent
sendMessage(textMessage) sendMessage(textMessage)
clearMessageComposition()
} }
button_show_attachment_options.setOnClickListener { button_show_attachment_options.setOnClickListener {
...@@ -621,23 +622,23 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -621,23 +622,23 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private fun setupSuggestionsView() { private fun setupSuggestionsView() {
suggestions_view.anchorTo(text_message) suggestions_view.anchorTo(text_message)
.setMaximumHeight(resources.getDimensionPixelSize(R.dimen.suggestions_box_max_height)) .setMaximumHeight(resources.getDimensionPixelSize(R.dimen.suggestions_box_max_height))
.addTokenAdapter(PeopleSuggestionsAdapter(context!!)) .addTokenAdapter(PeopleSuggestionsAdapter(context!!))
.addTokenAdapter(CommandSuggestionsAdapter()) .addTokenAdapter(CommandSuggestionsAdapter())
.addTokenAdapter(RoomSuggestionsAdapter()) .addTokenAdapter(RoomSuggestionsAdapter())
.addSuggestionProviderAction("@") { query -> .addSuggestionProviderAction("@") { query ->
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
presenter.spotlight(query, PEOPLE, true) presenter.spotlight(query, PEOPLE, true)
}
} }
.addSuggestionProviderAction("#") { query -> }
if (query.isNotEmpty()) { .addSuggestionProviderAction("#") { query ->
presenter.loadChatRooms() if (query.isNotEmpty()) {
} presenter.loadChatRooms()
}
.addSuggestionProviderAction("/") { _ ->
presenter.loadCommands()
} }
}
.addSuggestionProviderAction("/") { _ ->
presenter.loadCommands()
}
presenter.loadCommands() presenter.loadCommands()
} }
...@@ -672,7 +673,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -672,7 +673,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private fun subscribeTextMessage() { private fun subscribeTextMessage() {
val disposable = text_message.asObservable(0) val disposable = text_message.asObservable(0)
.subscribe({ t -> setupComposeMessageButtons(t) }) .subscribe({ t -> setupComposeMessageButtons(t) })
compositeDisposable.add(disposable) compositeDisposable.add(disposable)
} }
......
...@@ -13,7 +13,8 @@ data class AudioAttachmentViewModel( ...@@ -13,7 +13,8 @@ data class AudioAttachmentViewModel(
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<AudioAttachment> { ) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
......
...@@ -5,17 +5,18 @@ import chat.rocket.core.model.Message ...@@ -5,17 +5,18 @@ import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.AuthorAttachment import chat.rocket.core.model.attachment.AuthorAttachment
data class AuthorAttachmentViewModel( data class AuthorAttachmentViewModel(
override val attachmentUrl: String, override val attachmentUrl: String,
val id: Long, val id: Long,
val name: CharSequence?, val name: CharSequence?,
val icon: String?, val icon: String?,
val fields: CharSequence?, val fields: CharSequence?,
override val message: Message, override val message: Message,
override val rawData: AuthorAttachment, override val rawData: AuthorAttachment,
override val messageId: String, override val messageId: String,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseAttachmentViewModel<AuthorAttachment> { ) : BaseAttachmentViewModel<AuthorAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.AUTHOR_ATTACHMENT.viewType get() = BaseViewModel.ViewType.AUTHOR_ATTACHMENT.viewType
......
...@@ -12,6 +12,7 @@ interface BaseViewModel<out T> { ...@@ -12,6 +12,7 @@ interface BaseViewModel<out T> {
var reactions: List<ReactionViewModel> var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>? var nextDownStreamMessage: BaseViewModel<*>?
var preview: Message? var preview: Message?
var isTemporary: Boolean
enum class ViewType(val viewType: Int) { enum class ViewType(val viewType: Int) {
MESSAGE(0), MESSAGE(0),
...@@ -21,7 +22,8 @@ interface BaseViewModel<out T> { ...@@ -21,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
...@@ -13,7 +13,8 @@ data class ImageAttachmentViewModel( ...@@ -13,7 +13,8 @@ data class ImageAttachmentViewModel(
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<ImageAttachment> { ) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
...@@ -14,7 +14,8 @@ data class MessageAttachmentViewModel( ...@@ -14,7 +14,8 @@ data class MessageAttachmentViewModel(
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null, var messageLink: String? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Message> { ) : BaseViewModel<Message> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
......
...@@ -15,7 +15,8 @@ data class MessageViewModel( ...@@ -15,7 +15,8 @@ data class MessageViewModel(
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null, override var preview: Message? = null,
var isFirstUnread: Boolean var isFirstUnread: Boolean,
override var isTemporary: Boolean = false
) : BaseMessageViewModel<Message> { ) : BaseMessageViewModel<Message> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType get() = BaseViewModel.ViewType.MESSAGE.viewType
......
...@@ -14,7 +14,8 @@ data class UrlPreviewViewModel( ...@@ -14,7 +14,8 @@ data class UrlPreviewViewModel(
val thumbUrl: String?, val thumbUrl: String?,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Url> { ) : BaseViewModel<Url> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
...@@ -13,7 +13,8 @@ data class VideoAttachmentViewModel( ...@@ -13,7 +13,8 @@ data class VideoAttachmentViewModel(
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<VideoAttachment> { ) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
...@@ -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)
...@@ -212,12 +226,13 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -212,12 +226,13 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val time = getTime(message.timestamp) val time = getTime(message.timestamp)
val avatar = getUserAvatar(message) val avatar = getUserAvatar(message)
val preview = mapMessagePreview(message) val preview = mapMessagePreview(message)
val isTemp = message.isTemporary ?: false
val content = getContent(stripMessageQuotes(message)) val content = getContent(stripMessageQuotes(message))
MessageViewModel(message = stripMessageQuotes(message), rawData = message, MessageViewModel(message = stripMessageQuotes(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender, messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, reactions = getReactions(message), 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 { private suspend fun mapMessagePreview(message: Message): 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
...@@ -2,6 +2,7 @@ package chat.rocket.android.dagger ...@@ -2,6 +2,7 @@ package chat.rocket.android.dagger
import android.app.Application import android.app.Application
import chat.rocket.android.app.RocketChatApplication 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.ActivityBuilder
import chat.rocket.android.dagger.module.AppModule import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.dagger.module.ReceiverBuilder import chat.rocket.android.dagger.module.ReceiverBuilder
...@@ -29,6 +30,8 @@ interface AppComponent { ...@@ -29,6 +30,8 @@ interface AppComponent {
fun inject(service: FirebaseTokenService) fun inject(service: FirebaseTokenService)
fun inject(service: MessageService)
/*@Component.Builder /*@Component.Builder
abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/ abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/
} }
...@@ -2,16 +2,20 @@ package chat.rocket.android.dagger.module ...@@ -2,16 +2,20 @@ package chat.rocket.android.dagger.module
import android.app.Application import android.app.Application
import android.app.NotificationManager import android.app.NotificationManager
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.arch.persistence.room.Room import android.arch.persistence.room.Room
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.systemService
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
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.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
...@@ -20,11 +24,39 @@ import chat.rocket.android.push.GroupedPush ...@@ -20,11 +24,39 @@ import chat.rocket.android.push.GroupedPush
import chat.rocket.android.push.PushManager import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.* 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.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter 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.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.AttachmentAdapterFactory
import com.facebook.drawee.backends.pipeline.DraweeConfig import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.core.ImagePipelineConfig
...@@ -64,7 +96,7 @@ class AppModule { ...@@ -64,7 +96,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
...@@ -114,7 +146,7 @@ class AppModule { ...@@ -114,7 +146,7 @@ class AppModule {
@Provides @Provides
@ForFresco @ForFresco
@Singleton @Singleton
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor { fun provideFrescoAuthInterceptor(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
return FrescoAuthInterceptor(tokenRepository, currentServerInteractor) return FrescoAuthInterceptor(tokenRepository, currentServerInteractor)
} }
...@@ -159,9 +191,14 @@ class AppModule { ...@@ -159,9 +191,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
...@@ -201,11 +238,26 @@ class AppModule { ...@@ -201,11 +238,26 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideMoshi(): Moshi { fun provideMoshi(
logger: PlatformLogger,
currentServerInteractor: GetCurrentServerInteractor
): Moshi {
val url = currentServerInteractor.get() ?: ""
return Moshi.Builder() return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY) .add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE) .add(AppJsonAdapterFactory.INSTANCE)
.build() .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()
} }
@Provides @Provides
...@@ -216,8 +268,10 @@ class AppModule { ...@@ -216,8 +268,10 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideMessageRepository(): MessagesRepository { fun provideMessageRepository(@ForMessages preferences: SharedPreferences,
return MemoryMessagesRepository() moshi: Moshi,
currentServerInteractor: GetCurrentServerInteractor): MessagesRepository {
return SharedPreferencesMessagesRepository(preferences, moshi, currentServerInteractor)
} }
@Provides @Provides
...@@ -260,7 +314,8 @@ class AppModule { ...@@ -260,7 +314,8 @@ class AppModule {
SharedPreferencesAccountsRepository(preferences, moshi) SharedPreferencesAccountsRepository(preferences, moshi)
@Provides @Provides
fun provideNotificationManager(context: Context): NotificationManager = context.systemService() fun provideNotificationManager(context: Application) =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Provides @Provides
@Singleton @Singleton
...@@ -269,7 +324,7 @@ class AppModule { ...@@ -269,7 +324,7 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun providePushManager( fun providePushManager(
context: Context, context: Application,
groupedPushes: GroupedPush, groupedPushes: GroupedPush,
manager: NotificationManager, manager: NotificationManager,
moshi: Moshi, moshi: Moshi,
...@@ -277,4 +332,22 @@ class AppModule { ...@@ -277,4 +332,22 @@ class AppModule {
getSettingsInteractor: GetSettingsInteractor): PushManager { getSettingsInteractor: GetSettingsInteractor): PushManager {
return PushManager(groupedPushes, manager, moshi, getAccountInteractor, getSettingsInteractor, context) 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 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.FirebaseTokenService
import chat.rocket.android.push.GcmListenerService import chat.rocket.android.push.GcmListenerService
import chat.rocket.android.push.di.FirebaseTokenServiceProvider import chat.rocket.android.push.di.FirebaseTokenServiceProvider
...@@ -14,4 +16,7 @@ import dagger.android.ContributesAndroidInjector ...@@ -14,4 +16,7 @@ import dagger.android.ContributesAndroidInjector
@ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class]) @ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class])
abstract fun bindGcmListenerService(): GcmListenerService 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 { ...@@ -24,4 +24,5 @@ interface LocalRepository {
} }
} }
fun LocalRepository.checkIfMyself(username: String) = get(LocalRepository.CURRENT_USERNAME_KEY) == username fun LocalRepository.checkIfMyself(username: String) = username() == username
\ No newline at end of file fun LocalRepository.username() = get(LocalRepository.CURRENT_USERNAME_KEY)
\ No newline at end of file
...@@ -23,27 +23,25 @@ import chat.rocket.core.internal.rest.logout ...@@ -23,27 +23,25 @@ import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MainPresenter @Inject constructor( class MainPresenter @Inject constructor(
private val view: MainView, private val view: MainView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: MainNavigator, private val navigator: MainNavigator,
private val tokenRepository: TokenRepository, private val tokenRepository: TokenRepository,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val navHeaderMapper: NavHeaderViewModelMapper, private val navHeaderMapper: NavHeaderViewModelMapper,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInterector: RemoveAccountInterector, private val removeAccountInteractor: RemoveAccountInteractor,
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)
...@@ -58,7 +56,7 @@ class MainPresenter @Inject constructor( ...@@ -58,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") {
...@@ -105,7 +103,7 @@ class MainPresenter @Inject constructor( ...@@ -105,7 +103,7 @@ class MainPresenter @Inject constructor(
try { try {
disconnect() disconnect()
removeAccountInterector.remove(currentServer) removeAccountInteractor.remove(currentServer)
tokenRepository.remove(currentServer) tokenRepository.remove(currentServer)
navigator.toNewServer() navigator.toNewServer()
} catch (ex: Exception) { } catch (ex: Exception) {
......
package chat.rocket.android.profile.presentation package chat.rocket.android.profile.presentation
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.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
...@@ -36,11 +37,7 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView, ...@@ -36,11 +37,7 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView,
myself.emails?.get(0)?.address!! myself.emails?.get(0)?.address!!
) )
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
exception.message?.let { view.showMessage(exception)
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally { } finally {
view.hideLoading() view.hideLoading()
} }
......
...@@ -12,6 +12,7 @@ import chat.rocket.android.profile.presentation.ProfilePresenter ...@@ -12,6 +12,7 @@ import chat.rocket.android.profile.presentation.ProfilePresenter
import chat.rocket.android.profile.presentation.ProfileView import chat.rocket.android.profile.presentation.ProfileView
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.Observables import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.app_bar.* import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.avatar_profile.* import kotlinx.android.synthetic.main.avatar_profile.*
...@@ -25,6 +26,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -25,6 +26,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private lateinit var currentEmail: String private lateinit var currentEmail: String
private lateinit var currentAvatar: String private lateinit var currentAvatar: String
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private val disposables = CompositeDisposable()
companion object { companion object {
fun newInstance() = ProfileFragment() fun newInstance() = ProfileFragment()
...@@ -48,22 +50,29 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -48,22 +50,29 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
presenter.loadUserProfile() presenter.loadUserProfile()
} }
override fun onDestroyView() {
disposables.clear()
super.onDestroyView()
}
override fun showProfile(avatarUrl: String, name: String, username: String, email: String) { override fun showProfile(avatarUrl: String, name: String, username: String, email: String) {
image_avatar.setImageURI(avatarUrl) ui {
image_avatar.setImageURI(avatarUrl)
text_name.textContent = name text_name.textContent = name
text_username.textContent = username text_username.textContent = username
text_email.textContent = email text_email.textContent = email
text_avatar_url.textContent = "" text_avatar_url.textContent = ""
currentName = name currentName = name
currentUsername = username currentUsername = username
currentEmail = email currentEmail = email
currentAvatar = avatarUrl currentAvatar = avatarUrl
profile_container.setVisible(true) profile_container.setVisible(true)
listenToChanges() listenToChanges()
}
} }
override fun showProfileUpdateSuccessfullyMessage() { override fun showProfileUpdateSuccessfullyMessage() {
...@@ -72,22 +81,30 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -72,22 +81,30 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
override fun showLoading() { override fun showLoading() {
enableUserInput(false) enableUserInput(false)
view_loading.setVisible(true) ui {
view_loading.setVisible(true)
}
} }
override fun hideLoading() { override fun hideLoading() {
if (view_loading != null) { ui {
view_loading.setVisible(false) if (view_loading != null) {
view_loading.setVisible(false)
}
} }
enableUserInput(true) enableUserInput(true)
} }
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
showToast(resId) ui {
showToast(resId)
}
} }
override fun showMessage(message: String) { override fun showMessage(message: String) {
showToast(message) ui {
showToast(message)
}
} }
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
...@@ -136,7 +153,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -136,7 +153,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
} }
private fun listenToChanges() { private fun listenToChanges() {
Observables.combineLatest(text_name.asObservable(), disposables.add(Observables.combineLatest(text_name.asObservable(),
text_username.asObservable(), text_username.asObservable(),
text_email.asObservable(), text_email.asObservable(),
text_avatar_url.asObservable()) { text_name, text_username, text_email, text_avatar_url -> text_avatar_url.asObservable()) { text_name, text_username, text_email, text_avatar_url ->
...@@ -144,7 +161,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -144,7 +161,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_username.toString() != currentUsername || text_username.toString() != currentUsername ||
text_email.toString() != currentEmail || text_email.toString() != currentEmail ||
(text_avatar_url.toString() != "" && text_avatar_url.toString() != currentAvatar)) (text_avatar_url.toString() != "" && text_avatar_url.toString() != currentAvatar))
}.subscribe({ isValid -> }.subscribe { isValid ->
if (isValid) { if (isValid) {
startActionMode() startActionMode()
} else { } else {
...@@ -162,9 +179,11 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -162,9 +179,11 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private fun finishActionMode() = actionMode?.finish() private fun finishActionMode() = actionMode?.finish()
private fun enableUserInput(value: Boolean) { private fun enableUserInput(value: Boolean) {
text_username.isEnabled = value ui {
text_username.isEnabled = value text_username.isEnabled = value
text_email.isEnabled = value text_username.isEnabled = value
text_avatar_url.isEnabled = value text_email.isEnabled = value
text_avatar_url.isEnabled = value
}
} }
} }
...@@ -4,6 +4,7 @@ import chat.rocket.android.R ...@@ -4,6 +4,7 @@ import chat.rocket.android.R
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.registerPushToken
import com.google.android.gms.gcm.GoogleCloudMessaging import com.google.android.gms.gcm.GoogleCloudMessaging
...@@ -32,23 +33,27 @@ class FirebaseTokenService : FirebaseInstanceIdService() { ...@@ -32,23 +33,27 @@ class FirebaseTokenService : FirebaseInstanceIdService() {
override fun onTokenRefresh() { override fun onTokenRefresh() {
//TODO: We need to use the Cordova Project gcm_sender_id since it's the one configured on RC //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. // default push gateway. We should register this project's own project sender id into it.
val gcmToken = InstanceID.getInstance(this) try {
.getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null) val gcmToken = InstanceID.getInstance(this)
val currentServer = getCurrentServerInteractor.get() .getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
val client = currentServer?.let { factory.create(currentServer) } val currentServer = getCurrentServerInteractor.get()
val client = currentServer?.let { factory.create(currentServer) }
gcmToken?.let { gcmToken?.let {
localRepository.save(LocalRepository.KEY_PUSH_TOKEN, gcmToken) localRepository.save(LocalRepository.KEY_PUSH_TOKEN, gcmToken)
client?.let { client?.let {
launch { launch {
try { try {
Timber.d("Registering push token: $gcmToken for ${client.url}") Timber.d("Registering push token: $gcmToken for ${client.url}")
client.registerPushToken(gcmToken) retryIO("register push token") { client.registerPushToken(gcmToken) }
} catch (ex: RocketChatException) { } 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 { ...@@ -11,7 +11,7 @@ interface MessagesRepository {
* *
* @return The Message object given by the id or null if message wasn't found. * @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. * Get all messages from the current room id.
...@@ -19,7 +19,7 @@ interface MessagesRepository { ...@@ -19,7 +19,7 @@ interface MessagesRepository {
* @param rid The room id. * @param rid The room id.
* @return A list of Message objects for the room with given room id or an empty list. * @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. * Get most recent messages up to count different users.
...@@ -29,43 +29,47 @@ interface MessagesRepository { ...@@ -29,43 +29,47 @@ interface MessagesRepository {
* *
* @return List of last count messages. * @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! * Get all messages. Use carefully!
* *
* @return All messages or an empty list. * @return All messages or an empty list.
*/ */
fun getAll(): List<Message> suspend fun getAll(): List<Message>
/** /**
* Save a single message object. * Save a single message object.
* *
* @param The message object to saveAll. * @param The message object to saveAll.
*/ */
fun save(message: Message) suspend fun save(message: Message)
/** /**
* Save a list of messages. * Save a list of messages.
*/ */
fun saveAll(newMessages: List<Message>) suspend fun saveAll(newMessages: List<Message>)
/** /**
* Removes all messages. * Removes all messages.
*/ */
fun clear() suspend fun clear()
/** /**
* Remove message by id. * Remove message by id.
* *
* @param id The id of the message to remove. * @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. * Remove all messages from a given room.
* *
* @param rid The room id where messages are to be removed. * @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 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)
} }
} }
......
...@@ -2,7 +2,7 @@ package chat.rocket.android.server.domain ...@@ -2,7 +2,7 @@ package chat.rocket.android.server.domain
import javax.inject.Inject import javax.inject.Inject
class RemoveAccountInterector @Inject constructor(val repository: AccountsRepository) { class RemoveAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun remove(serverUrl: String) { suspend fun remove(serverUrl: String) {
repository.remove(serverUrl) 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 ...@@ -2,45 +2,67 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.MessagesRepository import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
class MemoryMessagesRepository : MessagesRepository { class MemoryMessagesRepository : MessagesRepository {
private val messages: HashMap<String, Message> = HashMap() private val messages: HashMap<String, Message> = HashMap()
override fun getById(id: String): Message? { override suspend fun getById(id: String): Message? = withContext(CommonPool) {
return messages[id] return@withContext messages[id]
} }
override fun getByRoomId(rid: String): List<Message> { override suspend fun getByRoomId(rid: String): List<Message> = withContext(CommonPool) {
return messages.filter { it.value.roomId == rid }.values.toList() return@withContext messages.filter { it.value.roomId == rid }.values.toList()
} }
override fun getRecentMessages(rid: String, count: Long): List<Message> { override suspend fun getRecentMessages(rid: String, count: Long): List<Message> = withContext(CommonPool) {
return getByRoomId(rid).sortedByDescending { it.timestamp } return@withContext getByRoomId(rid).sortedByDescending { it.timestamp }
.distinctBy { it.sender }.take(count.toInt()) .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 messages[message.id] = message
} }
override fun saveAll(newMessages: List<Message>) { override suspend fun saveAll(newMessages: List<Message>) = withContext(CommonPool) {
for (msg in newMessages) { for (msg in newMessages) {
messages[msg.id] = msg messages[msg.id] = msg
} }
} }
override fun clear() { override suspend fun clear() = withContext(CommonPool) {
messages.clear() messages.clear()
} }
override fun removeById(id: String) { override suspend fun removeById(id: String) {
messages.remove(id) 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 val roomMessages = messages.filter { it.value.roomId == rid }.values
roomMessages.forEach { roomMessages.forEach {
messages.remove(it.roomId) 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,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