Unverified Commit ac5c1dbc authored by Rafael Kellermann Streit's avatar Rafael Kellermann Streit Committed by GitHub

Merge pull request #1127 from RocketChat/develop

[RELEASE] Merge develop into beta
parents ca864422 db38a4ab
Your Rocket.Chat.Android version: (make sure you are running the latest)
<!-- Version can be found by opening the side menu and then clicking on the chevron alongside username -->
## Description
<!-- Please, describe what's the issue here. -->
## Devices and Versions
<!-- Version can be found by opening the side menu and then clicking on "Settings" and then "About" -->
Your Rocket.Chat.Android version: (e.g. 2.1.0)
Your Rocket.Chat Server version: (e.g. 0.63.1-develop)
<!-- Found a bug? List all devices that reproduced it and all that doesn't -->
Mobile device model and OS version: (e.g. "Nexus 7 - Android 6.0.1")
<!-- Don't forget to list the steps to reproduce. Stack traces may help too :) -->
## Steps to reproduce
<!-- In case it is a bug, can you describe the steps to reproduce it please? -->
## Logs
<!-- Do you have any logs? It can help the developers indentifying the cause in case it's a bug. -->
<!-- To get the logs, you can use [Logcat](https://developer.android.com/studio/debug/am-logcat.html) in Android Studio or you can use [Pidcat](https://github.com/JakeWharton/pidcat) -->
# Mobile Team Releases Planning
This document describes the suggested method by which our mobile teams should plan for releases.
**What’s a release?**
The release is every version of the app that’s sent to the store as a production release. The release is always a major/minor update, like from (1.0.0 to 1.1.0 or to 2.0.0). Patch releases (from 1.0.0 to 1.0.1) won’t follow these steps and are considered hotfixes releases. Our versioning is following the [Semantic Versioning 2.0.0](https://semver.org) guide.
**What are the important days of a release?**
The first release candidate (TestFlight and Beta) needs be done by 27th of each month. The release will always happen on day 5th of each month, unless there’s some critical crash/bug happening.
**What happens if something could not be done in time for the release candidate?**
In general, if it’s a new feature, it’ll be postponed for the next release only. Under extreme circumstances, when it will result in significant business impact a extraordinary release could happen.
**When do we plan the release features/improvements/bugs?**
Every month can be a different day between 27th and 5th to plan the next release. The leader of the team will schedule the session and all the team will be able to participate in the planning. At this moment, most of the issues will be assigned to each member of the team.
Example (in April, 2018):
| Day | Description |
|------------|-------------------------|
| 27th Mar ~ 5th Apr | Planning new cycle |
| 5th Apr | Start new release cycle |
| 27th Apr | Release candidate |
| 5th May | Production release |
**How do we organize a release?**
Every release is a Project in GitHub. There are 6 boards on each project:
- **Desirable (temporary):** what we want to have on the release. This is very useful while planning. This is where everybody can add features/improvements that wanna see on the release;
- **Blocked:** when something is blocked (waiting asset, waiting API, etc) the issue will be on this board;
- **To-do:** after planning, all to-do issues come here;
- **In progress:** when something is in progress, the issue/PR will be on this board;
- **Review/QA:** when something is done and waiting for review or waiting to be tested, the issue/PR will be on this board;
- **Done:** when the issue is closed (merged), the issue/PR will be on this board;
**What happens when the release candidate is shipped?**
All changes in develop needs to be merged into the branch beta at this point. A new tag needs to be created following the pattern: “2.1.0-beta1”.
**What happens if there’s no bug/crash on the release candidate?**
That’s great, congrats! This time can be used in a creative way: write more tests, code maintenance that sometimes is required, resolving issues to the next release, planning, ideas and experiments.
**What happens when the release is done?**
Project and milestones are closed, all the changes are merged to the branch master and the tag is created, following the release’s pattern of the repository.
## Hotfix Releases
**When a hotfix release happen?**
Hotfix release will happen when a critical bug or crash is found in the production version of the app.
**How to handle hotfix releases?**
Simply open an issue on GitHub describing the issue, the issue is usually closed from a pull-request getting merged and a new milestone is created with the minor update, including all PRs required to the hotfix be completed. Once the milestone is completed, it can be closed and the release tag can be created.
......@@ -19,18 +19,12 @@ Since both the versions use `kotlin` for some or all of their classes, following
- After checking out to `develop` branch as mentioned above, simply import the project in Android Studio.
#### v2+
- This version requires the [Kotlin SDK](https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK) for Rocket.Chat. Clone the Kotlin SDK in the **same directory** as the android repository by running `git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git`. Make sure that the android repository and the kotlin sdk have the same immediate parent directory.
- First, a build is required for the SDK. Change your directory to the SDK directory by running `cd Rocket.Chat.Kotlin.SDK/` in your terminal. Any of the following approaches can be followed to successfully build the SDK.
- **Command Line** - (Within the kotlin SDK directory) Run `./gradlew clean && ./gradlew assemble` to successfully build the project.
- **Android Studio** - Import the project in Android Studio. Go to `Build > Make Project` to build the SDK successfully.
After following the above methods, follow the following steps in your terminal window:
- This version requires the [Kotlin SDK](https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK) for Rocket.Chat. Clone the Kotlin SDK in by running `git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git`.
- First, a build is required for the SDK, so that required jar files are generated. Make sure that the android repository and the kotlin sdk have the same immediate parent directory. Change the current directory to `Rocket.Chat.Android/app` and run the `build-sdk.sh` which will result in creating of the required jar file `core*.jar` and `common*.jar` in `Rocket.Chat.Android/app/libs`,by the following steps in your terminal window:
```
cd ..
cd Rocket.Chat.Android/app/libs
ls
cd Rocket.Chat.Android/app
./build-sdk.sh
```
Two `jar` files will be found in this directory (the `common` and `core` jar files), this indicates that the SDK was built correctly.
- After the SDK has been built successfully, import the project in Android Studio and build it by following `Build > Make Project`.
## How to run
### Command Line
......
......@@ -13,8 +13,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 2001
versionName "2.0.0-beta1"
versionCode 2012
versionName "2.0.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......@@ -124,6 +124,10 @@ kotlin {
}
}
androidExtensions {
experimental = true
}
// FIXME - build and install the sdk into the app/libs directory
// We were having some issues with the kapt generated files from the sdk when importing as a module
task compileSdk(type:Exec) {
......
......@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission
......@@ -16,9 +17,11 @@
<application
android:name=".app.RocketChatApplication"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true">
<activity
......@@ -32,43 +35,48 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="auth"
android:scheme="rocketchat" />
<data
android:host="go.rocket.chat"
android:path="/auth"
android:scheme="https" />
</intent-filter>
</activity>
<activity
android:name=".server.ui.ChangeServerActivity"
android:theme="@style/AuthenticationTheme" />
<activity
android:name=".main.ui.MainActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".webview.ui.WebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".webview.cas.ui.CasWebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".webview.oauth.ui.OauthWebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".chatroom.ui.ChatRoomActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".chatroom.ui.PinnedMessagesActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".settings.password.ui.PasswordActivity"
android:theme="@style/AppTheme" />
......@@ -85,6 +93,15 @@
</intent-filter>
</receiver>
<receiver
android:name=".push.DirectReplyReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="chat.rocket.android.ACTION_REPLY" />
</intent-filter>
</receiver>
<service
android:name=".push.FirebaseTokenService"
android:exported="false">
......@@ -92,7 +109,6 @@
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<service
android:name=".push.GcmListenerService"
android:exported="false">
......@@ -100,10 +116,18 @@
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<service
android:name=".chatroom.service.MessageService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<meta-data
android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
<activity
android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme" />
</application>
</manifest>
\ No newline at end of file
</manifest>
......@@ -2,7 +2,6 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
import android.support.v4.graphics.drawable.DrawableCompat
import android.widget.EditText
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.common.model.UserStatus
......@@ -16,7 +15,8 @@ object DrawableHelper {
* @param context The context.
* @return A drawable.
*/
fun getDrawableFromId(id: Int, context: Context): Drawable = context.resources.getDrawable(id, null)
fun getDrawableFromId(id: Int, context: Context): Drawable =
context.resources.getDrawable(id, null)
/**
* Wraps an array of Drawable to be used for example for tinting.
......@@ -68,7 +68,8 @@ object DrawableHelper {
* @see tintDrawables
* @see wrapDrawable
*/
fun tintDrawable(drawable: Drawable, context: Context, resId: Int) = DrawableCompat.setTint(drawable, ContextCompat.getColor(context, resId))
fun tintDrawable(drawable: Drawable, context: Context, resId: Int) =
DrawableCompat.setTint(drawable, ContextCompat.getColor(context, resId))
/**
* Compounds an array of Drawable (to appear to the left of the text) into an array of TextView.
......@@ -96,25 +97,29 @@ object DrawableHelper {
* @param drawable The Drawable.
* @see compoundDrawables
*/
fun compoundDrawable(textView: TextView, drawable: Drawable) = textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
fun compoundDrawable(textView: TextView, drawable: Drawable) =
textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
/**
* Returns the user status drawable.
*
* @param userStatus The user status.
* @param context The context.
* @see [UserStatus]
* @return The user status drawable.
*/
fun getUserStatusDrawable(userStatus: UserStatus, context: Context): Drawable {
val userStatusDrawable = getDrawableFromId(R.drawable.ic_user_status_black, context).mutate()
wrapDrawable(userStatusDrawable)
when (userStatus) {
is UserStatus.Online -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOnline)
is UserStatus.Busy -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusBusy)
is UserStatus.Away -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusAway)
is UserStatus.Offline -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
else -> tintDrawable(userStatusDrawable, context, R.color.colorUserStatusOffline)
return when (userStatus) {
is UserStatus.Online -> {
getDrawableFromId(R.drawable.ic_status_online_24dp, context)
}
is UserStatus.Away -> {
getDrawableFromId(R.drawable.ic_status_away_24dp, context)
}
is UserStatus.Busy -> {
getDrawableFromId(R.drawable.ic_status_busy_24dp, context)
}
else -> getDrawableFromId(R.drawable.ic_status_invisible_24dp, context)
}
return userStatusDrawable
}
}
\ No newline at end of file
......@@ -6,7 +6,7 @@ import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.SharedPreferences
import androidx.content.edit
import androidx.core.content.edit
import chat.rocket.android.BuildConfig
import chat.rocket.android.app.migration.RealmMigration
import chat.rocket.android.app.migration.RocketChatLibraryModule
......@@ -18,10 +18,11 @@ import chat.rocket.android.app.migration.model.RealmUser
import chat.rocket.android.authentication.domain.model.toToken
import chat.rocket.android.dagger.DaggerAppComponent
import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.common.model.Token
import chat.rocket.core.model.Value
......@@ -148,12 +149,12 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
private fun migrateServerInfo(url: String, authToken: String, settings: PublicSettings, user: RealmUser) {
val userId = user._id
val avatar = UrlHelper.getAvatarUrl(url, user.username!!)
val avatar = url.avatarUrl(user.username!!)
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(url, it)
url.serverLogoUrl(it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(url, it)
url.serverLogoUrl(it)
}
val account = Account(url, icon, logo, user.username!!, avatar)
launch(CommonPool) {
......
package chat.rocket.android.authentication.domain.model
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
@SuppressLint("ParcelCreator")
@Parcelize
data class LoginDeepLinkInfo(
val url: String,
val userId: String,
val token: String
) : Parcelable
fun Intent.getLoginDeepLinkInfo(): LoginDeepLinkInfo? {
val uri = data
return if (action == Intent.ACTION_VIEW && uri != null && uri.isAuthenticationDeepLink()) {
val host = uri.getQueryParameter("host")
val url = if (host.startsWith("http")) host else "https://$host"
val userId = uri.getQueryParameter("userId")
val token = uri.getQueryParameter("token")
try {
LoginDeepLinkInfo(url, userId, token)
} catch (ex: Exception) {
Timber.d(ex, "Error parsing login deeplink")
null
}
} else null
}
private inline fun Uri.isAuthenticationDeepLink(): Boolean {
if (host == "auth")
return true
else if (host == "go.rocket.chat" && path == "/auth")
return true
return false
}
\ No newline at end of file
package chat.rocket.android.authentication.infraestructure
import android.content.SharedPreferences
import androidx.content.edit
import androidx.core.content.edit
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.common.model.Token
......
package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface LoginView : LoadingView, MessageView, InternetView, VersionCheckView {
interface LoginView : LoadingView, MessageView {
/**
* Shows the form view (i.e the username/email and password fields) if it is enabled by the server settings.
......@@ -146,6 +145,14 @@ interface LoginView : LoadingView, MessageView, InternetView, VersionCheckView {
*/
fun setupLinkedinButtonListener(linkedinUrl: String, state: String)
/**
* Setups the Facebook button when tapped.
*
* @param facebookOauthUrl The Facebook 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 setupFacebookButtonListener(facebookOauthUrl: String, state: String)
/**
* Shows the "login by Meteor" view if it is enable by the server settings.
*/
......@@ -187,9 +194,4 @@ interface LoginView : LoadingView, MessageView, InternetView, VersionCheckView {
* Alerts the user about a wrong inputted password.
*/
fun alertWrongPassword()
/**
* Alerts the user about the need of creating an username using the web app when creating an user through OAuth.
*/
fun alertRequiresUsername()
}
\ No newline at end of file
......@@ -2,7 +2,9 @@ package chat.rocket.android.authentication.presentation
import android.content.Intent
import chat.rocket.android.R
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.login.ui.LoginFragment
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity
......@@ -20,6 +22,12 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
}
}
fun toLogin(deepLinkInfo: LoginDeepLinkInfo) {
activity.addFragmentBackStack("LoginFragment", R.id.fragment_container) {
LoginFragment.newInstance(deepLinkInfo)
}
}
fun toTwoFA(username: String, password: String) {
activity.addFragmentBackStack("TwoFAFragment", R.id.fragment_container) {
TwoFAFragment.newInstance(username, password)
......@@ -37,6 +45,12 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
activity.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
fun toRegisterUsername(userId: String, authToken: String) {
activity.addFragmentBackStack("RegisterUsernameFragment", R.id.fragment_container) {
RegisterUsernameFragment.newInstance(userId, authToken)
}
}
fun toChatList() {
activity.startActivity(Intent(activity, MainActivity::class.java))
activity.finish()
......
......@@ -25,7 +25,7 @@ class AuthenticationPresenter @Inject constructor(
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, account.userName)
}
if (newServer || currentServer == null || serverToken == null || settings == null) {
if (newServer || currentServer == null || serverToken == null || settings == null || account?.userName == null) {
callback(false)
} else {
callback(true)
......
package chat.rocket.android.authentication.registerusername.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernameView
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class RegisterUsernameFragmentModule {
@Provides
fun registerUsernameView(frag: RegisterUsernameFragment): RegisterUsernameView {
return frag
}
@Provides
fun provideLifecycleOwner(frag: RegisterUsernameFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
\ No newline at end of file
package chat.rocket.android.authentication.registerusername.di
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class RegisterUsernameFragmentProvider {
@ContributesAndroidInjector(modules = [RegisterUsernameFragmentModule::class])
abstract fun provideRegisterUsernameFragment(): RegisterUsernameFragment
}
\ No newline at end of file
package chat.rocket.android.authentication.registerusername.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
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.avatarUrl
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.Token
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.updateOwnBasicInformation
import javax.inject.Inject
class RegisterUsernamePresenter @Inject constructor(
private val view: RegisterUsernameView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
serverInteractor: GetCurrentServerInteractor,
settingsInteractor: GetSettingsInteractor
) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
fun registerUsername(username: String, userId: String, authToken: String) {
if (username.isBlank()) {
view.alertBlankUsername()
} else {
launchUI(strategy) {
view.showLoading()
try {
val me = retryIO("updateOwnBasicInformation(username = $username)") {
client.updateOwnBasicInformation(username = username)
}
val registeredUsername = me.username
if (registeredUsername != null) {
saveAccount(registeredUsername)
tokenRepository.save(currentServer, Token(userId, authToken))
registerPushToken()
navigator.toChatList()
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
}
}
}
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// 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(username: String) {
val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it)
}
val logo = settings.wideTile()?.let {
currentServer.serverLogoUrl(it)
}
val thumb = currentServer.avatarUrl(username)
val account = Account(currentServer, icon, logo, username, thumb)
saveAccountInteractor.save(account)
}
}
\ No newline at end of file
package chat.rocket.android.authentication.registerusername.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface RegisterUsernameView : LoadingView, MessageView {
/**
* Alerts the user about a blank username.
*/
fun alertBlankUsername()
}
\ No newline at end of file
package chat.rocket.android.authentication.registerusername.ui
import DrawableHelper
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernamePresenter
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernameView
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_register_username.*
import javax.inject.Inject
class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
@Inject lateinit var presenter: RegisterUsernamePresenter
private lateinit var userId: String
private lateinit var authToken: String
companion object {
private const val USER_ID = "user_id"
private const val AUTH_TOKEN = "auth_token"
fun newInstance(userId: String, authToken: String) = RegisterUsernameFragment().apply {
arguments = Bundle(1).apply {
putString(USER_ID, userId)
putString(AUTH_TOKEN, authToken)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
// TODO - research a better way to initialize parameters on fragments.
userId = arguments?.getString(USER_ID) ?: ""
authToken = arguments?.getString(AUTH_TOKEN) ?: ""
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_register_username)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.apply {
text_username.requestFocus()
showKeyboard(text_username)
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart()
}
setupOnClickListener()
}
override fun alertBlankUsername() {
ui {
vibrateSmartPhone()
text_username.shake()
}
}
override fun showLoading() {
ui {
disableUserInput()
view_loading.setVisible(true)
}
}
override fun hideLoading() {
ui {
view_loading.setVisible(false)
enableUserInput()
}
}
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
private fun tintEditTextDrawableStart() {
ui {
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, it)
DrawableHelper.wrapDrawable(atDrawable)
DrawableHelper.tintDrawable(atDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_username, atDrawable)
}
}
private fun enableUserInput() {
button_use_this_username.isEnabled = true
text_username.isEnabled = true
}
private fun disableUserInput() {
button_use_this_username.isEnabled = false
text_username.isEnabled = true
}
private fun setupOnClickListener() {
button_use_this_username.setOnClickListener {
presenter.registerUsername(text_username.textContent, userId, authToken)
}
}
}
\ No newline at end of file
package chat.rocket.android.authentication.server.presentation
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.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.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.isValidUrl
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.util.ifNull
import javax.inject.Inject
class ServerPresenter @Inject constructor(private val view: ServerView,
......@@ -16,9 +18,27 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor) {
private val getAccountsInteractor: GetAccountsInteractor,
factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, view) {
fun checkServer(server: String) {
if (!server.isValidUrl()) {
view.showInvalidServerUrlMessage()
} else {
view.showLoading()
checkServerInfo(server)
}
}
fun connect(server: String) {
if (!UrlHelper.isValidUrl(server)) {
connectToServer(server) {
navigator.toLogin()
}
}
private fun connectToServer(server: String, block: () -> Unit) {
if (!server.isValidUrl()) {
view.showInvalidServerUrlMessage()
} else {
launchUI(strategy) {
......@@ -29,25 +49,23 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
return@launchUI
}
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
try {
refreshSettingsInteractor.refresh(server)
serverInteractor.save(server)
navigator.toLogin()
} catch (ex: Exception) {
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
} else {
view.showNoInternetConnection()
view.showLoading()
try {
refreshSettingsInteractor.refresh(server)
serverInteractor.save(server)
block()
} catch (ex: Exception) {
view.showMessage(ex)
} finally {
view.hideLoading()
}
}
}
}
fun deepLink(deepLinkInfo: LoginDeepLinkInfo) {
connectToServer(deepLinkInfo.url) {
navigator.toLogin(deepLinkInfo)
}
}
}
\ No newline at end of file
package chat.rocket.android.authentication.server.presentation
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface ServerView : LoadingView, MessageView, InternetView {
interface ServerView : LoadingView, MessageView, VersionCheckView {
/**
* Shows an invalid server URL message.
......
......@@ -10,4 +10,9 @@ interface VersionCheckView {
* Block user to proceed and alert him due to server having an unsupported server version.
*/
fun blockAndAlertNotRequiredVersion()
/**
* Do some action if version is ok. This is optional.
*/
fun versionOk() {}
}
\ No newline at end of file
package chat.rocket.android.authentication.server.ui
import android.app.AlertDialog
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.server.presentation.ServerPresenter
import chat.rocket.android.authentication.server.presentation.ServerView
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.util.extensions.*
import chat.rocket.common.util.ifNull
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_server.*
import javax.inject.Inject
class ServerFragment : Fragment(), ServerView {
@Inject lateinit var presenter: ServerPresenter
@Inject
lateinit var presenter: ServerPresenter
private var deepLinkInfo: LoginDeepLinkInfo? = null
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
text_server_url.isCursorVisible = KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)
}
companion object {
fun newInstance() = ServerFragment()
private const val DEEP_LINK_INFO = "DeepLinkInfo"
fun newInstance(deepLinkInfo: LoginDeepLinkInfo?) = ServerFragment().apply {
arguments = Bundle().apply {
putParcelable(DEEP_LINK_INFO, deepLinkInfo)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
deepLinkInfo = arguments?.getParcelable(DEEP_LINK_INFO)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
......@@ -37,6 +52,12 @@ class ServerFragment : Fragment(), ServerView {
super.onViewCreated(view, savedInstanceState)
relative_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
setupOnClickListener()
deepLinkInfo?.let {
val uri = Uri.parse(it.url)
uri?.let { text_server_protocol.hintContent = it.host }
presenter.deepLink(it)
}
}
override fun onDestroyView() {
......@@ -47,29 +68,76 @@ class ServerFragment : Fragment(), ServerView {
override fun showInvalidServerUrlMessage() = showMessage(getString(R.string.msg_invalid_server_url))
override fun showLoading() {
enableUserInput(false)
view_loading.setVisible(true)
ui {
enableUserInput(false)
view_loading.setVisible(true)
}
}
override fun hideLoading() {
view_loading.setVisible(false)
enableUserInput(true)
ui {
view_loading.setVisible(false)
enableUserInput(true)
}
}
override fun showMessage(resId: Int){
showToast(resId)
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
override fun showNoInternetConnection() {
showMessage(getString(R.string.msg_no_internet_connection))
override fun alertNotRecommendedVersion() {
ui {
hideLoading()
AlertDialog.Builder(it)
.setMessage(getString(R.string.msg_ver_not_recommended, BuildConfig.RECOMMENDED_SERVER_VERSION))
.setPositiveButton(R.string.msg_ok, { _, _ ->
performConnect()
})
.create()
.show()
}
}
override fun blockAndAlertNotRequiredVersion() {
ui {
hideLoading()
AlertDialog.Builder(it)
.setMessage(getString(R.string.msg_ver_not_minimum, BuildConfig.REQUIRED_SERVER_VERSION))
.setPositiveButton(R.string.msg_ok, null)
.setOnDismissListener {
// reset the deeplink info, so the user can log to another server...
deepLinkInfo = null
}
.create()
.show()
}
}
override fun versionOk() {
performConnect()
}
private fun performConnect() {
ui {
deepLinkInfo?.let {
presenter.deepLink(it)
}.ifNull {
val url = text_server_url.textContent.ifEmpty(text_server_url.hintContent)
presenter.connect(text_server_protocol.textContent + url)
}
}
}
private fun enableUserInput(value: Boolean) {
......@@ -78,9 +146,11 @@ class ServerFragment : Fragment(), ServerView {
}
private fun setupOnClickListener() {
button_connect.setOnClickListener {
val url = text_server_url.textContent.ifEmpty(text_server_url.hintContent)
presenter.connect(text_server_protocol.textContent + url)
ui {
button_connect.setOnClickListener {
val url = text_server_url.textContent.ifEmpty(text_server_url.hintContent)
presenter.checkServer(text_server_protocol.textContent + url)
}
}
}
}
\ No newline at end of file
......@@ -2,21 +2,22 @@ package chat.rocket.android.authentication.signup.presentation
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.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.avatarUrl
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.privacyPolicyUrl
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.extensions.termsOfServiceUrl
import chat.rocket.android.util.retryIO
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.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
......@@ -55,31 +56,26 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
else -> {
val client = factory.create(server)
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
try {
// TODO This function returns a user so should we save it?
client.signup(email, name, username, password)
// TODO This function returns a user token so should we save it?
client.login(username, password)
val me = client.me()
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
view.showLoading()
try {
// TODO This function returns a user so should we save it?
retryIO("signup") { client.signup(email, name, username, password) }
// TODO This function returns a user token so should we save it?
retryIO("login") { client.login(username, password) }
val me = retryIO("me") { client.me() }
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} else {
view.showNoInternetConnection()
} finally {
view.hideLoading()
}
}
}
......@@ -88,13 +84,13 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
fun termsOfService() {
serverInteractor.get()?.let {
navigator.toWebPage(UrlHelper.getTermsOfServiceUrl(it))
navigator.toWebPage(it.termsOfServiceUrl())
}
}
fun privacyPolicy() {
serverInteractor.get()?.let {
navigator.toWebPage(UrlHelper.getPrivacyPolicyUrl(it))
navigator.toWebPage(it.privacyPolicyUrl())
}
}
......@@ -108,12 +104,12 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
private suspend fun saveAccount(me: Myself) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
currentServer.serverLogoUrl(it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
currentServer.serverLogoUrl(it)
}
val thumb = UrlHelper.getAvatarUrl(currentServer, me.username!!)
val thumb = currentServer.avatarUrl(me.username!!)
val account = Account(currentServer, icon, logo, me.username!!, thumb)
saveAccountInteractor.save(account)
}
......
package chat.rocket.android.authentication.signup.presentation
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface SignupView : LoadingView, MessageView, InternetView {
interface SignupView : LoadingView, MessageView {
/**
* Alerts the user about a blank name.
......
......@@ -25,7 +25,7 @@ class SignupFragment : Fragment(), SignupView {
} else {
bottom_container.apply {
postDelayed({
setVisible(true)
ui { setVisible(true) }
}, 3)
}
}
......@@ -64,65 +64,77 @@ class SignupFragment : Fragment(), SignupView {
}
override fun alertBlankName() {
vibrateSmartPhone()
text_name.shake()
text_name.requestFocus()
ui {
vibrateSmartPhone()
text_name.shake()
text_name.requestFocus()
}
}
override fun alertBlankUsername() {
vibrateSmartPhone()
text_username.shake()
text_username.requestFocus()
ui {
vibrateSmartPhone()
text_username.shake()
text_username.requestFocus()
}
}
override fun alertEmptyPassword() {
vibrateSmartPhone()
text_password.shake()
text_password.requestFocus()
ui {
vibrateSmartPhone()
text_password.shake()
text_password.requestFocus()
}
}
override fun alertBlankEmail() {
vibrateSmartPhone()
text_email.shake()
text_email.requestFocus()
ui {
vibrateSmartPhone()
text_email.shake()
text_email.requestFocus()
}
}
override fun showLoading() {
enableUserInput(false)
view_loading.setVisible(true)
ui {
enableUserInput(false)
view_loading.setVisible(true)
}
}
override fun hideLoading() {
view_loading.setVisible(false)
enableUserInput(true)
ui {
view_loading.setVisible(false)
enableUserInput(true)
}
}
override fun showMessage(resId: Int) {
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
override fun showNoInternetConnection() {
Toast.makeText(activity, getString(R.string.msg_no_internet_connection), Toast.LENGTH_SHORT).show()
}
private fun tintEditTextDrawableStart() {
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, this)
ui {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, it)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, it)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, it)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, it)
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.tintDrawables(drawables, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_password, text_email), drawables)
}
}
......
......@@ -2,14 +2,15 @@ package chat.rocket.android.authentication.twofactor.presentation
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.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
......@@ -46,32 +47,29 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
else -> {
launchUI(strategy) {
val client = factory.create(server)
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
try {
// The token is saved via the client TokenProvider
val token =
client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
val me = client.me()
saveAccount(me)
tokenRepository.save(server, token)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
if (exception is RocketChatAuthException) {
view.alertInvalidTwoFactorAuthenticationCode()
} else {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showLoading()
try {
// The token is saved via the client TokenProvider
val token = retryIO("login") {
client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
}
val me = retryIO("me") { client.me() }
saveAccount(me)
tokenRepository.save(server, token)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
if (exception is RocketChatAuthException) {
view.alertInvalidTwoFactorAuthenticationCode()
} else {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
} else {
view.showNoInternetConnection()
} finally {
view.hideLoading()
}
}
}
......@@ -90,12 +88,12 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private suspend fun saveAccount(me: Myself) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
currentServer.serverLogoUrl(it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
currentServer.serverLogoUrl(it)
}
val thumb = UrlHelper.getAvatarUrl(currentServer, me.username!!)
val thumb = currentServer.avatarUrl(me.username!!)
val account = Account(currentServer, icon, logo, me.username!!, thumb)
saveAccountInteractor.save(account)
}
......
package chat.rocket.android.authentication.twofactor.presentation
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface TwoFAView : LoadingView, MessageView, InternetView {
interface TwoFAView : LoadingView, MessageView {
/**
* Alerts the user about a blank Two Factor Authentication code.
......
......@@ -28,7 +28,7 @@ class TwoFAFragment : Fragment(), TwoFAView {
private const val PASSWORD = "password"
fun newInstance(username: String, password: String) = TwoFAFragment().apply {
arguments = Bundle(1).apply {
arguments = Bundle(2).apply {
putString(USERNAME, username)
putString(PASSWORD, password)
}
......@@ -44,7 +44,7 @@ class TwoFAFragment : Fragment(), TwoFAView {
password = arguments?.getString(PASSWORD) ?: ""
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_authentication_two_fa, container, false)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_two_fa)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -58,12 +58,15 @@ class TwoFAFragment : Fragment(), TwoFAView {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart()
}
setupOnClickListener()
}
override fun alertBlankTwoFactorAuthenticationCode() {
vibrateSmartPhone()
text_two_factor_auth.shake()
ui {
vibrateSmartPhone()
text_two_factor_auth.shake()
}
}
override fun alertInvalidTwoFactorAuthenticationCode() {
......@@ -71,32 +74,38 @@ class TwoFAFragment : Fragment(), TwoFAView {
}
override fun showLoading() {
enableUserInput(false)
view_loading.setVisible(true)
ui {
enableUserInput(false)
view_loading.setVisible(true)
}
}
override fun hideLoading() {
view_loading.setVisible(false)
enableUserInput(true)
ui {
view_loading.setVisible(false)
enableUserInput(true)
}
}
override fun showMessage(resId: Int) {
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection))
private fun tintEditTextDrawableStart() {
activity?.apply {
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, this)
ui {
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, it)
DrawableHelper.wrapDrawable(lockDrawable)
DrawableHelper.tintDrawable(lockDrawable, this, R.color.colorDrawableTintGrey)
DrawableHelper.tintDrawable(lockDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_two_factor_auth, lockDrawable)
}
}
......
......@@ -6,10 +6,11 @@ import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.domain.model.getLoginDeepLinkInfo
import chat.rocket.android.authentication.presentation.AuthenticationPresenter
import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.launchUI
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......@@ -30,11 +31,13 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState)
val deepLinkInfo = intent.getLoginDeepLinkInfo()
launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
presenter.loadCredentials(newServer) { authenticated ->
// if we got authenticateWithDeepLink information, pass true to newServer also
presenter.loadCredentials(newServer || deepLinkInfo != null) { authenticated ->
if (!authenticated) {
showServerInput(savedInstanceState)
showServerInput(savedInstanceState, deepLinkInfo)
}
}
}
......@@ -49,9 +52,9 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector
}
fun showServerInput(savedInstanceState: Bundle?) {
fun showServerInput(savedInstanceState: Bundle?, deepLinkInfo: LoginDeepLinkInfo?) {
addFragment("ServerFragment", R.id.fragment_container) {
ServerFragment.newInstance()
ServerFragment.newInstance(deepLinkInfo)
}
}
}
......
package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.net.Uri
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.viewmodel.AuthorAttachmentViewModel
import chat.rocket.android.util.extensions.content
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.common.util.ifNull
import kotlinx.android.synthetic.main.item_author_attachment.view.*
class AuthorAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<AuthorAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(author_attachment_container)
setupActionMenu(text_fields)
setupActionMenu(text_author_name)
}
}
override fun bindViews(data: AuthorAttachmentViewModel) {
with(itemView) {
data.icon?.let { icon ->
author_icon.isVisible = true
author_icon.setImageURI(icon)
}.ifNull {
author_icon.isGone = true
}
author_icon.setImageURI(data.icon)
text_author_name.content = data.name
data.fields?.let { fields ->
text_fields.content = fields
text_fields.isVisible = true
}.ifNull {
text_fields.isGone = true
}
text_author_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.attachmentUrl)))
}
}
}
}
\ No newline at end of file
......@@ -53,6 +53,14 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_message_attachment)
MessageAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.AUTHOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_author_attachment)
AuthorAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.COLOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_color_attachment)
ColorAttachmentViewHolder(view, actionsListener, reactionListener)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
......@@ -92,6 +100,8 @@ class ChatRoomAdapter(
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
is AuthorAttachmentViewHolder -> holder.bind(dataSet[position] as AuthorAttachmentViewModel)
is ColorAttachmentViewHolder -> holder.bind(dataSet[position] as ColorAttachmentViewModel)
}
}
......@@ -100,6 +110,7 @@ class ChatRoomAdapter(
return when (model) {
is MessageViewModel -> model.messageId.hashCode().toLong()
is BaseFileAttachmentViewModel -> model.id
is AuthorAttachmentViewModel -> model.id
else -> return position.toLong()
}
}
......@@ -111,12 +122,22 @@ class ChatRoomAdapter(
}
fun prependData(dataSet: List<BaseViewModel<*>>) {
val item = dataSet.firstOrNull { newItem ->
val item = dataSet.indexOfFirst { newItem ->
this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1
}
if (item == null) {
if (item == -1) {
this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size)
} else {
dataSet.forEach { item ->
val index = this.dataSet.indexOfFirst {
item.messageId == it.messageId && item.viewType == it.viewType
}
if (index > -1) {
this.dataSet[index] = item
notifyItemChanged(index)
}
}
}
}
......
package chat.rocket.android.chatroom.adapter
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.ColorAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_color_attachment.view.*
class ColorAttachmentViewHolder(itemView: View,
listener: BaseViewHolder.ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ColorAttachmentViewModel>(itemView, listener, reactionListener) {
val drawable: Drawable? = ContextCompat.getDrawable(itemView.context,
R.drawable.quote_vertical_bar)
init {
with(itemView) {
setupActionMenu(attachment_text)
setupActionMenu(color_attachment_container)
attachment_text.movementMethod = LinkMovementMethod()
}
}
override fun bindViews(data: ColorAttachmentViewModel) {
with(itemView) {
drawable?.let {
quote_bar.background = drawable.mutate().apply { setTint(data.color) }
attachment_text.text = data.text
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.graphics.Color
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
......@@ -29,6 +30,9 @@ class MessageViewHolder(
text_sender.text = data.senderName
text_content.text = data.content
image_avatar.setImageURI(data.avatar)
text_content.setTextColor(
if (data.isTemporary) Color.GRAY else Color.BLACK
)
}
}
}
\ No newline at end of file
......@@ -15,9 +15,6 @@ import kotlinx.coroutines.experimental.Job
@PerFragment
class ChatRoomFragmentModule {
@Provides
fun provideChatRoomNavigator(activity: ChatRoomActivity) = ChatRoomNavigator(activity)
@Provides
fun chatRoomView(frag: ChatRoomFragment): ChatRoomView {
return frag
......
package chat.rocket.android.chatroom.di
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module
import dagger.Provides
@Module
@PerActivity
class ChatRoomModule {
@Provides
fun provideChatRoomNavigator(activity: ChatRoomActivity) = ChatRoomNavigator(activity)
}
\ No newline at end of file
package chat.rocket.android.chatroom.di
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.module.AppModule
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class MessageServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideMessageService(): MessageService
}
\ No newline at end of file
......@@ -3,6 +3,7 @@ package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.members.ui.newInstance
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack
class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
......@@ -12,4 +13,9 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
newInstance(chatRoomId, chatRoomType)
}
}
fun toNewServer() {
activity.startActivity(activity.changeServerIntent())
activity.finish()
}
}
\ No newline at end of file
......@@ -7,7 +7,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewMo
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.socket.model.State
interface ChatRoomView : LoadingView, MessageView {
......@@ -103,7 +103,9 @@ interface ChatRoomView : LoadingView, MessageView {
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
fun showConnectionState(state: State)
fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>)
fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>)
/**
* This user has joined the chat callback.
......
package chat.rocket.android.chatroom.service
import android.app.job.JobParameters
import android.app.job.JobService
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.model.Message
import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
class MessageService : JobService() {
@Inject
lateinit var factory: ConnectionManagerFactory
@Inject
lateinit var currentServerRepository: CurrentServerRepository
@Inject
lateinit var messageRepository: MessagesRepository
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onStopJob(params: JobParameters?): Boolean {
return false
}
override fun onStartJob(params: JobParameters?): Boolean {
launch(CommonPool) {
val currentServer = currentServerRepository.get()
if (currentServer != null) {
retrySendingMessages(params, currentServer)
jobFinished(params, false)
}
}
return true
}
private suspend fun retrySendingMessages(params: JobParameters?, currentServer: String) {
val temporaryMessages = messageRepository.getAllUnsent()
.sortedWith(compareBy(Message::timestamp))
if (temporaryMessages.isNotEmpty()) {
val connectionManager = factory.create(currentServer)
val client = connectionManager.client
temporaryMessages.forEach { message ->
try {
client.sendMessage(
message = message.message,
messageId = message.id,
roomId = message.roomId,
avatar = message.avatar,
attachments = message.attachments,
alias = message.senderAlias
)
messageRepository.save(message.copy(isTemporary = false))
Timber.d("Sent scheduled message given by id: ${message.id}")
} catch (ex: RocketChatException) {
Timber.e(ex)
if (ex.message?.contains("E11000", true) == true) {
// XXX: Temporary solution. We need proper error codes from the api.
messageRepository.save(message.copy(isTemporary = false))
}
jobFinished(params, true)
}
}
}
}
companion object {
const val RETRY_SEND_MESSAGE_ID = 1
}
}
\ No newline at end of file
......@@ -8,12 +8,14 @@ import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......@@ -51,6 +53,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
// TODO - workaround for now... We will move to a single activity
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var navigator: ChatRoomNavigator
@Inject lateinit var managerFactory: ConnectionManagerFactory
private lateinit var chatRoomId: String
......@@ -66,7 +69,13 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
setContentView(R.layout.activity_chat_room)
// Workaround for when we are coming to the app via the recents app and the app was killed.
managerFactory.create(serverInteractor.get()!!).connect()
val serverUrl = serverInteractor.get()
if (serverUrl != null) {
managerFactory.create(serverUrl).connect()
} else {
navigator.toNewServer()
return
}
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
......
......@@ -16,6 +16,7 @@ import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_pinned_messages.*
import javax.inject.Inject
......@@ -62,22 +63,30 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
presenter.loadPinnedMessages(chatRoomId)
}
override fun showLoading() = view_loading.setVisible(true)
override fun showLoading() {
ui { view_loading.setVisible(true) }
}
override fun hideLoading() = view_loading.setVisible(false)
override fun hideLoading() {
ui { view_loading.setVisible(false) }
}
override fun showMessage(resId: Int) {
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
activity?.apply {
ui {
if (recycler_view_pinned.adapter == null) {
// TODO - add a better constructor for this case...
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, null, false)
......@@ -92,9 +101,22 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
}
})
}
togglePinView(pinnedMessages.size)
}
adapter.appendData(pinnedMessages)
}
}
private fun togglePinView(size: Int) {
if (size == 0){
iv_pin_icon.setVisible(true)
tv_pin_title.setVisible(true)
tv_pin_description.setVisible(true)
}else{
iv_pin_icon.setVisible(false)
tv_pin_title.setVisible(false)
tv_pin_description.setVisible(false)
}
}
}
\ No newline at end of file
......@@ -13,7 +13,8 @@ data class AudioAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.AuthorAttachment
data class AuthorAttachmentViewModel(
override val attachmentUrl: String,
val id: Long,
val name: CharSequence?,
val icon: String?,
val fields: CharSequence?,
override val message: Message,
override val rawData: AuthorAttachment,
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseAttachmentViewModel<AuthorAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUTHOR_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_author_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
interface BaseAttachmentViewModel<out T> : BaseViewModel<T> {
val attachmentUrl: String
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
interface BaseFileAttachmentViewModel<out T> : BaseViewModel<T> {
val attachmentUrl: String
interface BaseFileAttachmentViewModel<out T> : BaseAttachmentViewModel<T> {
val attachmentTitle: CharSequence
val id: Long
}
\ No newline at end of file
......@@ -12,6 +12,7 @@ interface BaseViewModel<out T> {
var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>?
var preview: Message?
var isTemporary: Boolean
enum class ViewType(val viewType: Int) {
MESSAGE(0),
......@@ -20,7 +21,9 @@ interface BaseViewModel<out T> {
IMAGE_ATTACHMENT(3),
VIDEO_ATTACHMENT(4),
AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6)
MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8)
}
}
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.ColorAttachment
data class ColorAttachmentViewModel(
override val attachmentUrl: String,
val id: Long,
val color: Int,
val text: CharSequence,
override val message: Message,
override val rawData: ColorAttachment,
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseAttachmentViewModel<ColorAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.COLOR_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_color_attachment
}
\ No newline at end of file
......@@ -13,7 +13,8 @@ data class ImageAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
......@@ -14,7 +14,8 @@ data class MessageAttachmentViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
......
......@@ -15,7 +15,8 @@ data class MessageViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
var isFirstUnread: Boolean
var isFirstUnread: Boolean,
override var isTemporary: Boolean = false
) : BaseMessageViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType
......
......@@ -14,7 +14,8 @@ data class UrlPreviewViewModel(
val thumbUrl: String?,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Url> {
override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
......@@ -13,7 +13,8 @@ data class VideoAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
......@@ -4,14 +4,21 @@ import DateTimeHelper
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.support.v4.content.ContextCompat
import android.text.Html
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.text.scale
import chat.rocket.android.R
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.isNotNullNorEmpty
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
......@@ -39,6 +46,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
private val baseUrl = settings.baseUrl()
private val token = tokenRepository.get(currentServer)
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
private val secundaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
suspend fun map(message: Message): List<BaseViewModel<*>> {
return translate(message)
......@@ -68,8 +76,8 @@ class ViewModelMapper @Inject constructor(private val context: Context,
}
mapMessage(message).let {
if (list.size > 0) {
it.preview = list[0].preview
if (list.isNotEmpty()) {
it.preview = list.first().preview
}
list.add(it)
}
......@@ -98,10 +106,52 @@ class ViewModelMapper @Inject constructor(private val context: Context,
return when (attachment) {
is FileAttachment -> mapFileAttachment(message, attachment)
is MessageAttachment -> mapMessageAttachment(message, attachment)
is AuthorAttachment -> mapAuthorAttachment(message, attachment)
is ColorAttachment -> mapColorAttachment(message, attachment)
else -> null
}
}
private suspend fun mapColorAttachment(message: Message, attachment: ColorAttachment): BaseViewModel<*>? {
return with(attachment) {
val content = stripMessageQuotes(message)
val id = attachmentId(message, attachment)
ColorAttachmentViewModel(attachmentUrl = url, id = id, color = color.color,
text = text, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message))
}
}
private suspend fun mapAuthorAttachment(message: Message, attachment: AuthorAttachment): AuthorAttachmentViewModel {
return with(attachment) {
val content = stripMessageQuotes(message)
val fieldsText = fields?.let {
buildSpannedString {
it.forEachIndexed { index, field ->
bold { append(field.title) }
append("\n")
if (field.value.isNotEmpty()) {
append(field.value)
}
if (index != it.size - 1) { // it is not the last one, append a new line
append("\n\n")
}
}
}
}
val id = attachmentId(message, attachment)
AuthorAttachmentViewModel(attachmentUrl = url, id = id, name = authorName,
icon = authorIcon, fields = fieldsText, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message))
}
}
private suspend fun mapMessageAttachment(message: Message, attachment: MessageAttachment): MessageAttachmentViewModel {
val attachmentAuthor = attachment.author
val time = attachment.timestamp?.let { getTime(it) }
......@@ -136,7 +186,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
}
}
private fun attachmentId(message: Message, attachment: FileAttachment): Long {
private fun attachmentId(message: Message, attachment: Attachment): Long {
return "${message.id}_${attachment.url}".hashCode().toLong()
}
......@@ -176,12 +226,13 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val time = getTime(message.timestamp)
val avatar = getUserAvatar(message)
val preview = mapMessagePreview(message)
val isTemp = message.isTemporary ?: false
val content = getContent(stripMessageQuotes(message))
MessageViewModel(message = stripMessageQuotes(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, reactions = getReactions(message),
isFirstUnread = false, preview = preview)
isFirstUnread = false, preview = preview, isTemporary = isTemp)
}
private suspend fun mapMessagePreview(message: Message): Message {
......@@ -213,16 +264,26 @@ class ViewModelMapper @Inject constructor(private val context: Context,
private suspend fun stripMessageQuotes(message: Message): Message {
val baseUrl = settings.baseUrl()
return message.copy(
message = message.message.replace("\\[\\s\\]\\($baseUrl.*\\)".toRegex(), "").trim()
message = message.message.replace("\\[[^\\]]+\\]\\($baseUrl[^)]+\\)".toRegex(), "").trim()
)
}
private fun getSenderName(message: Message): CharSequence {
if (!message.senderAlias.isNullOrEmpty()) {
return message.senderAlias!!
val username = message.sender?.username
message.senderAlias.isNotNullNorEmpty { alias ->
return buildSpannedString {
append(alias)
username?.let {
append(" ")
scale(0.8f) {
color(secundaryTextColor) {
append("@$username")
}
}
}
}
}
val username = message.sender?.username
val realName = message.sender?.name
val senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString()
......@@ -235,7 +296,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val username = message.sender?.username ?: "?"
return baseUrl?.let {
UrlHelper.getAvatarUrl(baseUrl, username)
baseUrl.avatarUrl(username)
}
}
......
......@@ -2,7 +2,7 @@ package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
interface ChatRoomsView : LoadingView, MessageView {
......
......@@ -12,11 +12,11 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.checkIfMyself
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
......@@ -24,7 +24,6 @@ import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.*
......@@ -74,14 +73,17 @@ class ChatRoomsAdapter(private val context: Context,
}
private fun bindAvatar(chatRoom: ChatRoom, drawee: SimpleDraweeView) {
val avatarId = if (chatRoom.type is RoomType.DirectMessage) chatRoom.name else "@${chatRoom.name}"
drawee.setImageURI(UrlHelper.getAvatarUrl(chatRoom.client.url, avatarId))
if (chatRoom.type is RoomType.DirectMessage) {
drawee.setImageURI(chatRoom.client.url.avatarUrl(chatRoom.name))
} else {
drawee.setImageURI(chatRoom.client.url.avatarUrl(chatRoom.name, true))
}
}
private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.textContent = chatRoom.name
var drawable = when (chatRoom.type) {
val drawable = when (chatRoom.type) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, context)
}
......
......@@ -17,24 +17,24 @@ import android.widget.RadioGroup
import chat.rocket.android.R
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants
import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.common.model.RoomType
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.NonCancellable.isActive
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
......@@ -101,8 +101,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_sort -> {
val dialogLayout = layoutInflater.inflate(R.layout.chatroom_sort_dialog, null)
val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY)
......@@ -154,49 +154,57 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) {
activity?.apply {
listJob?.cancel()
listJob = launch(UI) {
val adapter = recycler_view.adapter as SimpleSectionedRecyclerViewAdapter
// FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android.dev/issues/5a90d4718cb3c2fa63b3f557?time=last-seven-days
// TODO - fix this bug to reenable DiffUtil
val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet))
}.await()
if (isActive) {
adapter.baseAdapter.updateRooms(newDataSet)
diff.dispatchUpdatesTo(adapter)
//Set sections always after data set is updated
setSections()
}
listJob?.cancel()
listJob = ui {
val adapter = recycler_view.adapter as SimpleSectionedRecyclerViewAdapter
// FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android/issues/5ac2916c36c7b235275ccccf
// TODO - fix this bug to re-enable DiffUtil
/*val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet))
}.await()*/
if (isActive) {
adapter.baseAdapter.updateRooms(newDataSet)
// TODO - fix crash to re-enable diff.dispatchUpdatesTo(adapter)
adapter.notifyDataSetChanged()
//Set sections always after data set is updated
setSections()
}
}
}
override fun showNoChatRoomsToDisplay() = text_no_data_to_display.setVisible(true)
override fun showNoChatRoomsToDisplay() {
ui { text_no_data_to_display.setVisible(true) }
}
override fun showLoading() = view_loading.setVisible(true)
override fun showLoading(){
ui { view_loading.setVisible(true) }
}
override fun hideLoading() {
if (view_loading != null) {
ui {
view_loading.setVisible(false)
}
}
override fun showMessage(resId: Int) {
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showConnectionState(state: State) {
activity?.apply {
Timber.d("Got new state: $state")
ui {
connection_status_text.fadeIn()
handler.removeCallbacks(dismissStatus)
when (state) {
......@@ -220,22 +228,25 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
private fun setupToolbar() {
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.title_chats)
(activity as AppCompatActivity?)?.supportActionBar?.title = getString(R.string.title_chats)
}
private fun setupRecyclerView() {
activity?.apply {
recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(this,
ui {
recycler_view.layoutManager = LinearLayoutManager(it, LinearLayoutManager.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(it,
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start),
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)))
recycler_view.itemAnimator = DefaultItemAnimator()
// TODO - use a ViewModel Mapper instead of using settings on the adapter
val baseAdapter = ChatRoomsAdapter(this,
settingsRepository.get(serverInteractor.get()!!), localRepository) { chatRoom -> presenter.loadChatRoom(chatRoom) }
val baseAdapter = ChatRoomsAdapter(it,
settingsRepository.get(serverInteractor.get()!!), localRepository) {
chatRoom -> presenter.loadChatRoom(chatRoom)
}
sectionedAdapter = SimpleSectionedRecyclerViewAdapter(this, R.layout.item_chatroom_header, R.id.text_chatroom_header, baseAdapter!!)
sectionedAdapter = SimpleSectionedRecyclerViewAdapter(it,
R.layout.item_chatroom_header, R.id.text_chatroom_header, baseAdapter)
recycler_view.adapter = sectionedAdapter
}
}
......
package chat.rocket.android.core.behaviours
interface InternetView {
fun showNoInternetConnection()
}
\ No newline at end of file
package chat.rocket.android.core.behaviours
import android.support.annotation.StringRes
import chat.rocket.common.util.ifNull
interface MessageView {
......@@ -14,4 +15,12 @@ interface MessageView {
fun showMessage(message: String)
fun showGenericErrorMessage()
}
fun MessageView.showMessage(ex: Exception) {
ex.message?.let {
showMessage(it)
}.ifNull {
showGenericErrorMessage()
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ package chat.rocket.android.dagger
import android.app.Application
import chat.rocket.android.app.RocketChatApplication
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.module.ActivityBuilder
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.dagger.module.ReceiverBuilder
......@@ -29,6 +30,8 @@ interface AppComponent {
fun inject(service: FirebaseTokenService)
fun inject(service: MessageService)
/*@Component.Builder
abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/
}
......@@ -2,11 +2,13 @@ package chat.rocket.android.dagger.module
import chat.rocket.android.authentication.di.AuthenticationModule
import chat.rocket.android.authentication.login.di.LoginFragmentProvider
import chat.rocket.android.authentication.registerusername.di.RegisterUsernameFragmentProvider
import chat.rocket.android.authentication.server.di.ServerFragmentProvider
import chat.rocket.android.authentication.signup.di.SignupFragmentProvider
import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider
import chat.rocket.android.chatroom.di.ChatRoomModule
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.PinnedMessagesActivity
......@@ -30,6 +32,7 @@ abstract class ActivityBuilder {
@ContributesAndroidInjector(modules = [AuthenticationModule::class,
ServerFragmentProvider::class,
LoginFragmentProvider::class,
RegisterUsernameFragmentProvider::class,
SignupFragmentProvider::class,
TwoFAFragmentProvider::class
])
......@@ -43,7 +46,9 @@ abstract class ActivityBuilder {
abstract fun bindMainActivity(): MainActivity
@PerActivity
@ContributesAndroidInjector(modules = [ChatRoomFragmentProvider::class, MembersFragmentProvider::class])
@ContributesAndroidInjector(modules = [ChatRoomModule::class,
ChatRoomFragmentProvider::class,
MembersFragmentProvider::class])
abstract fun bindChatRoomActivity(): ChatRoomActivity
@PerActivity
......
......@@ -2,45 +2,59 @@ package chat.rocket.android.dagger.module
import android.app.Application
import android.app.NotificationManager
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.arch.persistence.room.Room
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import androidx.content.systemService
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.dagger.qualifier.ForMessages
import chat.rocket.android.helper.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.push.GroupedPush
import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.ChatRoomsRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetPermissionsInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.JobSchedulerInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.infraestructure.JobSchedulerInteractorImpl
import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository
import chat.rocket.android.server.infraestructure.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.SharedPreferencesMessagesRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesSettingsRepository
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.AttachmentAdapterFactory
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig
......@@ -80,7 +94,7 @@ class AppModule {
@Provides
@Singleton
fun provideRocketChatDatabase(context: Application): RocketChatDatabase {
return Room.databaseBuilder(context, RocketChatDatabase::class.java, "rocketchat-db").build()
return Room.databaseBuilder(context.applicationContext, RocketChatDatabase::class.java, "rocketchat-db").build()
}
@Provides
......@@ -130,7 +144,7 @@ class AppModule {
@Provides
@ForFresco
@Singleton
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
fun provideFrescoAuthInterceptor(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
return FrescoAuthInterceptor(tokenRepository, currentServerInteractor)
}
......@@ -175,9 +189,14 @@ class AppModule {
}
@Provides
fun provideSharedPreferences(context: Application): SharedPreferences {
return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
}
fun provideSharedPreferences(context: Application) =
context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
@Provides
@ForMessages
fun provideMessagesSharedPreferences(context: Application) =
context.getSharedPreferences("messages", Context.MODE_PRIVATE)
@Provides
@Singleton
......@@ -211,10 +230,16 @@ class AppModule {
@Provides
@Singleton
fun provideMoshi(): Moshi {
fun provideMoshi(logger: PlatformLogger,
currentServerInteractor: GetCurrentServerInteractor):
Moshi {
val url = currentServerInteractor.get() ?: ""
return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE)
.add(AttachmentAdapterFactory(Logger(logger, url)))
.add(java.lang.Long::class.java, ISO8601Date::class.java, TimestampAdapter(CalendarISO8601Converter()))
.add(Long::class.java, ISO8601Date::class.java, TimestampAdapter(CalendarISO8601Converter()))
.build()
}
......@@ -226,8 +251,10 @@ class AppModule {
@Provides
@Singleton
fun provideMessageRepository(): MessagesRepository {
return MemoryMessagesRepository()
fun provideMessageRepository(@ForMessages preferences: SharedPreferences,
moshi: Moshi,
currentServerInteractor: GetCurrentServerInteractor): MessagesRepository {
return SharedPreferencesMessagesRepository(preferences, moshi, currentServerInteractor)
}
@Provides
......@@ -270,9 +297,40 @@ class AppModule {
SharedPreferencesAccountsRepository(preferences, moshi)
@Provides
fun provideNotificationManager(context: Context): NotificationManager = context.systemService()
fun provideNotificationManager(context: Application) =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Provides
@Singleton
fun provideGroupedPush() = GroupedPush()
@Provides
@Singleton
fun providePushManager(
context: Application,
groupedPushes: GroupedPush,
manager: NotificationManager,
moshi: Moshi,
getAccountInteractor: GetAccountInteractor,
getSettingsInteractor: GetSettingsInteractor): PushManager {
return PushManager(groupedPushes, manager, moshi, getAccountInteractor, getSettingsInteractor, context)
}
@Provides
fun provideJobScheduler(context: Application): JobScheduler {
return context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
}
@Provides
fun provideSendMessageJob(context: Application): JobInfo {
return JobInfo.Builder(MessageService.RETRY_SEND_MESSAGE_ID,
ComponentName(context, MessageService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build()
}
@Provides
fun provideJobSchedulerInteractor(jobScheduler: JobScheduler, jobInfo: JobInfo): JobSchedulerInteractor {
return JobSchedulerInteractorImpl(jobScheduler, jobInfo)
}
}
\ No newline at end of file
package chat.rocket.android.dagger.module
import chat.rocket.android.push.DeleteReceiver
import chat.rocket.android.push.DirectReplyReceiver
import chat.rocket.android.push.DirectReplyReceiverProvider
import chat.rocket.android.push.di.DeleteReceiverProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -10,4 +12,7 @@ abstract class ReceiverBuilder {
@ContributesAndroidInjector(modules = [DeleteReceiverProvider::class])
abstract fun bindDeleteReceiver(): DeleteReceiver
@ContributesAndroidInjector(modules = [DirectReplyReceiverProvider::class])
abstract fun bindDirectReplyReceiver(): DirectReplyReceiver
}
\ No newline at end of file
package chat.rocket.android.dagger.module
import chat.rocket.android.chatroom.di.MessageServiceProvider
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.push.FirebaseTokenService
import chat.rocket.android.push.GcmListenerService
import chat.rocket.android.push.di.FirebaseTokenServiceProvider
......@@ -14,4 +16,7 @@ import dagger.android.ContributesAndroidInjector
@ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class])
abstract fun bindGcmListenerService(): GcmListenerService
@ContributesAndroidInjector(modules = [MessageServiceProvider::class])
abstract fun bindMessageService(): MessageService
}
\ No newline at end of file
package chat.rocket.android.dagger.qualifier
import javax.inject.Qualifier
/**
* Created by luciofm on 4/14/18.
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ForMessages
\ No newline at end of file
package chat.rocket.android.helper
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Socket
object NetworkHelper {
/**
* Checks whether there is internet access.
*
* The original author of this code is Levit and you can see his answer here: https://stackoverflow.com/a/27312494/4744263
*
* @return true if there is internet access, false otherwise.
*/
suspend fun hasInternetAccess(): Boolean = withContext(CommonPool) {
try {
val socket = Socket()
val inetSocketAddress = InetSocketAddress("8.8.8.8", 53)
socket.connect(inetSocketAddress, 1500)
socket.close()
true
} catch (e: IOException) {
false
}
}
}
\ No newline at end of file
package chat.rocket.android.helper
import chat.rocket.android.util.extensions.removeTrailingSlash
object OauthHelper {
/**
......@@ -27,7 +29,7 @@ object OauthHelper {
fun getGoogleOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://accounts.google.com/o/oauth2/v2/auth" +
"?client_id=$clientId" +
"&redirect_uri=${UrlHelper.removeTrailingSlash(serverUrl)}/_oauth/google?close" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/google?close" +
"&state=$state" +
"&response_type=code" +
"&scope=email%20profile"
......@@ -44,7 +46,7 @@ object OauthHelper {
fun getLinkedinOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://linkedin.com/oauth/v2/authorization" +
"?client_id=$clientId" +
"&redirect_uri=${UrlHelper.removeTrailingSlash(serverUrl)}/_oauth/linkedin?close" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/linkedin?close" +
"&state=$state" +
"&response_type=code"
}
......@@ -60,9 +62,26 @@ object OauthHelper {
fun getGitlabOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://gitlab.com/oauth/authorize" +
"?client_id=$clientId" +
"&redirect_uri=${UrlHelper.removeTrailingSlash(serverUrl)}/_oauth/gitlab?close" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/gitlab?close" +
"&state=$state" +
"&response_type=code" +
"&scope=read_user"
}
}
\ No newline at end of file
/**
* Returns the Facebook Oauth URL.
*
* @param clientId The Facebook client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Facebook Oauth URL.
*/
fun getFacebookOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://facebook.com/v2.9/dialog/oauth" +
"?client_id=$clientId" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/facebook?close" +
"&state=$state" +
"&response_type=code" +
"&scope=email"
}
}
package chat.rocket.android.helper
import android.util.Patterns
object UrlHelper {
/**
* Returns the avatar URL.
*
* @param serverUrl The server URL.
* @param avatarName The avatar name.
* @return The avatar URL.
*/
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.
*
* @param casLoginUrl The CAS login URL from the server settings.
* @param serverUrl The server URL.
* @param token The token to be send to the CAS server.
* @return The avatar URL.
*/
fun getCasUrl(casLoginUrl: String, serverUrl: String, token: String): String =
removeTrailingSlash(casLoginUrl) + "?service=" + removeTrailingSlash(serverUrl) + "/_cas/" + token
/**
* Returns the server's Terms of Service URL.
*
* @param serverUrl The server URL.
* @return The server's Terms of Service URL.
*/
fun getTermsOfServiceUrl(serverUrl: String) = removeTrailingSlash(serverUrl) + "/terms-of-service"
/**
* Returns the server's Privacy Policy URL.
*
* @param serverUrl The server URL.
* @return The server's Privacy Policy URL.
*/
fun getPrivacyPolicyUrl(serverUrl: String) = removeTrailingSlash(serverUrl) + "/privacy-policy"
/**
* Returns an URL without trailing slash.
*
* @param serverUrl The URL to remove the trailing slash (if exists).
* @return An URL without trailing slash.
*/
fun removeTrailingSlash(serverUrl: String): String {
return if (serverUrl[serverUrl.length - 1] == '/') {
serverUrl.replace("/+$", "")
} else {
serverUrl
}
}
/**
* Checks if the given URL is valid or not.
* @param url The url to check its valid.
* @return True if url is valid, false otherwise.
*/
fun isValidUrl(url: String): Boolean = Patterns.WEB_URL.matcher(url).matches()
}
\ No newline at end of file
......@@ -24,4 +24,5 @@ interface LocalRepository {
}
}
fun LocalRepository.checkIfMyself(username: String) = get(LocalRepository.CURRENT_USERNAME_KEY) == username
\ No newline at end of file
fun LocalRepository.checkIfMyself(username: String) = username() == username
fun LocalRepository.username() = get(LocalRepository.CURRENT_USERNAME_KEY)
\ No newline at end of file
......@@ -6,6 +6,7 @@ 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)
......
......@@ -5,50 +5,64 @@ import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.util.extensions.inflate
import chat.rocket.common.model.UserStatus
private const val VIEW_TYPE_CHANGE_STATUS = 0
private const val VIEW_TYPE_ACCOUNT = 1
private const val VIEW_TYPE_ADD_ACCOUNT = 2
class AccountsAdapter(
private val accounts: List<Account>,
private val selector: AccountSelector
private val selector: Selector
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_CHANGE_STATUS -> StatusViewHolder(parent.inflate(R.layout.item_change_status))
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 getItemCount() = accounts.size + 2
override fun getItemViewType(position: Int) =
if (position == accounts.size) VIEW_TYPE_ADD_ACCOUNT else VIEW_TYPE_ACCOUNT
override fun getItemViewType(position: Int): Int {
return when {
position == 0 -> VIEW_TYPE_CHANGE_STATUS
position <= accounts.size -> VIEW_TYPE_ACCOUNT
else -> VIEW_TYPE_ADD_ACCOUNT
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is StatusViewHolder -> bindStatusViewHolder(holder)
is AccountViewHolder -> bindAccountViewHolder(holder, position)
is AddAccountViewHolder -> bindAddAccountViewHolder(holder, position)
is AddAccountViewHolder -> bindAddAccountViewHolder(holder)
}
}
private fun bindStatusViewHolder(holder: StatusViewHolder) {
holder.bind { userStatus -> selector.onStatusSelected(userStatus) }
}
private fun bindAccountViewHolder(holder: AccountViewHolder, position: Int) {
val account = accounts[position]
val account = accounts[position - 1]
holder.bind(account)
holder.itemView.setOnClickListener {
selector.onAccountSelected(account.serverUrl)
}
}
private fun bindAddAccountViewHolder(holder: AddAccountViewHolder, position: Int) {
private fun bindAddAccountViewHolder(holder: AddAccountViewHolder) {
holder.itemView.setOnClickListener {
selector.onAddedAccountSelected()
}
}
}
interface AccountSelector {
interface Selector {
fun onStatusSelected(userStatus: UserStatus)
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
}
\ 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.common.model.UserStatus
import kotlinx.android.synthetic.main.item_change_status.view.*
class StatusViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(listener: (UserStatus) -> Unit) {
with(itemView) {
text_online.setOnClickListener { listener(UserStatus.Online()) }
text_away.setOnClickListener { listener(UserStatus.Away()) }
text_busy.setOnClickListener { listener(UserStatus.Busy()) }
text_invisible.setOnClickListener { listener(UserStatus.Offline()) }
}
}
}
\ No newline at end of file
......@@ -4,8 +4,24 @@ import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.model.Account
import chat.rocket.common.model.UserStatus
interface MainView : MessageView, VersionCheckView {
fun setupNavHeader(model: NavHeaderViewModel, accounts: List<Account>)
/**
* Shows the current user status.
*
* @see [UserStatus]
*/
fun showUserStatus(userStatus: UserStatus)
/**
* Setups the navigation header.
*
* @param viewModel The [NavHeaderViewModel].
* @param accounts The list of accounts.
*/
fun setupNavHeader(viewModel: NavHeaderViewModel, accounts: List<Account>)
fun closeServerSelection()
}
\ No newline at end of file
package chat.rocket.android.main.viewmodel
import chat.rocket.common.model.UserStatus
data class NavHeaderViewModel(
val username: String,
val server: String,
val avatar: String?,
val userDisplayName: String?,
val userStatus: UserStatus?,
val userAvatar: String?,
val serverUrl: 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.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.core.model.Myself
import javax.inject.Inject
class NavHeaderViewModelMapper @Inject constructor(serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor) {
class NavHeaderViewModelMapper @Inject constructor(
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor
) {
private val currentServer = serverInteractor.get()!!
private var settings: PublicSettings = getSettingsInteractor.get(currentServer)
fun mapToViewModel(me: Myself): NavHeaderViewModel {
val username = mapUsername(me)
val thumb = me.username?.let { UrlHelper.getAvatarUrl(currentServer, it) }
val displayName = mapDisplayName(me)
val status = me.status
val avatar = me.username?.let { currentServer.avatarUrl(it) }
val image = settings.wideTile() ?: settings.faviconLarge()
val logo = image?.let { UrlHelper.getServerLogoUrl(currentServer, it) }
val logo = image?.let { currentServer.serverLogoUrl(it) }
return NavHeaderViewModel(username, currentServer, thumb, logo)
return NavHeaderViewModel(displayName, status, avatar, currentServer, logo)
}
private fun mapUsername(me: Myself): String {
private fun mapDisplayName(me: Myself): String? {
val username = me.username
val realName = me.name
val senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString()
return senderName ?: username
}
}
\ No newline at end of file
package chat.rocket.android.members.presentation
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.member.ui.newInstance
import chat.rocket.android.members.ui.newInstance
class MembersNavigator(internal val activity: ChatRoomActivity) {
......
package chat.rocket.android.member.ui
package chat.rocket.android.members.ui
import android.os.Bundle
import android.support.design.widget.BottomSheetDialogFragment
......
......@@ -2,7 +2,7 @@ package chat.rocket.android.server.domain
import javax.inject.Inject
class RemoveAccountInterector @Inject constructor(val repository: AccountsRepository) {
class RemoveAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun remove(serverUrl: String) {
repository.remove(serverUrl)
}
......
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.
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.
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