Commit 5f4cce90 authored by Divyanshu Bhargava's avatar Divyanshu Bhargava

Merge remote-tracking branch 'origin/lang-hindi' into lang-hindi

parents 26641276 7b0dda78
......@@ -3,6 +3,7 @@ apply plugin: 'io.fabric'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
android {
compileSdkVersion versions.compileSdk
......
......@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
......@@ -35,6 +35,10 @@
</intent-filter>
</activity>
<activity
android:name=".server.ui.ChangeServerActivity"
android:theme="@style/AuthenticationTheme" />
<activity
android:name=".main.ui.MainActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
......@@ -50,6 +54,11 @@
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".webview.oauth.ui.OauthWebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".chatroom.ui.ChatRoomActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
......
package chat.rocket.android.app.migration
import chat.rocket.android.BuildConfig
import chat.rocket.android.app.migration.model.RealmUser
import io.realm.DynamicRealm
import io.realm.RealmMigration
class RealmMigration : RealmMigration {
override fun migrate(dynamicRealm: DynamicRealm, oldVersion: Long, newVersion: Long) {
var oldVersion = oldVersion
val schema = dynamicRealm.schema
if (oldVersion == 0L) {
// NOOP
oldVersion++
}
if (oldVersion == 1L) {
oldVersion++
}
if (oldVersion == 2L) {
oldVersion++
}
if (oldVersion == 3L) {
oldVersion++
}
if (oldVersion == 4L) {
oldVersion++
}
if (oldVersion == 5L) {
val userSchema = schema.get("RealmUser")
try {
userSchema?.addField(RealmUser.NAME, String::class.java)
} catch (e: IllegalArgumentException) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
// ignore; it makes here if the schema for this model was already update before without migration
}
}
}
// hack around to avoid "new different configuration cannot access the same file" error
override fun hashCode(): Int {
return 37
}
override fun equals(o: Any?): Boolean {
return o is chat.rocket.android.app.migration.RealmMigration
}
// end hack
}
\ No newline at end of file
package chat.rocket.android.app.migration
import io.realm.annotations.RealmModule
@RealmModule(library = true, allClasses = true)
class RocketChatLibraryModule
\ No newline at end of file
package chat.rocket.android.app.migration
import chat.rocket.android.app.migration.model.RealmBasedServerInfo
import io.realm.annotations.RealmModule
@RealmModule(library = true, classes = arrayOf(RealmBasedServerInfo::class))
class RocketChatServerModule
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmBasedServerInfo : RealmObject() {
@PrimaryKey
@JvmField var hostname: String? = null
@JvmField var name: String? = null
@JvmField var session: String? = null
@JvmField var insecure: Boolean = false
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmEmail : RealmObject() {
@PrimaryKey
@JvmField
var address: String? = null
@JvmField
var verified: Boolean = false
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmPreferences : RealmObject() {
@PrimaryKey
@JvmField
var id: String? = null
@JvmField
var newRoomNotification: String? = null
@JvmField
var newMessageNotification: String? = null
@JvmField
var useEmojis: Boolean = false
@JvmField
var convertAsciiEmoji: Boolean = false
@JvmField
var saveMobileBandwidth: Boolean = false
@JvmField
var collapseMediaByDefault: Boolean = false
@JvmField
var unreadRoomsMode: Boolean = false
@JvmField
var autoImageLoad: Boolean = false
@JvmField
var emailNotificationMode: String? = null
@JvmField
var unreadAlert: Boolean = false
@JvmField
var desktopNotificationDuration: Int = 0
@JvmField
var viewMode: Int = 0
@JvmField
var hideUsernames: Boolean = false
@JvmField
var hideAvatars: Boolean = false
@JvmField
var hideFlexTab: Boolean = false
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmPublicSetting : RealmObject() {
@PrimaryKey
@JvmField
var _id: String? = null
@JvmField
var group: String? = null
@JvmField
var type: String? = null
@JvmField
var value: String? = null
@JvmField
var _updatedAt: Long = 0
@JvmField
var meta: String? = null
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmSession : RealmObject() {
@JvmField
@PrimaryKey
var sessionId: Int = 0 //only 0 is used!
@JvmField
var token: String? = null
@JvmField
var tokenVerified: Boolean = false
@JvmField
var error: String? = null
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmSettings : RealmObject() {
@PrimaryKey
@JvmField
var id: String? = null
@JvmField
var preferences: RealmPreferences? = null
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmUser : RealmObject() {
companion object {
const val ID = "_id"
const val NAME = "name"
const val USERNAME = "username"
const val STATUS = "status"
const val UTC_OFFSET = "utcOffset"
const val EMAILS = "emails"
const val SETTINGS = "settings"
const val STATUS_ONLINE = "online"
const val STATUS_BUSY = "busy"
const val STATUS_AWAY = "away"
const val STATUS_OFFLINE = "offline"
}
@PrimaryKey
@JvmField
var _id: String? = null
@JvmField
var name: String? = null
@JvmField
var username: String? = null
@JvmField
var status: String? = null
@JvmField
var utcOffset: Double = 0.toDouble()
@JvmField
var emails: RealmList<RealmEmail>? = null
@JvmField
var settings: RealmSettings? = null
}
\ No newline at end of file
package chat.rocket.android.authentication.di
import android.content.Context
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.dagger.scope.PerActivity
......@@ -12,5 +11,5 @@ class AuthenticationModule {
@Provides
@PerActivity
fun provideAuthenticationNavigator(activity: AuthenticationActivity, context: Context) = AuthenticationNavigator(activity, context)
fun provideAuthenticationNavigator(activity: AuthenticationActivity) = AuthenticationNavigator(activity)
}
\ No newline at end of file
package chat.rocket.android.authentication.domain.model
import chat.rocket.common.model.Token
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class TokenModel(val userId: String, val authToken: String)
\ No newline at end of file
data class TokenModel(val userId: String, val authToken: String)
fun TokenModel.toToken() = Token(userId, authToken)
\ No newline at end of file
package chat.rocket.android.authentication.infraestructure
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
class MemoryTokenRepository : TokenRepository {
var savedToken: Token? = null
override fun get(): Token? {
return savedToken
}
override fun save(token: Token) {
savedToken = token
}
}
\ No newline at end of file
package chat.rocket.android.authentication.infraestructure
import android.content.SharedPreferences
import androidx.content.edit
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.common.model.Token
import com.squareup.moshi.Moshi
import timber.log.Timber
class SharedPreferencesTokenRepository(private val prefs: SharedPreferences, moshi: Moshi) : TokenRepository {
private var servers = prefs.getStringSet(KEY_SERVERS, emptySet()).toMutableSet()
private var currentUrl: String? = null
private var currentToken: Token? = null
private val adapter = moshi.adapter<TokenModel>(TokenModel::class.java)
override fun get(url: String): Token? {
if (currentToken != null && url == currentUrl) {
return currentToken
}
try {
prefs.getString(tokenKey(url), null)?.let { tokenStr ->
val model = adapter.fromJson(tokenStr)
model?.let {
val token = Token(model.userId, model.authToken)
currentToken = token
currentUrl = url
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error parsing token for ${tokenKey(url)}")
ex.printStackTrace()
}
return currentToken
}
override fun save(url: String, token: Token) {
try {
val model = TokenModel(token.userId, token.authToken)
val str = adapter.toJson(model)
servers.add(url)
prefs.edit {
putString(tokenKey(url), str)
putStringSet(KEY_SERVERS, servers)
}
currentToken = token
currentUrl = url
} catch (ex: Exception) {
Timber.d(ex, "Error saving token for ${tokenKey(url)}")
ex.printStackTrace()
}
}
override fun remove(url: String) {
servers.remove(url)
prefs.edit {
remove(url)
putStringSet(KEY_SERVERS, servers)
}
}
override fun clear() {
servers.forEach { server ->
prefs.edit { remove(server) }
}
servers.clear()
prefs.edit {
remove(KEY_SERVERS)
}
}
private fun tokenKey(url: String) = "$KEY_TOKEN$url"
}
private const val KEY_TOKEN = "KEY_TOKEN_"
private const val KEY_SERVERS = "KEY_SERVERS"
\ No newline at end of file
......@@ -37,7 +37,7 @@ interface LoginView : LoadingView, MessageView, InternetView {
/**
* Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server settings.
*
* REMARK: We must set up the CAS button listener [setupCasButtonListener].
* REMARK: We must set up the CAS button listener before showing it [setupCasButtonListener].
*/
fun showCasButton()
......@@ -49,8 +49,8 @@ interface LoginView : LoadingView, MessageView, InternetView {
/**
* Setups the CAS button when tapped.
*
* @param casUrl The CAS URL to login/sign up with.
* @param casToken The requested Token sent to the CAS server.
* @param casUrl The CAS URL to authenticate with.
* @param casToken The requested token to be sent to the CAS server.
*/
fun setupCasButtonListener(casUrl: String, casToken: String)
......@@ -96,40 +96,80 @@ interface LoginView : LoadingView, MessageView, InternetView {
fun hideLoginButton()
/**
* Shows the "login by Facebook view if it is enabled by the server settings.
* Shows the "login by Facebook view if it is enable by the server settings.
*/
fun enableLoginByFacebook()
/**
* Shows the "login by Github" view if it is enabled by the server settings.
* Shows the "login by Github" view if it is enable by the server settings.
*
* REMARK: We must set up the Github button listener before enabling it [setupGithubButtonListener].
*/
fun enableLoginByGithub()
/**
* Shows the "login by Google" view if it is enabled by the server settings.
* Setups the Github button when tapped.
*
* @param githubUrl The Github OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGithubButtonListener(githubUrl: String, state: String)
/**
* Shows the "login by Google" view if it is enable by the server settings.
*
* REMARK: We must set up the Google button listener before enabling it [setupGoogleButtonListener].
*/
fun enableLoginByGoogle()
/**
* Shows the "login by Linkedin" view if it is enabled by the server settings.
* Setups the Google button when tapped.
*
* @param googleUrl The Google OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGoogleButtonListener(googleUrl: String, state: String)
/**
* Shows the "login by Linkedin" view if it is enable by the server settings.
*
* REMARK: We must set up the Linkedin button listener before enabling it [setupLinkedinButtonListener].
*/
fun enableLoginByLinkedin()
/**
* Shows the "login by Meteor" view if it is enabled by the server settings.
* Setups the Linkedin button when tapped.
*
* @param linkedinUrl The Linkedin OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupLinkedinButtonListener(linkedinUrl: String, state: String)
/**
* Shows the "login by Meteor" view if it is enable by the server settings.
*/
fun enableLoginByMeteor()
/**
* Shows the "login by Twitter" view if it is enabled by the server settings.
* Shows the "login by Twitter" view if it is enable by the server settings.
*/
fun enableLoginByTwitter()
/**
* Shows the "login by Gitlab" view if it is enabled by the server settings.
* Shows the "login by Gitlab" view if it is enable by the server settings.
*
* REMARK: We must set up the Gitlab button listener before enabling it [setupGitlabButtonListener].
*/
fun enableLoginByGitlab()
/**
* Setups the Gitlab button when tapped.
*
* @param gitlabUrl The Gitlab OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGitlabButtonListener(gitlabUrl: String, state: String)
/**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*/
......
......@@ -19,12 +19,17 @@ import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.*
import chat.rocket.android.webview.cas.ui.webViewIntent
import chat.rocket.android.webview.cas.ui.INTENT_CAS_TOKEN
import chat.rocket.android.webview.cas.ui.casWebViewIntent
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_SECRET
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN
import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject
internal const val REQUEST_CODE_FOR_CAS = 1
internal const val REQUEST_CODE_FOR_OAUTH = 2
class LoginFragment : Fragment(), LoginView {
@Inject lateinit var presenter: LoginPresenter
......@@ -64,10 +69,14 @@ class LoginFragment : Fragment(), LoginView {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
if (resultCode == Activity.RESULT_OK) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
data?.apply {
presenter.authenticateWithCas(getStringExtra("cas_token"))
presenter.authenticateWithCas(getStringExtra(INTENT_CAS_TOKEN))
}
} else if (requestCode == REQUEST_CODE_FOR_OAUTH) {
data?.apply {
presenter.authenticateWithOauth(getStringExtra(INTENT_OAUTH_CREDENTIAL_TOKEN), getStringExtra(INTENT_OAUTH_CREDENTIAL_SECRET))
}
}
}
......@@ -121,7 +130,7 @@ class LoginFragment : Fragment(), LoginView {
override fun setupLoginButtonListener() {
button_log_in.setOnClickListener {
presenter.authenticate(text_username_or_email.textContent, text_password.textContent)
presenter.authenticateWithUserAndPassword(text_username_or_email.textContent, text_password.textContent)
}
}
......@@ -147,7 +156,7 @@ class LoginFragment : Fragment(), LoginView {
override fun setupCasButtonListener(casUrl: String, casToken: String) {
button_cas.setOnClickListener {
startActivityForResult(context?.webViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS)
startActivityForResult(context?.casWebViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
}
......@@ -192,31 +201,60 @@ class LoginFragment : Fragment(), LoginView {
}
override fun enableLoginByFacebook() {
button_facebook.isEnabled = true
button_facebook.isClickable = true
}
override fun enableLoginByGithub() {
button_github.isEnabled = true
button_github.isClickable = true
}
override fun setupGithubButtonListener(githubUrl: String, state: String) {
button_github.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(githubUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
}
override fun enableLoginByGoogle() {
button_google.isEnabled = true
button_google.isClickable = true
}
// TODO: Use custom tabs instead of web view. See https://github.com/RocketChat/Rocket.Chat.Android/issues/968
override fun setupGoogleButtonListener(googleUrl: String, state: String) {
button_google.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(googleUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
}
override fun enableLoginByLinkedin() {
button_linkedin.isEnabled = true
button_linkedin.isClickable = true
}
override fun setupLinkedinButtonListener(linkedinUrl: String, state: String) {
button_linkedin.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(linkedinUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
}
override fun enableLoginByMeteor() {
button_meteor.isEnabled = true
button_meteor.isClickable = true
}
override fun enableLoginByTwitter() {
button_twitter.isEnabled = true
button_twitter.isClickable = true
}
override fun enableLoginByGitlab() {
button_gitlab.isEnabled = true
button_gitlab.isClickable = true
}
override fun setupGitlabButtonListener(gitlabUrl: String, state: String) {
button_gitlab.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(gitlabUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
}
override fun setupFabListener() {
......@@ -253,8 +291,8 @@ class LoginFragment : Fragment(), LoginView {
social_accounts_container.postDelayed({
(0..social_accounts_container.childCount)
.mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
.filter { it.isEnabled }
.forEach { it.visibility = View.VISIBLE }
.filter { it.isClickable }
.forEach { it.setVisible(true)}
}, 1000)
}
......@@ -284,13 +322,10 @@ class LoginFragment : Fragment(), LoginView {
}
private fun showThreeSocialAccountsMethods() {
var count = 0
for (i in 0..social_accounts_container.childCount) {
val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue
if (view.isEnabled && count < 3) {
view.visibility = View.VISIBLE
count++
}
}
(0..social_accounts_container.childCount)
.mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
.filter { it.isClickable }
.take(3)
.forEach { it.setVisible(true) }
}
}
\ No newline at end of file
package chat.rocket.android.authentication.presentation
import android.content.Context
import android.content.Intent
import chat.rocket.android.R
import chat.rocket.android.authentication.login.ui.LoginFragment
import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack
import chat.rocket.android.webview.ui.webViewIntent
class AuthenticationNavigator(internal val activity: AuthenticationActivity, internal val context: Context) {
class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
fun toLogin() {
activity.addFragmentBackStack("LoginFragment", R.id.fragment_container) {
......@@ -32,7 +33,7 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int
}
fun toWebPage(url: String) {
activity.startActivity(context.webViewIntent(url))
activity.startActivity(activity.webViewIntent(url))
activity.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
......@@ -41,7 +42,13 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int
activity.finish()
}
fun toChatList(serverUrl: String) {
activity.startActivity(activity.changeServerIntent(serverUrl))
activity.finish()
}
fun toServerScreen() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
activity.startActivity(activity.newServerIntent())
activity.finish()
}
}
\ No newline at end of file
package chat.rocket.android.authentication.presentation
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
import chat.rocket.android.server.domain.TokenRepository
import javax.inject.Inject
class AuthenticationPresenter @Inject constructor(private val navigator: AuthenticationNavigator,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val multiServerRepository: MultiServerTokenRepository,
private val settingsRepository: SettingsRepository,
private val tokenRepository: TokenRepository) {
fun loadCredentials(callback: (authenticated: Boolean) -> Unit) {
class AuthenticationPresenter @Inject constructor(
private val navigator: AuthenticationNavigator,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val getAccountInteractor: GetAccountInteractor,
private val settingsRepository: SettingsRepository,
private val localRepository: LocalRepository,
private val tokenRepository: TokenRepository
) {
suspend fun loadCredentials(newServer: Boolean, callback: (authenticated: Boolean) -> Unit) {
val currentServer = getCurrentServerInteractor.get()
val serverToken = currentServer?.let { multiServerRepository.get(currentServer) }
val serverToken = currentServer?.let { tokenRepository.get(currentServer) }
val settings = currentServer?.let { settingsRepository.get(currentServer) }
val account = currentServer?.let { getAccountInteractor.get(currentServer) }
account?.let {
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, account.userName)
}
if (currentServer == null || serverToken == null || settings == null) {
if (newServer || currentServer == null || serverToken == null || settings == null) {
callback(false)
} else {
tokenRepository.save(Token(serverToken.userId, serverToken.authToken))
callback(true)
navigator.toChatList()
}
......
......@@ -4,6 +4,7 @@ import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.util.extensions.launchUI
......@@ -14,12 +15,20 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor) {
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor) {
fun connect(server: String) {
if (!UrlHelper.isValidUrl(server)) {
view.showInvalidServerUrlMessage()
} else {
launchUI(strategy) {
// Check if we already have an account for this server...
val account = getAccountsInteractor.get().firstOrNull { it.serverUrl == server }
if (account != null) {
navigator.toChatList(server)
return@launchUI
}
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
try {
......
......@@ -56,13 +56,21 @@ class ServerFragment : Fragment(), ServerView {
enableUserInput(true)
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int){
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection))
override fun showNoInternetConnection() {
showMessage(getString(R.string.msg_no_internet_connection))
}
private fun enableUserInput(value: Boolean) {
button_connect.isEnabled = value
......
......@@ -5,9 +5,12 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
......@@ -15,6 +18,7 @@ import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.internal.rest.signup
import chat.rocket.core.model.Myself
import javax.inject.Inject
class SignupPresenter @Inject constructor(private val view: SignupView,
......@@ -22,8 +26,13 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
private val navigator: AuthenticationNavigator,
private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
fun signup(name: String, username: String, password: String, email: String) {
val server = serverInteractor.get()
......@@ -55,7 +64,8 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
// TODO This function returns a user token so should we save it?
client.login(username, password)
val me = client.me()
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
......@@ -90,8 +100,21 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it)
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// TODO: Schedule push token registering when it comes up null
// TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
}
private suspend fun saveAccount(me: Myself) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val thumb = UrlHelper.getAvatarUrl(currentServer, me.username!!)
val account = Account(currentServer, icon, logo, me.username!!, thumb)
saveAccountInteractor.save(account)
}
}
\ No newline at end of file
......@@ -97,9 +97,13 @@ class SignupFragment : Fragment(), SignupView {
enableUserInput(true)
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
......
package chat.rocket.android.authentication.twofactor.presentation
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.internal.rest.me
import chat.rocket.core.model.Myself
import javax.inject.Inject
class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val multiServerRepository: MultiServerTokenRepository,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
// TODO: If the usernameOrEmail and password was informed by the user on the previous screen, then we should pass only the pin, like this: fun authenticate(pin: EditText)
fun authenticate(usernameOrEmail: String, password: String, twoFactorAuthenticationCode: String) {
......@@ -45,10 +52,9 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
// The token is saved via the client TokenProvider
val token =
client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
multiServerRepository.save(
server,
TokenModel(token.userId, token.authToken)
)
val me = client.me()
saveAccount(me)
tokenRepository.save(server, token)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
......@@ -76,8 +82,21 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it)
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// TODO: Schedule push token registering when it comes up null
// TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
}
private suspend fun saveAccount(me: Myself) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val thumb = UrlHelper.getAvatarUrl(currentServer, me.username!!)
val account = Account(currentServer, icon, logo, me.username!!, thumb)
saveAccountInteractor.save(account)
}
}
\ No newline at end of file
......@@ -66,7 +66,9 @@ class TwoFAFragment : Fragment(), TwoFAView {
text_two_factor_auth.shake()
}
override fun alertInvalidTwoFactorAuthenticationCode() = showMessage(getString(R.string.msg_invalid_2fa_code))
override fun alertInvalidTwoFactorAuthenticationCode() {
showMessage(getString(R.string.msg_invalid_2fa_code))
}
override fun showLoading() {
enableUserInput(false)
......@@ -78,9 +80,13 @@ class TwoFAFragment : Fragment(), TwoFAView {
enableUserInput(true)
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
package chat.rocket.android.authentication.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
......@@ -7,40 +9,58 @@ import chat.rocket.android.R
import chat.rocket.android.authentication.presentation.AuthenticationPresenter
import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.launchUI
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import javax.inject.Inject
class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject lateinit var presenter: AuthenticationPresenter
val job = Job()
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
presenter.loadCredentials { authenticated ->
if (authenticated) {
// just call onCreate, and the presenter will call the navigator...
super.onCreate(savedInstanceState)
} else {
showServerInput(savedInstanceState)
setContentView(R.layout.activity_authentication)
setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState)
launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
presenter.loadCredentials(newServer) { authenticated ->
if (!authenticated) {
showServerInput(savedInstanceState)
}
}
}
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
}
fun showServerInput(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_authentication)
setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState)
addFragment("ServerFragment", R.id.fragment_container) {
ServerFragment.newInstance()
}
}
}
const val INTENT_ADD_NEW_SERVER = "INTENT_ADD_NEW_SERVER"
fun Context.newServerIntent(): Intent {
return Intent(this, AuthenticationActivity::class.java).apply {
putExtra(INTENT_ADD_NEW_SERVER, true)
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
\ No newline at end of file
......@@ -2,9 +2,9 @@ package chat.rocket.android.chatroom.adapter
import android.support.annotation.IntDef
const val PEOPLE = 0L
const val ROOMS = 1L
const val PEOPLE = 0
const val ROOMS = 1
@Retention(AnnotationRetention.SOURCE)
@IntDef(value = [PEOPLE, ROOMS])
@IntDef(PEOPLE, ROOMS)
annotation class AutoCompleteType
......@@ -97,7 +97,7 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
val countTextView = findViewById<TextView>(R.id.text_count)
emojiTextView.text = reaction.unicode
countTextView.text = reaction.count.toString()
val myself = localRepository.get(LocalRepository.USERNAME_KEY)
val myself = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
if (reaction.usernames.contains(myself)) {
val context = itemView.context
val resources = context.resources
......
......@@ -53,7 +53,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer)
private val client = manager.client
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private val messagesChannel = Channel<Message>()
private var chatRoomId: String? = null
......@@ -364,7 +364,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
try {
val members = client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 50).result
usersRepository.saveAll(members)
val self = localRepository.get(LocalRepository.USERNAME_KEY)
val self = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
// Take at most the 100 most recent messages distinguished by user. Can return less.
val recentMessages = messagesRepository.getRecentMessages(chatRoomId, 100)
.filterNot { filterSelfOut && it.sender?.username == self }
......@@ -402,7 +402,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
}
fun spotlight(query: String, @AutoCompleteType type: Long, filterSelfOut: Boolean = false) {
fun spotlight(query: String, @AutoCompleteType type: Int, filterSelfOut: Boolean = false) {
launchUI(strategy) {
try {
val (users, rooms) = client.spotlight(query)
......@@ -411,7 +411,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
if (users.isNotEmpty()) {
usersRepository.saveAll(users)
}
val self = localRepository.get(LocalRepository.USERNAME_KEY)
val self = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
view.populatePeopleSuggestions(users.map {
val username = it.username ?: ""
val name = it.name ?: ""
......
......@@ -24,7 +24,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
getSettingsInteractor: GetSettingsInteractor) {
private val client = factory.create(serverInteractor.get()!!)
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private var pinnedMessagesListOffset: Int = 0
/**
......
......@@ -234,7 +234,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
presenter.uploadFile(chatRoomId, uri, "")
}
override fun showInvalidFileMessage() = showMessage(getString(R.string.msg_invalid_file))
override fun showInvalidFileMessage() {
showMessage(getString(R.string.msg_invalid_file))
}
override fun showNewMessage(message: List<BaseViewModel<*>>) {
adapter.prependData(message)
......@@ -288,9 +290,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
......@@ -66,9 +66,13 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
......@@ -15,7 +15,6 @@ import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
import chat.rocket.core.model.Value
......@@ -23,6 +22,7 @@ import chat.rocket.core.model.attachment.*
import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import okhttp3.HttpUrl
import timber.log.Timber
......@@ -32,15 +32,17 @@ import javax.inject.Inject
class ViewModelMapper @Inject constructor(private val context: Context,
private val parser: MessageParser,
private val messagesRepository: MessagesRepository,
private val getAccountInteractor: GetAccountInteractor,
tokenRepository: TokenRepository,
localRepository: LocalRepository,
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor) {
getSettingsInteractor: GetSettingsInteractor,
localRepository: LocalRepository) {
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private val currentServer = serverInteractor.get()!!
private val settings: Map<String, Value<Any>> = getSettingsInteractor.get(currentServer)
private val baseUrl = settings.baseUrl()
private val currentUsername: String? = localRepository.get(LocalRepository.USERNAME_KEY)
private val token = tokenRepository.get()
private val token = tokenRepository.get(currentServer)
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
suspend fun map(message: Message): List<BaseViewModel<*>> {
return translate(message)
......@@ -249,6 +251,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val quoteMessage: Message = quote
quoteViewModel = mapMessage(quoteMessage)
}
return parser.renderMarkdown(message.message, quoteViewModel, currentUsername)
}
......
......@@ -36,7 +36,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val currentServer = serverInteractor.get()!!
private val client = manager.client
private var reloadJob: Deferred<List<ChatRoom>>? = null
private val settings = settingsRepository.get(currentServer)!!
private val settings = settingsRepository.get(currentServer)
private val subscriptionsChannel = Channel<StreamMessage<BaseRoom>>()
private val stateChannel = Channel<State>()
......@@ -101,21 +101,52 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private suspend fun usersToChatRooms(users: List<User>): List<ChatRoom> {
return users.map {
ChatRoom(it.id, RoomType.DIRECT_MESSAGE, SimpleUser(
username = it.username, name = it.name, id = null), it.name ?: "",
it.name, false, null, null, null,
null, null, false, false, false,
0L, null, 0L, null, client
ChatRoom(id = it.id,
type = RoomType.DIRECT_MESSAGE,
user = SimpleUser(username = it.username, name = it.name, id = null),
name = it.name ?: "",
fullName = it.name,
readonly = false,
updatedAt = null,
timestamp = null,
lastSeen = null,
topic = null,
description = null,
announcement = null,
default = false,
open = false,
alert = false,
unread = 0L,
userMenstions = null,
groupMentions = 0L,
lastMessage = null,
client = client
)
}
}
private suspend fun roomsToChatRooms(rooms: List<Room>): List<ChatRoom> {
return rooms.map {
ChatRoom(it.id, it.type, it.user, it.name ?: "",
it.fullName, it.readonly, it.updatedAt, null, null,
it.topic, it.announcement, false, false, false,
0L, null, 0L, it.lastMessage, client
ChatRoom(id = it.id,
type = it.type,
user = it.user,
name = it.name ?: "",
fullName = it.fullName,
readonly = it.readonly,
updatedAt = it.updatedAt,
timestamp = null,
lastSeen = null,
topic = it.topic,
description = it.description,
announcement = it.announcement,
default = false,
open = false,
alert = false,
unread = 0L,
userMenstions = null,
groupMentions = 0L,
lastMessage = it.lastMessage,
client = client
)
}
}
......@@ -135,7 +166,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private fun updateRooms() {
Timber.d("Updating Rooms")
launch {
launch(strategy.jobs) {
view.updateChatRooms(getChatRoomsInteractor.get(currentServer))
}
}
......@@ -255,6 +286,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
timestamp,
lastSeen,
room.topic,
room.description,
room.announcement,
default,
open,
......@@ -286,6 +318,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
subscription.timestamp ?: timestamp,
subscription.lastSeen ?: lastSeen,
topic,
description,
announcement,
subscription.isDefault,
subscription.open,
......
......@@ -110,9 +110,13 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......@@ -153,7 +157,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
recycler_view.itemAnimator = DefaultItemAnimator()
// TODO - use a ViewModel Mapper instead of using settings on the adapter
recycler_view.adapter = ChatRoomsAdapter(this,
settingsRepository.get(serverInteractor.get()!!)!!) { chatRoom ->
settingsRepository.get(serverInteractor.get()!!)) { chatRoom ->
presenter.loadChatRoom(chatRoom)
}
}
......
......@@ -4,6 +4,7 @@ import android.app.Application
import chat.rocket.android.app.RocketChatApplication
import chat.rocket.android.dagger.module.ActivityBuilder
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.dagger.module.ReceiverBuilder
import chat.rocket.android.dagger.module.ServiceBuilder
import chat.rocket.android.push.FirebaseTokenService
import dagger.BindsInstance
......@@ -12,7 +13,8 @@ import dagger.android.support.AndroidSupportInjectionModule
import javax.inject.Singleton
@Singleton
@Component(modules = [AndroidSupportInjectionModule::class, AppModule::class, ActivityBuilder::class, ServiceBuilder::class])
@Component(modules = [AndroidSupportInjectionModule::class,
AppModule::class, ActivityBuilder::class, ServiceBuilder::class, ReceiverBuilder::class])
interface AppComponent {
@Component.Builder
......
......@@ -16,6 +16,8 @@ import chat.rocket.android.main.di.MainModule
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.members.di.MembersFragmentProvider
import chat.rocket.android.profile.di.ProfileFragmentProvider
import chat.rocket.android.server.di.ChangeServerModule
import chat.rocket.android.server.ui.ChangeServerActivity
import chat.rocket.android.settings.password.di.PasswordFragmentProvider
import chat.rocket.android.settings.password.ui.PasswordActivity
import dagger.Module
......@@ -51,4 +53,8 @@ abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(modules = [PasswordFragmentProvider::class])
abstract fun bindPasswordActivity(): PasswordActivity
@PerActivity
@ContributesAndroidInjector(modules = [ChangeServerModule::class])
abstract fun bindChangeServerActivity(): ChangeServerActivity
}
\ No newline at end of file
package chat.rocket.android.dagger.module
import android.app.Application
import android.app.NotificationManager
import android.arch.persistence.room.Room
import android.content.Context
import android.content.SharedPreferences
import androidx.content.systemService
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.MemoryTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.helper.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.*
import chat.rocket.android.push.GroupedPush
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.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetPermissionsInteractor
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.MemoryChatRoomsRepository
import chat.rocket.android.server.infraestructure.MemoryMessagesRepository
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.SharedPreferencesSettingsRepository
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient
import chat.rocket.core.TokenRepository
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig
......@@ -110,8 +130,8 @@ class AppModule {
@Provides
@ForFresco
@Singleton
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository): Interceptor {
return FrescoAuthInterceptor(tokenRepository)
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
return FrescoAuthInterceptor(tokenRepository, currentServerInteractor)
}
@Provides
......@@ -144,8 +164,8 @@ class AppModule {
@Provides
@Singleton
fun provideTokenRepository(): TokenRepository {
return MemoryTokenRepository()
fun provideTokenRepository(prefs: SharedPreferences, moshi: Moshi): TokenRepository {
return SharedPreferencesTokenRepository(prefs, moshi)
}
@Provides
......@@ -192,7 +212,10 @@ class AppModule {
@Provides
@Singleton
fun provideMoshi(): Moshi {
return Moshi.Builder().add(AppJsonAdapterFactory.INSTANCE).build()
return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE)
.build()
}
@Provides
......@@ -240,4 +263,16 @@ class AppModule {
fun providePermissionInteractor(settingsRepository: SettingsRepository, serverRepository: CurrentServerRepository): GetPermissionsInteractor {
return GetPermissionsInteractor(settingsRepository, serverRepository)
}
@Provides
@Singleton
fun provideAccountsRepository(preferences: SharedPreferences, moshi: Moshi): AccountsRepository =
SharedPreferencesAccountsRepository(preferences, moshi)
@Provides
fun provideNotificationManager(context: Context): NotificationManager = context.systemService()
@Provides
@Singleton
fun provideGroupedPush() = GroupedPush()
}
\ No newline at end of file
package chat.rocket.android.dagger.module
import chat.rocket.android.push.DeleteReceiver
import chat.rocket.android.push.di.DeleteReceiverProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class ReceiverBuilder {
@ContributesAndroidInjector(modules = [DeleteReceiverProvider::class])
abstract fun bindDeleteReceiver(): DeleteReceiver
}
\ No newline at end of file
package chat.rocket.android.dagger.module
import chat.rocket.android.push.FirebaseTokenService
import chat.rocket.android.push.GcmListenerService
import chat.rocket.android.push.di.FirebaseTokenServiceProvider
import chat.rocket.android.push.di.GcmListenerServiceProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -9,4 +11,7 @@ import dagger.android.ContributesAndroidInjector
@ContributesAndroidInjector(modules = [FirebaseTokenServiceProvider::class])
abstract fun bindFirebaseTokenService(): FirebaseTokenService
@ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class])
abstract fun bindGcmListenerService(): GcmListenerService
}
\ No newline at end of file
package chat.rocket.android.helper
import chat.rocket.core.TokenRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.TokenRepository
import okhttp3.Interceptor
import okhttp3.Response
class FrescoAuthInterceptor(private val tokenRepository: TokenRepository) : Interceptor {
class FrescoAuthInterceptor(
private val tokenRepository: TokenRepository,
private val currentServerInteractor: GetCurrentServerInteractor
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenRepository.get()
var request = chain.request()
currentServerInteractor.get()?.let { serverUrl ->
val token = tokenRepository.get(serverUrl)
token?.let {
val url = request.url().newBuilder().apply {
addQueryParameter("rc_uid", token.userId)
addQueryParameter("rc_token", token.authToken)
}.build()
request = request.newBuilder().apply {
url(url)
}.build()
}
return@let token?.let {
val url = request.url().newBuilder().apply {
addQueryParameter("rc_uid", token.userId)
addQueryParameter("rc_token", token.authToken)
}.build()
request = request.newBuilder().apply {
url(url)
}.build()
}
}
return chain.proceed(request)
}
}
\ No newline at end of file
......@@ -14,6 +14,16 @@ object UrlHelper {
fun getAvatarUrl(serverUrl: String, avatarName: String, format: String = "jpeg"): String =
removeTrailingSlash(serverUrl) + "/avatar/" + removeTrailingSlash(avatarName) + "?format=$format"
/**
* Returns the server logo URL.
*
* @param serverUrl The server URL.
* @param favicon The faviconLarge from the server settings.
* @return The server logo URL.
*/
fun getServerLogoUrl(serverUrl: String, favicon: String): String =
removeTrailingSlash(serverUrl) + "/$favicon"
/**
* Returns the CAS URL.
*
......@@ -25,6 +35,49 @@ object UrlHelper {
fun getCasUrl(casLoginUrl: String, serverUrl: String, token: String): String =
removeTrailingSlash(casLoginUrl) + "?service=" + removeTrailingSlash(serverUrl) + "/_cas/" + token
/**
* Returns the Github Oauth URL.
*
* @param clientId The GitHub client ID.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Github Oauth URL.
*/
fun getGithubOauthUrl(clientId: String, state: String): String =
"https://github.com/login/oauth/authorize?scope=user:email&client_id=$clientId&state=$state"
/**
* Returns the Google Oauth URL.
*
* @param clientId The Google client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Google Oauth URL.
*/
fun getGoogleOauthUrl(clientId: String, serverUrl: String, state: String) =
"https://accounts.google.com/o/oauth2/v2/auth?client_id=$clientId&redirect_uri=${removeTrailingSlash(serverUrl)}/_oauth/google?close&response_type=code&state=$state&scope=email%20profile"
/**
* Returns the Linkedin Oauth URL.
*
* @param clientId The Linkedin client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Linkedin Oauth URL.
*/
fun getLinkedinOauthUrl(clientId: String, serverUrl: String, state: String) =
"https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=$clientId&redirect_uri=${removeTrailingSlash(serverUrl)}/_oauth/linkedin?close&state=$state"
/**
* Returns the Gitlab Oauth URL.
*
* @param clientId The Gitlab client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Gitlab Oauth URL.
*/
fun getGitlabOauthUrl(clientId: String, serverUrl: String, state: String): String =
"https://gitlab.com/oauth/authorize?client_id=$clientId&redirect_uri=${removeTrailingSlash(serverUrl)}/_oauth/gitlab?close&response_type=code&state=$state&scope=read_user"
/**
* Returns the server's Terms of Service URL.
*
......
......@@ -2,18 +2,24 @@ package chat.rocket.android.infrastructure
interface LocalRepository {
fun save(key: String, value: String?)
fun save(key: String, value: Boolean)
fun save(key: String, value: Int)
fun save(key: String, value: Long)
fun save(key: String, value: Float)
fun get(key: String): String?
fun getBoolean(key: String): Boolean
fun getFloat(key: String): Float
fun getInt(key: String): Int
fun getLong(key: String): Long
fun clear(key: String)
fun clearAllFromServer(server: String)
companion object {
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
const val MIGRATION_FINISHED_KEY = "MIGRATION_FINISHED_KEY"
const val TOKEN_KEY = "token_"
const val SETTINGS_KEY = "settings_"
const val USERNAME_KEY = "my_username"
const val CURRENT_USERNAME_KEY = "username_"
}
fun save(key: String, value: String?)
fun get(key: String): String?
fun clear(key: String)
fun clearAllFromServer(server: String)
}
\ No newline at end of file
......@@ -3,23 +3,32 @@ package chat.rocket.android.infrastructure
import android.content.SharedPreferences
class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : LocalRepository {
override fun getBoolean(key: String) = preferences.getBoolean(key, false)
override fun save(key: String, value: String?) {
preferences.edit().putString(key, value).apply()
}
override fun getFloat(key: String) = preferences.getFloat(key, -1f)
override fun get(key: String): String? {
return preferences.getString(key, null)
}
override fun getInt(key: String) = preferences.getInt(key, -1)
override fun clear(key: String) {
preferences.edit().remove(key).apply()
}
override fun getLong(key: String) = preferences.getLong(key, -1L)
override fun save(key: String, value: Int) = preferences.edit().putInt(key, value).apply()
override fun save(key: String, value: Float) = preferences.edit().putFloat(key, value).apply()
override fun save(key: String, value: Long) = preferences.edit().putLong(key, value).apply()
override fun save(key: String, value: Boolean) = preferences.edit().putBoolean(key, value).apply()
override fun save(key: String, value: String?) = preferences.edit().putString(key, value).apply()
override fun get(key: String): String? = preferences.getString(key, null)
override fun clear(key: String) = preferences.edit().remove(key).apply()
override fun clearAllFromServer(server: String) {
clear(LocalRepository.KEY_PUSH_TOKEN)
clear(LocalRepository.TOKEN_KEY + server)
clear(LocalRepository.SETTINGS_KEY + server)
clear(LocalRepository.USERNAME_KEY + server)
clear(LocalRepository.CURRENT_USERNAME_KEY)
}
}
\ No newline at end of file
package chat.rocket.android.main.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
import chat.rocket.android.server.domain.model.Account
import kotlinx.android.synthetic.main.item_account.view.*
class AccountViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(account: Account) {
with(itemView) {
server_logo.setImageURI(account.serverLogo)
text_server_url.text = account.serverUrl
text_username.text = account.userName
}
}
}
\ No newline at end of file
package chat.rocket.android.main.adapter
import android.support.v7.widget.RecyclerView
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.util.extensions.inflate
class AccountsAdapter(
private val accounts: List<Account>,
private val selector: AccountSelector
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_ACCOUNT -> AccountViewHolder(parent.inflate(R.layout.item_account))
else -> AddAccountViewHolder(parent.inflate(R.layout.item_add_account))
}
}
override fun getItemCount() = accounts.size + 1
override fun getItemViewType(position: Int) =
if (position == accounts.size) VIEW_TYPE_ADD_ACCOUNT else VIEW_TYPE_ACCOUNT
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is AccountViewHolder -> bindAccountViewHolder(holder, position)
is AddAccountViewHolder -> bindAddAccountViewHolder(holder, position)
}
}
private fun bindAccountViewHolder(holder: AccountViewHolder, position: Int) {
val account = accounts[position]
holder.bind(account)
holder.itemView.setOnClickListener {
selector.onAccountSelected(account.serverUrl)
}
}
private fun bindAddAccountViewHolder(holder: AddAccountViewHolder, position: Int) {
holder.itemView.setOnClickListener {
selector.onAddedAccountSelected()
}
}
}
interface AccountSelector {
fun onAccountSelected(serverUrl: String)
fun onAddedAccountSelected()
}
private const val VIEW_TYPE_ACCOUNT = 0
private const val VIEW_TYPE_ADD_ACCOUNT = 1
\ No newline at end of file
package chat.rocket.android.main.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
class AddAccountViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
\ No newline at end of file
......@@ -16,7 +16,7 @@ class MainModule {
@Provides
@PerActivity
fun provideMainNavigator(activity: MainActivity, context: Context) = MainNavigator(activity, context)
fun provideMainNavigator(activity: MainActivity) = MainNavigator(activity)
@Provides
fun provideMainView(activity: MainActivity): MainView = activity
......
......@@ -2,14 +2,16 @@ package chat.rocket.android.main.presentation
import android.content.Context
import chat.rocket.android.R
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.ui.ProfileFragment
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.settings.ui.SettingsFragment
import chat.rocket.android.util.extensions.addFragment
class MainNavigator(internal val activity: MainActivity, internal val context: Context) {
class MainNavigator(internal val activity: MainActivity) {
fun toChatList() {
activity.addFragment("ChatRoomsFragment", R.id.fragment_container) {
......@@ -35,8 +37,17 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean) {
activity.startActivity(context.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
fun toNewServer(serverUrl: String? = null) {
activity.startActivity(activity.changeServerIntent(serverUrl))
activity.finish()
}
fun toServerScreen() {
activity.startActivity(activity.newServerIntent())
}
}
\ No newline at end of file
package chat.rocket.android.main.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.main.viewmodel.NavHeaderViewModelMapper
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.disconnect
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.internal.rest.unregisterPushToken
import timber.log.Timber
import javax.inject.Inject
class MainPresenter @Inject constructor(private val view: MainView,
private val strategy: CancelStrategy,
private val navigator: MainNavigator,
private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository,
managerFactory: ConnectionManagerFactory,
factory: RocketChatClientFactory) {
class MainPresenter @Inject constructor(
private val view: MainView,
private val strategy: CancelStrategy,
private val navigator: MainNavigator,
private val tokenRepository: TokenRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository,
private val navHeaderMapper: NavHeaderViewModelMapper,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInterector: RemoveAccountInterector,
private val factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory
) {
private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
fun toChatList() = navigator.toChatList()
......@@ -31,6 +47,32 @@ class MainPresenter @Inject constructor(private val view: MainView,
fun toSettings() = navigator.toSettings()
fun loadCurrentInfo() {
launchUI(strategy) {
try {
val me = client.me()
val model = navHeaderMapper.mapToViewModel(me)
saveAccount(model)
view.setupNavHeader(model, getAccountsInteractor.get())
} catch (ex: Exception) {
Timber.d(ex, "Error loading my information for navheader")
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
}
private suspend fun saveAccount(me: NavHeaderViewModel) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val account = Account(currentServer, icon, me.serverLogo, me.username, me.avatar)
saveAccountInteractor.save(account)
}
/**
* Logout from current server.
*/
......@@ -39,9 +81,10 @@ class MainPresenter @Inject constructor(private val view: MainView,
try {
clearTokens()
client.logout()
//TODO: Add the code to unsubscribe to all subscriptions.
client.disconnect()
view.onLogout()
disconnect()
removeAccountInterector.remove(currentServer)
tokenRepository.remove(currentServer)
navigator.toNewServer()
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
......@@ -69,4 +112,22 @@ class MainPresenter @Inject constructor(private val view: MainView,
fun disconnect() {
manager.disconnect()
}
fun changeServer(serverUrl: String) {
if (currentServer != serverUrl) {
navigator.toNewServer(serverUrl)
} else {
view.closeServerSelection()
}
}
fun addNewServer() {
navigator.toServerScreen()
}
suspend fun refreshToken(token: String?) {
token?.let {
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
}
}
\ No newline at end of file
package chat.rocket.android.main.presentation
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.model.Account
interface MainView : MessageView {
/**
* User has successfully logged out from the current server.
**/
fun onLogout()
fun setupNavHeader(model: NavHeaderViewModel, accounts: List<Account>)
fun closeServerSelection()
}
\ No newline at end of file
package chat.rocket.android.main.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.view.Gravity
import android.view.MenuItem
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.main.adapter.AccountSelector
import chat.rocket.android.main.adapter.AccountsAdapter
import chat.rocket.android.main.presentation.MainPresenter
import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.showToast
import com.google.android.gms.gcm.GoogleCloudMessaging
import com.google.android.gms.iid.InstanceID
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......@@ -19,6 +28,10 @@ import dagger.android.HasActivityInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.nav_header.view.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupportFragmentInjector {
......@@ -32,7 +45,14 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
launch(CommonPool) {
val token = InstanceID.getInstance(this@MainActivity).getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
Timber.d("GCM token: $token")
presenter.refreshToken(token)
}
presenter.connect()
presenter.loadCurrentInfo()
setupToolbar()
setupNavigationView()
}
......@@ -52,11 +72,45 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
}
}
override fun onLogout() {
finish()
val intent = Intent(this, AuthenticationActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
override fun setupNavHeader(model: NavHeaderViewModel, accounts: List<Account>) {
Timber.d("Setting up nav header: $model")
val headerLayout = view_navigation.getHeaderView(0)
headerLayout.text_name.text = model.username
headerLayout.text_server.text = model.server
headerLayout.image_avatar.setImageURI(model.avatar)
headerLayout.server_logo.setImageURI(model.serverLogo)
setupAccountsList(headerLayout, accounts)
}
override fun closeServerSelection() {
view_navigation.getHeaderView(0).account_container.performClick()
}
private var expanded = false
private fun setupAccountsList(header: View, accounts: List<Account>) {
accounts_list.layoutManager = LinearLayoutManager(this)
accounts_list.adapter = AccountsAdapter(accounts, object : AccountSelector {
override fun onAccountSelected(serverUrl: String) {
presenter.changeServer(serverUrl)
}
override fun onAddedAccountSelected() {
presenter.addNewServer()
}
})
header.account_container.setOnClickListener {
header.account_expand.rotateBy(180f)
if (expanded) {
accounts_list.fadeOut()
} else {
accounts_list.fadeIn()
}
expanded = !expanded
}
}
override fun showMessage(resId: Int) = showToast(resId)
......
package chat.rocket.android.main.viewmodel
data class NavHeaderViewModel(
val username: String,
val server: String,
val avatar: String?,
val serverLogo: String?
)
\ No newline at end of file
package chat.rocket.android.main.viewmodel
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.*
import chat.rocket.core.model.Myself
import javax.inject.Inject
class NavHeaderViewModelMapper @Inject constructor(serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor) {
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
private val baseUrl = settings.baseUrl()!!
fun mapToViewModel(me: Myself): NavHeaderViewModel {
val username = mapUsername(me)
val thumb = me.username?.let { UrlHelper.getAvatarUrl(baseUrl, it) }
val image = settings.wideTile() ?: settings.faviconLarge()
val logo = image?.let { UrlHelper.getServerLogoUrl(baseUrl, it) }
return NavHeaderViewModel(username, baseUrl, thumb, logo)
}
private fun mapUsername(me: Myself): String {
val username = me.username
val realName = me.name
val senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString()
}
}
\ No newline at end of file
......@@ -90,9 +90,13 @@ class MembersFragment : Fragment(), MembersView {
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
......@@ -66,7 +66,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
listenToChanges()
}
override fun showProfileUpdateSuccessfullyMessage() = showMessage(getString(R.string.msg_profile_update_successfully))
override fun showProfileUpdateSuccessfullyMessage() {
showMessage(getString(R.string.msg_profile_update_successfully))
}
override fun showLoading() {
enableUserInput(false)
......@@ -78,9 +80,13 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
enableUserInput(true)
}
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message)
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
package chat.rocket.android.push
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import dagger.android.AndroidInjection
import javax.inject.Inject
/**
* BroadcastReceiver for dismissed notifications.
*/
class DeleteReceiver : BroadcastReceiver() {
@Inject
lateinit var groupedPushes: GroupedPush
override fun onReceive(context: Context, intent: Intent) {
AndroidInjection.inject(this, context)
val notId = intent.extras?.getInt(EXTRA_NOT_ID)
val host = intent.extras?.getString(EXTRA_HOSTNAME)
if (host != null && notId != null) {
clearNotificationsByHostAndNotificationId(host, notId)
}
}
/**
* Clear notifications by the host they belong to and its unique id.
*/
fun clearNotificationsByHostAndNotificationId(host: String, notificationId: Int) {
if (groupedPushes.hostToPushMessageList.isNotEmpty()) {
val notifications = groupedPushes.hostToPushMessageList[host]
notifications?.let {
notifications.removeAll {
it.notificationId.toInt() == notificationId
}
}
}
}
}
\ No newline at end of file
......@@ -2,6 +2,8 @@ package chat.rocket.android.push
import chat.rocket.android.R
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.registerPushToken
......@@ -16,14 +18,16 @@ import javax.inject.Inject
class FirebaseTokenService : FirebaseInstanceIdService() {
@Inject
lateinit var client: RocketChatClient
lateinit var factory: RocketChatClientFactory
@Inject
lateinit var getCurrentServerInteractor: GetCurrentServerInteractor
@Inject
lateinit var localRepository: LocalRepository
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this);
AndroidInjection.inject(this)
}
override fun onTokenRefresh() {
......@@ -31,11 +35,14 @@ class FirebaseTokenService : FirebaseInstanceIdService() {
// default push gateway. We should register this project's own project sender id into it.
val gcmToken = InstanceID.getInstance(this)
.getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
val currentServer = getCurrentServerInteractor.get()!!
val client = factory.create(currentServer)
gcmToken?.let {
localRepository.save(LocalRepository.KEY_PUSH_TOKEN, gcmToken)
launch {
try {
Timber.d("Registering push token: $gcmToken for ${client.url}")
client.registerPushToken(gcmToken)
} catch (ex: RocketChatException) {
Timber.e(ex)
......
......@@ -2,12 +2,22 @@ package chat.rocket.android.push
import android.os.Bundle
import com.google.android.gms.gcm.GcmListenerService
import dagger.android.AndroidInjection
import javax.inject.Inject
class GcmListenerService : GcmListenerService() {
@Inject
lateinit var pushManager: PushManager
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onMessageReceived(from: String?, data: Bundle?) {
data?.let {
PushManager.handle(this, data)
pushManager.handle(data)
}
}
}
\ No newline at end of file
package chat.rocket.android.push
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Singleton
typealias TupleGroupIdMessageCount = Pair<Int, AtomicInteger>
class GroupedPush {
// Notifications received from the same server are grouped in a single bundled notification.
// This map associates a host to a group id.
val groupMap = HashMap<String, TupleGroupIdMessageCount>()
// Map a hostname to a list of push messages that pertain to it.
val hostToPushMessageList = HashMap<String, MutableList<PushMessage>>()
}
\ No newline at end of file
package chat.rocket.android.push.di
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.push.DeleteReceiver
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class DeleteReceiverProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideDeleteReceiver(): DeleteReceiver
}
\ No newline at end of file
package chat.rocket.android.push.di
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.push.GcmListenerService
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class GcmListenerServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideGcmListenerService(): GcmListenerService
}
\ No newline at end of file
package chat.rocket.android.server.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.server.presentation.ChangeServerNavigator
import chat.rocket.android.server.presentation.ChangeServerView
import chat.rocket.android.server.ui.ChangeServerActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
class ChangeServerModule {
@Provides
@PerActivity
fun provideChangeServerNavigator(activity: ChangeServerActivity) = ChangeServerNavigator(activity)
@Provides
@PerActivity
fun ChangeServerView(activity: ChangeServerActivity): ChangeServerView {
return activity
}
@Provides
fun provideLifecycleOwner(activity: ChangeServerActivity): LifecycleOwner = activity
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy = CancelStrategy(owner, jobs)
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.android.server.domain.model.Account
interface AccountsRepository {
suspend fun save(account: Account)
suspend fun load(): List<Account>
suspend fun remove(serverUrl: String)
}
\ No newline at end of file
package chat.rocket.android.server.domain
import javax.inject.Inject
class GetAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun get(url: String) = repository.load().firstOrNull { account ->
url == account.serverUrl
}
}
\ No newline at end of file
package chat.rocket.android.server.domain
import javax.inject.Inject
class GetAccountsInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun get() = repository.load()
}
\ No newline at end of file
package chat.rocket.android.server.domain;
import java.util.List;
import io.reactivex.Scheduler;
import io.reactivex.Single;
public class GetServersInteractor {
private final ServersRepository repository;
private final Scheduler executionScheduler;
public GetServersInteractor(ServersRepository repository, Scheduler executionScheduler) {
this.repository = repository;
this.executionScheduler = executionScheduler;
}
}
......@@ -17,11 +17,12 @@ class RefreshSettingsInteractor @Inject constructor(private val factory: RocketC
ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR,
ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, ACCOUNT_GITLAB,
SITE_URL, SITE_NAME, FAVICON_512, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS,
SITE_URL, SITE_NAME, FAVICON_512, FAVICON_196, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS,
FAVORITE_ROOMS, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE, UPLOAD_WHITELIST_MIMETYPES,
HIDE_USER_JOIN, HIDE_USER_LEAVE,
HIDE_TYPE_AU, HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ALLOW_MESSAGE_DELETING,
ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS,
WIDE_TILE_310)
suspend fun refresh(server: String) {
withContext(CommonPool) {
......
package chat.rocket.android.server.domain
import javax.inject.Inject
class RemoveAccountInterector @Inject constructor(val repository: AccountsRepository) {
suspend fun remove(serverUrl: String) {
repository.remove(serverUrl)
}
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.android.server.domain.model.Account
import javax.inject.Inject
class SaveAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun save(account: Account) = repository.save(account)
}
\ No newline at end of file
package chat.rocket.android.server.domain;
public class SaveServerInteractor {
}
package chat.rocket.android.server.domain
import chat.rocket.android.server.domain.model.Server
import chat.rocket.android.server.infraestructure.ServerEntity
import io.reactivex.Completable
import io.reactivex.Single
interface ServersRepository {
val servers: Single<List<Server>>
fun saveServer(server: Server): Completable
fun updateServer(server: Server): Completable
}
\ No newline at end of file
......@@ -29,7 +29,9 @@ const val ACCOUNT_GITLAB = "Accounts_OAuth_Gitlab"
const val SITE_URL = "Site_Url"
const val SITE_NAME = "Site_Name"
const val FAVICON_196 = "Assets_favicon_192"
const val FAVICON_512 = "Assets_favicon_512"
const val WIDE_TILE_310 = "Assets_tile_310_wide"
const val USE_REALNAME = "UI_Use_Real_Name"
const val ALLOW_ROOM_NAME_SPECIAL_CHARS = "UI_Allow_room_names_with_special_chars"
const val FAVORITE_ROOMS = "Favorite_Rooms"
......@@ -69,6 +71,9 @@ fun PublicSettings.isGitlabAuthenticationEnabled(): Boolean = this[ACCOUNT_GITLA
fun PublicSettings.isWordpressAuthenticationEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true
fun PublicSettings.faviconLarge(): String? = this[FAVICON_512]?.value as String?
fun PublicSettings.favicon(): String? = this[FAVICON_196]?.value as String?
fun PublicSettings.wideTile(): String? = this[WIDE_TILE_310]?.value as String?
// Message settings
fun PublicSettings.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
......@@ -90,4 +95,5 @@ fun PublicSettings.uploadMaxFileSize(): Int {
return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE
}
fun PublicSettings.baseUrl(): String? = this[SITE_URL]?.value as String
\ No newline at end of file
fun PublicSettings.baseUrl(): String? = this[SITE_URL]?.value as String
fun PublicSettings.siteName(): String? = this[SITE_NAME]?.value as String?
\ No newline at end of file
package chat.rocket.android.server.domain
interface TokenRepository : chat.rocket.core.TokenRepository {
fun remove(url: String)
fun clear()
}
\ No newline at end of file
package chat.rocket.android.server.domain.model
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class Account(
val serverUrl: String,
val serverLogo: String?,
val serverBg: String?,
val userName: String,
val avatar: String?
)
\ No newline at end of file
......@@ -19,4 +19,6 @@ class ConnectionManagerFactory @Inject constructor(private val factory: RocketCh
cache[url] = manager
return manager
}
fun get(url: String) = cache[url]
}
\ No newline at end of file
......@@ -2,16 +2,16 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient
import chat.rocket.core.TokenRepository
import chat.rocket.android.server.domain.TokenRepository
import okhttp3.OkHttpClient
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RocketChatClientFactory @Inject constructor(val okHttpClient: OkHttpClient,
val repository: TokenRepository,
val logger: PlatformLogger) {
class RocketChatClientFactory @Inject constructor(private val okHttpClient: OkHttpClient,
private val repository: TokenRepository,
private val logger: PlatformLogger) {
private val cache = HashMap<String, RocketChatClient>()
fun create(url: String): RocketChatClient {
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.ServersRepository
import chat.rocket.android.server.domain.model.Server
import io.reactivex.Completable
import io.reactivex.Single
class RoomServersRepository : ServersRepository {
override val servers: Single<List<Server>>
get() = TODO("not implemented")
override fun saveServer(server: Server): Completable {
TODO("not implemented")
}
override fun updateServer(server: Server): Completable {
TODO("not implemented")
}
}
package chat.rocket.android.server.infraestructure
import android.content.SharedPreferences
import androidx.content.edit
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.model.Account
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
class SharedPreferencesAccountsRepository(
private val preferences: SharedPreferences,
private val moshi: Moshi
) : AccountsRepository {
override suspend fun save(newAccount: Account) {
withContext(CommonPool) {
val accounts = load()
val newList = accounts.filter { account -> newAccount.serverUrl != account.serverUrl }
.toMutableList()
newList.add(0, newAccount)
save(newList)
}
}
override suspend fun load(): List<Account> = withContext(CommonPool) {
val json = preferences.getString(ACCOUNTS_KEY, "[]")
val type = Types.newParameterizedType(List::class.java, Account::class.java)
val adapter = moshi.adapter<List<Account>>(type)
adapter.fromJson(json) ?: emptyList()
}
override suspend fun remove(serverUrl: String) {
withContext(CommonPool) {
val accounts = load()
val newList = accounts.filter { account -> serverUrl != account.serverUrl }
.toMutableList()
save(newList)
}
}
private fun save(accounts: List<Account>) {
val type = Types.newParameterizedType(List::class.java, Account::class.java)
val adapter = moshi.adapter<List<Account>>(type)
preferences.edit {
putString(ACCOUNTS_KEY, adapter.toJson(accounts))
}
}
}
private const val ACCOUNTS_KEY = "ACCOUNTS_KEY"
\ No newline at end of file
......@@ -15,9 +15,7 @@ class SharedPreferencesSettingsRepository(private val localRepository: LocalRepo
}
override fun get(url: String): PublicSettings {
val settings = localRepository.get("$SETTINGS_KEY$url")!!
settings.let {
return adapter.fromJson(it)!!
}
val settings = localRepository.get("$SETTINGS_KEY$url")
return if (settings == null) hashMapOf() else adapter.fromJson(settings) ?: hashMapOf()
}
}
\ No newline at end of file
package chat.rocket.android.server.presentation
import android.content.Intent
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.ui.ChangeServerActivity
class ChangeServerNavigator (internal val activity: ChangeServerActivity) {
fun toServerScreen() {
activity.startActivity(activity.newServerIntent())
activity.finish()
}
fun toChatRooms() {
activity.startActivity(Intent(activity, MainActivity::class.java))
activity.finish()
}
}
\ No newline at end of file
package chat.rocket.android.server.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.util.ifNull
import javax.inject.Inject
class ChangeServerPresenter @Inject constructor(
private val view: ChangeServerView,
private val navigator: ChangeServerNavigator,
private val strategy: CancelStrategy,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val getAccountInteractor: GetAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val settingsRepository: SettingsRepository,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val connectionManager: ConnectionManagerFactory
) {
fun loadServer(newUrl: String?) {
launchUI(strategy) {
view.showProgress()
var url = newUrl
if (url == null) { // Try to load next server on the list...
val accounts = getAccountsInteractor.get()
url = accounts.firstOrNull()?.serverUrl
}
url?.let { serverUrl ->
val token = tokenRepository.get(serverUrl)
if (token == null) {
view.showInvalidCredentials()
view.hideProgress()
navigator.toServerScreen()
return@launchUI
}
val settings = settingsRepository.get(serverUrl)
if (settings == null) {
// TODO - reload settings...
}
// Call disconnect on the old url if any...
getCurrentServerInteractor.get()?.let { url ->
connectionManager.get(url)?.disconnect()
}
// Save the current username.
getAccountInteractor.get(serverUrl)?.let { account ->
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, account.userName)
}
saveCurrentServerInteractor.save(serverUrl)
view.hideProgress()
navigator.toChatRooms()
}.ifNull {
view.hideProgress()
navigator.toServerScreen()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.server.presentation
interface ChangeServerView {
fun showInvalidCredentials()
fun showProgress()
fun hideProgress()
}
\ No newline at end of file
package chat.rocket.android.server.ui
import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.server.presentation.ChangeServerPresenter
import chat.rocket.android.server.presentation.ChangeServerView
import chat.rocket.android.util.extensions.showToast
import dagger.android.AndroidInjection
import javax.inject.Inject
class ChangeServerActivity : AppCompatActivity(), ChangeServerView {
@Inject lateinit var presenter: ChangeServerPresenter
var progress: ProgressDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
val serverUrl: String? = intent.getStringExtra(INTENT_SERVER_URL)
presenter.loadServer(serverUrl)
}
override fun showInvalidCredentials() {
showToast("Missing credentials for this server")
}
override fun showProgress() {
progress = ProgressDialog.show(this, "Rocket.Chat", "Changing Server")
}
override fun hideProgress() {
progress?.dismiss()
}
}
private const val INTENT_SERVER_URL = "INTENT_SERVER_URL"
private const val INTENT_CHAT_ROOM_NAME = "INTENT_CHAT_ROOM_NAME"
private const val INTENT_CHAT_ROOM_TYPE = "INTENT_CHAT_ROOM_TYPE"
fun Context.changeServerIntent(serverUrl: String?): Intent {
return Intent(this, ChangeServerActivity::class.java).apply {
serverUrl?.let { url ->
putExtra(INTENT_SERVER_URL, url)
}
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
\ No newline at end of file
......@@ -29,12 +29,12 @@ fun View.fadeIn(startValue: Float = 0f, finishValue: Float = 1f, duration: Long
animate()
.alpha(startValue)
.setDuration(duration)
.setDuration(duration / 2)
.setInterpolator(DecelerateInterpolator())
.withEndAction({
animate()
.alpha(finishValue)
.setDuration(duration)
.setDuration(duration / 2)
.setInterpolator(AccelerateInterpolator()).start()
}).start()
......
package chat.rocket.android.util.extensions
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.registerPushToken
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
suspend fun RocketChatClient.registerPushToken(
token: String,
accounts: List<Account>,
factory: RocketChatClientFactory
) {
launch(CommonPool) {
accounts.forEach { account ->
try {
factory.create(account.serverUrl).registerPushToken(token)
} catch (ex: Exception) {
Timber.d(ex, "Error registering Push token for ${account.serverUrl}")
ex.printStackTrace()
}
}
}
}
\ No newline at end of file
......@@ -3,12 +3,15 @@ package chat.rocket.android.util.extensions
import android.text.Spannable
import android.text.Spanned
import android.text.TextUtils
import android.util.Base64
import android.util.Patterns
import android.widget.EditText
import android.widget.TextView
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import org.json.JSONObject
import ru.noties.markwon.Markwon
import java.net.URLDecoder
import java.security.SecureRandom
fun String.ifEmpty(value: String): String {
......@@ -33,7 +36,23 @@ fun EditText.erase() {
}
}
fun String.isEmailValid(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun String.isEmail(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun String.encodeToBase64(): String {
return Base64.encodeToString(this.toByteArray(charset("UTF-8")), Base64.NO_WRAP)
}
fun String.decodeFromBase64(): String {
return Base64.decode(this, Base64.DEFAULT).toString(charset("UTF-8"))
}
fun String.decodeUrl(): String {
return URLDecoder.decode(this, "UTF-8")
}
fun String.toJsonObject(): JSONObject {
return JSONObject(this)
}
fun generateRandomString(stringLength: Int): String {
val base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
......
......@@ -27,7 +27,8 @@ fun View.isVisible(): Boolean {
return visibility == View.VISIBLE
}
fun ViewGroup.inflate(@LayoutRes resource: Int): View = LayoutInflater.from(context).inflate(resource, this, false)
fun ViewGroup.inflate(@LayoutRes resource: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(resource, this, attachToRoot)
fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
......@@ -36,10 +37,12 @@ fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () ->
.commit()
}
fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int, newInstance: () -> Fragment) {
fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int,
newInstance: () -> Fragment) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right)
.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left,
R.anim.enter_from_left, R.anim.exit_to_right)
.replace(layoutId, fragment, tag)
.addToBackStack(tag)
.commit()
......@@ -52,13 +55,17 @@ fun Activity.hideKeyboard() {
}
}
fun Activity.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration)
fun Activity.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) =
showToast(getString(resource), duration)
fun Activity.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = Toast.makeText(this, message, duration).show()
fun Activity.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) =
Toast.makeText(this, message, duration).show()
fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration)
fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) =
showToast(getString(resource), duration)
fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = activity!!.showToast(message, duration)
fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) =
activity?.showToast(message, duration)
fun RecyclerView.isAtBottom(): Boolean {
val manager: RecyclerView.LayoutManager? = layoutManager
......
......@@ -13,7 +13,7 @@ import chat.rocket.android.R
import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.*
fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent {
fun Context.casWebViewIntent(webPageUrl: String, casToken: String): Intent {
return Intent(this, CasWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_CAS_TOKEN, casToken)
......@@ -21,7 +21,7 @@ fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent {
}
private const val INTENT_WEB_PAGE_URL = "web_page_url"
private const val INTENT_CAS_TOKEN = "cas_token"
const val INTENT_CAS_TOKEN = "cas_token"
class CasWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
......@@ -49,14 +49,14 @@ class CasWebViewActivity : AppCompatActivity() {
if (web_view.canGoBack()) {
web_view.goBack()
} else {
finishActivity(false)
closeView()
}
}
private fun setupToolbar() {
toolbar.title = getString(R.string.title_authentication)
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
toolbar.setNavigationOnClickListener { finishActivity(false) }
toolbar.setNavigationOnClickListener { closeView() }
}
@SuppressLint("SetJavaScriptEnabled")
......@@ -64,16 +64,16 @@ class CasWebViewActivity : AppCompatActivity() {
web_view.settings.javaScriptEnabled = true
web_view.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
// The user can be already logged in the CAS, so check if the URL contains the "ticket" word
// (that means he/she is successful authenticated and we don't need to wait until the page is finished.
// The user may have already been logged in the CAS, so check if the URL contains the "ticket" word
// (that means the user is successful authenticated and we don't need to wait until the page is fully loaded).
if (url.contains("ticket")) {
finishActivity(true)
closeView(Activity.RESULT_OK)
}
}
override fun onPageFinished(view: WebView, url: String) {
if (url.contains("ticket")) {
finishActivity(true)
closeView(Activity.RESULT_OK)
} else {
view_loading.hide()
}
......@@ -82,13 +82,9 @@ class CasWebViewActivity : AppCompatActivity() {
web_view.loadUrl(webPageUrl)
}
private fun finishActivity(setResultOk: Boolean) {
if (setResultOk) {
setResult(Activity.RESULT_OK, Intent().putExtra(INTENT_CAS_TOKEN, casToken))
finish()
} else {
super.onBackPressed()
}
private fun closeView(activityResult: Int = Activity.RESULT_CANCELED) {
setResult(activityResult, Intent().putExtra(INTENT_CAS_TOKEN, casToken))
finish()
overridePendingTransition(R.anim.hold, R.anim.slide_down)
}
}
\ No newline at end of file
package chat.rocket.android.webview.oauth.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.net.toUri
import chat.rocket.android.R
import chat.rocket.android.util.extensions.decodeUrl
import chat.rocket.android.util.extensions.toJsonObject
import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.*
import org.json.JSONObject
fun Context.oauthWebViewIntent(webPageUrl: String, state: String): Intent {
return Intent(this, OauthWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_STATE, state)
}
}
private const val INTENT_WEB_PAGE_URL = "web_page_url"
private const val INTENT_STATE = "state"
private const val JSON_CREDENTIAL_TOKEN = "credentialToken"
private const val JSON_CREDENTIAL_SECRET = "credentialSecret"
const val INTENT_OAUTH_CREDENTIAL_TOKEN = "credential_token"
const val INTENT_OAUTH_CREDENTIAL_SECRET = "credential_secret"
// Shows a WebView to the user authenticate with your Gitlab credentials.
class OauthWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
private lateinit var state: String
private var isWebViewSetUp: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
state = intent.getStringExtra(INTENT_STATE)
requireNotNull(state) { "no state provided in Intent extras" }
setupToolbar()
}
override fun onResume() {
super.onResume()
if (!isWebViewSetUp) {
setupWebView()
isWebViewSetUp = true
}
}
override fun onBackPressed() {
if (web_view.canGoBack()) {
web_view.goBack()
} else {
closeView()
}
}
private fun setupToolbar() {
with(toolbar) {
title = getString(R.string.title_authentication)
setNavigationIcon(R.drawable.ic_close_white_24dp)
setNavigationOnClickListener { closeView() }
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
with(web_view.settings) {
javaScriptEnabled = true
// TODO This is required to make Google OAuth work, but we shoud use Custom Tabs instead. See https://github.com/RocketChat/Rocket.Chat.Android/issues/968
userAgentString = "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19"
}
web_view.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
if (url.contains(JSON_CREDENTIAL_TOKEN) && url.contains(JSON_CREDENTIAL_SECRET)) {
if (isStateValid(url)) {
val jsonResult = url.decodeUrl()
.substringAfter("#")
.toJsonObject()
val credentialToken = getCredentialToken(jsonResult)
val credentialSecret = getCredentialSecret(jsonResult)
if (credentialToken.isNotEmpty() && credentialSecret.isNotEmpty()) {
closeView(Activity.RESULT_OK, credentialToken, credentialSecret)
}
}
}
view_loading.hide()
}
}
web_view.loadUrl(webPageUrl)
}
// If the states matches, then try to get the code, otherwise the request was created by a third party and the process should be aborted.
private fun isStateValid(url: String): Boolean =
url.substringBefore("#").toUri().getQueryParameter(INTENT_STATE) == state
private fun getCredentialToken(json: JSONObject): String =
json.optString(JSON_CREDENTIAL_TOKEN)
private fun getCredentialSecret(json: JSONObject): String =
json.optString(JSON_CREDENTIAL_SECRET)
private fun closeView(activityResult: Int = Activity.RESULT_CANCELED, credentialToken: String? = null, credentialSecret: String? = null) {
setResult(activityResult, Intent().putExtra(INTENT_OAUTH_CREDENTIAL_TOKEN, credentialToken).putExtra(INTENT_OAUTH_CREDENTIAL_SECRET, credentialSecret))
finish()
overridePendingTransition(R.anim.hold, R.anim.slide_down)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:endColor="#00000000"
android:centerColor="#30000000"
android:startColor="#C0000000"
android:type="linear" />
</shape>
\ No newline at end of file
......@@ -3,9 +3,7 @@
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF2F343D"
android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
\ No newline at end of file
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
</vector>
......@@ -23,11 +23,26 @@
android:layout_height="match_parent" />
</LinearLayout>
<android.support.design.widget.NavigationView
android:id="@+id/view_navigation"
<FrameLayout
android:id="@+id/navigation_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/navigation" />
android:layout_gravity="start">
<android.support.design.widget.NavigationView
android:id="@+id/view_navigation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:menu="@menu/navigation"
app:headerLayout="@layout/nav_header" />
<android.support.v7.widget.RecyclerView
android:id="@+id/accounts_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/nav_header_height"
android:elevation="20dp"
android:background="@color/white"
android:alpha="0"
android:visibility="gone"/>
</FrameLayout>
</android.support.v4.widget.DrawerLayout>
\ No newline at end of file
......@@ -113,6 +113,7 @@
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_facebook"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_facebook"
......@@ -124,6 +125,7 @@
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_github"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_github"
......@@ -135,6 +137,7 @@
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_google"
android:foreground="?android:attr/selectableItemBackground"
android:src="@drawable/ic_google"
......@@ -146,6 +149,7 @@
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_linkedin"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_linkedin"
......@@ -157,6 +161,7 @@
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_meteor"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_meteor"
......@@ -168,6 +173,7 @@
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_twitter"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_twitter"
......@@ -179,6 +185,7 @@
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_gitlab"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_gitlab"
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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