Commit 47d0275e authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Merge branch 'develop' into new/login-with-saml and implement the login support throught SAML

parents 52c075e1 23c88ff5
...@@ -58,6 +58,9 @@ jobs: ...@@ -58,6 +58,9 @@ jobs:
- run: - run:
name: Run Unit test name: Run Unit test
command: ./gradlew test command: ./gradlew test
- run:
name: Compile Instrumentation test
command: ./gradlew assembleAndroidTest
- store_artifacts: - store_artifacts:
path: app/build/reports/ path: app/build/reports/
destination: reports destination: reports
......
...@@ -13,8 +13,8 @@ android { ...@@ -13,8 +13,8 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 2015 versionCode 2022
versionName "2.1.0" versionName "2.3.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
} }
...@@ -74,6 +74,7 @@ dependencies { ...@@ -74,6 +74,7 @@ dependencies {
kapt libraries.daggerAndroidApt kapt libraries.daggerAndroidApt
implementation libraries.playServicesGcm implementation libraries.playServicesGcm
implementation libraries.playServicesAuth
implementation libraries.room implementation libraries.room
kapt libraries.roomProcessor kapt libraries.roomProcessor
...@@ -104,7 +105,6 @@ dependencies { ...@@ -104,7 +105,6 @@ dependencies {
implementation libraries.frescoImageViewer implementation libraries.frescoImageViewer
implementation libraries.markwon implementation libraries.markwon
implementation libraries.markwonImageLoader
implementation libraries.sheetMenu implementation libraries.sheetMenu
...@@ -115,9 +115,9 @@ dependencies { ...@@ -115,9 +115,9 @@ dependencies {
} }
testImplementation libraries.junit testImplementation libraries.junit
androidTestImplementation(libraries.expressoCore, { testImplementation libraries.truth
exclude group: 'com.android.support', module: 'support-annotations' androidTestImplementation libraries.espressoCore
}) androidTestImplementation libraries.espressoIntents
} }
kotlin { kotlin {
......
package chat.rocket.android.chatroom.ui
import android.content.Intent
import android.support.test.espresso.intent.rule.IntentsTestRule
import android.support.test.filters.LargeTest
import org.junit.Rule
import org.junit.Test
import android.app.Activity
import android.app.Instrumentation.ActivityResult
import android.support.test.InstrumentationRegistry
import android.support.test.espresso.intent.Intents.intended
import android.support.test.espresso.intent.Intents.intending
import android.support.test.espresso.intent.matcher.IntentMatchers.*
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.not
import org.junit.Before
@LargeTest
class ChatRoomFragmentTest {
@JvmField
@Rule
val activityRule = IntentsTestRule<ChatRoomActivity>(ChatRoomActivity::class.java, false, false)
@Before
fun stubAllExternalIntents() {
val activityIntent = InstrumentationRegistry.getTargetContext().chatRoomIntent("id", "name", "type", false, 0L)
activityRule.launchActivity(activityIntent)
intending(not(isInternal())).respondWith(ActivityResult(Activity.RESULT_OK, null))
}
@Test
fun showFileSelection_nonNullFiltersAreApplied() {
val fragment = activityRule.activity.supportFragmentManager.findFragmentByTag(ChatRoomActivity.TAG_CHAT_ROOM_FRAGMENT) as ChatRoomFragment
val filters = arrayOf("image/*")
fragment.showFileSelection(filters)
intended(allOf(
hasAction(Intent.ACTION_GET_CONTENT),
hasType("*/*"),
hasCategories(setOf(Intent.CATEGORY_OPENABLE)),
hasExtra(Intent.EXTRA_MIME_TYPES, filters)))
}
@Test
fun showFileSelection_nullFiltersAreNotApplied() {
val fragment = activityRule.activity.supportFragmentManager.findFragmentByTag(ChatRoomActivity.TAG_CHAT_ROOM_FRAGMENT) as ChatRoomFragment
fragment.showFileSelection(null)
intended(allOf(
hasAction(Intent.ACTION_GET_CONTENT),
hasType("*/*"),
hasCategories(setOf(Intent.CATEGORY_OPENABLE)),
not(hasExtraWithKey(Intent.EXTRA_MIME_TYPES))))
}
}
\ No newline at end of file
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" /> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<permission <permission
android:name="${applicationId}.permission.C2D_MESSAGE" android:name="${applicationId}.permission.C2D_MESSAGE"
...@@ -19,8 +20,8 @@ ...@@ -19,8 +20,8 @@
android:fullBackupContent="@xml/backup_config" android:fullBackupContent="@xml/backup_config"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"> android:supportsRtl="true">
<activity <activity
...@@ -29,17 +30,18 @@ ...@@ -29,17 +30,18 @@
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/AuthenticationTheme" android:theme="@style/AuthenticationTheme"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data <data
android:host="auth" android:host="auth"
android:scheme="rocketchat" /> android:scheme="rocketchat" />
...@@ -65,7 +67,7 @@ ...@@ -65,7 +67,7 @@
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" /> android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity <activity
android:name=".webview.cas.ui.CasWebViewActivity" android:name=".webview.sso.ui.SsoWebViewActivity"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" /> android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
...@@ -79,18 +81,12 @@ ...@@ -79,18 +81,12 @@
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" /> android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<!-- TODO: Change to fragment--> <!-- TODO: Change to fragment -->
<activity
android:name=".chatroom.ui.PinnedMessagesActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<!-- TODO: Change to fragment-->
<activity <activity
android:name=".settings.password.ui.PasswordActivity" android:name=".settings.password.ui.PasswordActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<!-- TODO: Change to fragment--> <!-- TODO: Change to fragment -->
<activity <activity
android:name=".settings.about.ui.AboutActivity" android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
...@@ -123,6 +119,7 @@ ...@@ -123,6 +119,7 @@
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" /> <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name=".push.GcmListenerService" android:name=".push.GcmListenerService"
android:exported="false"> android:exported="false">
...@@ -130,6 +127,7 @@ ...@@ -130,6 +127,7 @@
<action android:name="com.google.android.c2dm.intent.RECEIVE" /> <action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name=".chatroom.service.MessageService" android:name=".chatroom.service.MessageService"
android:exported="true" android:exported="true"
...@@ -140,4 +138,4 @@ ...@@ -140,4 +138,4 @@
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" /> android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
</application> </application>
</manifest> </manifest>
\ No newline at end of file
...@@ -12,7 +12,18 @@ object DateTimeHelper { ...@@ -12,7 +12,18 @@ object DateTimeHelper {
private val lastWeek = today.minusWeeks(1) private val lastWeek = today.minusWeeks(1)
/** /**
* Returns a date from a [LocalDateTime] or the textual representation if the [LocalDateTime] has a max period of a week from the current date. * Returns a [LocalDateTime] from a [Long].
*
* @param long The [Long]
* @return The [LocalDateTime] from a [Long].
*/
fun getLocalDateTime(long: Long): LocalDateTime {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(long), ZoneId.systemDefault())
}
/**
* Returns a date from a [LocalDateTime] or the textual representation if the [LocalDateTime]
* has a max period of a week from the current date.
* *
* @param localDateTime The [LocalDateTime]. * @param localDateTime The [LocalDateTime].
* @param context The context. * @param context The context.
...@@ -45,13 +56,18 @@ object DateTimeHelper { ...@@ -45,13 +56,18 @@ object DateTimeHelper {
} }
/** /**
* Returns a [LocalDateTime] from a [Long]. * Returns a date time from a [LocalDateTime].
* *
* @param long The [Long] * @param localDateTime The [LocalDateTime].
* @return The [LocalDateTime] from a [Long]. * @return The time from a [LocalDateTime].
*/ */
fun getLocalDateTime(long: Long): LocalDateTime { fun getDateTime(localDateTime: LocalDateTime): String {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(long), ZoneId.systemDefault()) return formatLocalDateTime(localDateTime)
}
private fun formatLocalDateTime(localDateTime: LocalDateTime): String {
val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
return localDateTime.format(formatter).toString()
} }
private fun formatLocalDate(localDate: LocalDate): String { private fun formatLocalDate(localDate: LocalDate): String {
......
...@@ -18,6 +18,7 @@ import chat.rocket.android.app.migration.model.RealmSession ...@@ -18,6 +18,7 @@ import chat.rocket.android.app.migration.model.RealmSession
import chat.rocket.android.app.migration.model.RealmUser import chat.rocket.android.app.migration.model.RealmUser
import chat.rocket.android.authentication.domain.model.toToken import chat.rocket.android.authentication.domain.model.toToken
import chat.rocket.android.dagger.DaggerAppComponent import chat.rocket.android.dagger.DaggerAppComponent
import chat.rocket.android.dagger.qualifier.ForMessages
import chat.rocket.android.helper.CrashlyticsTree import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
...@@ -84,6 +85,10 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -84,6 +85,10 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
@Inject @Inject
lateinit var localRepository: LocalRepository lateinit var localRepository: LocalRepository
@Inject
@field:ForMessages
lateinit var messagesPrefs: SharedPreferences
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
...@@ -107,6 +112,13 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -107,6 +112,13 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
setupFresco() setupFresco()
setupTimber() setupTimber()
if (localRepository.needOldMessagesCleanUp()) {
messagesPrefs.edit {
clear()
}
localRepository.setOldMessagesCleanedUp()
}
// TODO - remove this and all realm stuff when we got to 80% in 2.0 // TODO - remove this and all realm stuff when we got to 80% in 2.0
try { try {
if (!localRepository.hasMigrated()) { if (!localRepository.hasMigrated()) {
...@@ -286,5 +298,9 @@ private fun LocalRepository.setMigrated(migrated: Boolean) { ...@@ -286,5 +298,9 @@ private fun LocalRepository.setMigrated(migrated: Boolean) {
} }
private fun LocalRepository.hasMigrated() = getBoolean(LocalRepository.MIGRATION_FINISHED_KEY) private fun LocalRepository.hasMigrated() = getBoolean(LocalRepository.MIGRATION_FINISHED_KEY)
private fun LocalRepository.needOldMessagesCleanUp() = getBoolean(CLEANUP_OLD_MESSAGES_NEEDED, true)
private fun LocalRepository.setOldMessagesCleanedUp() = save(CLEANUP_OLD_MESSAGES_NEEDED, false)
private const val INTERNAL_TOKEN_MIGRATION_NEEDED = "INTERNAL_TOKEN_MIGRATION_NEEDED"
private const val INTERNAL_TOKEN_MIGRATION_NEEDED = "INTERNAL_TOKEN_MIGRATION_NEEDED" private const val CLEANUP_OLD_MESSAGES_NEEDED = "CLEANUP_OLD_MESSAGES_NEEDED"
\ No newline at end of file \ No newline at end of file
...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.login.presentation ...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import com.google.android.gms.auth.api.credentials.Credential
interface LoginView : LoadingView, MessageView { interface LoginView : LoadingView, MessageView {
...@@ -65,6 +66,18 @@ interface LoginView : LoadingView, MessageView { ...@@ -65,6 +66,18 @@ interface LoginView : LoadingView, MessageView {
*/ */
fun setupSignUpView() fun setupSignUpView()
/**
* Shows the forgot password view if enabled by the server settings.
*
* REMARK: We must set up the forgot password view listener [setupForgotPasswordView].
*/
fun showForgotPasswordView()
/**
* Setups the forgot password view when tapped.
*/
fun setupForgotPasswordView()
/** /**
* Hides the sign up view. * Hides the sign up view.
*/ */
...@@ -184,7 +197,7 @@ interface LoginView : LoadingView, MessageView { ...@@ -184,7 +197,7 @@ interface LoginView : LoadingView, MessageView {
* @state A random string generated by the app, which you'll verify later (to protect against forgery attacks). * @state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
* @serviceName The custom OAuth service name. * @serviceName The custom OAuth service name.
* @serviceNameColor The custom OAuth service name color (just stylizing). * @serviceNameColor The custom OAuth service name color (just stylizing).
* @buttonColor The color of the custom OAuth button (just stylizing). * @buttonColor The custom OAuth button color (just stylizing).
* @see [enableOauthView] * @see [enableOauthView]
*/ */
fun addCustomOauthServiceButton( fun addCustomOauthServiceButton(
...@@ -198,12 +211,19 @@ interface LoginView : LoadingView, MessageView { ...@@ -198,12 +211,19 @@ interface LoginView : LoadingView, MessageView {
/** /**
* Adds a SAML button in the oauth view. * Adds a SAML button in the oauth view.
* *
* @serviceName The custom OAuth service name. * @samlUrl The SAML url to sets up the button (the listener).
* @serviceNameColor The custom OAuth service name color (just stylizing). * @serviceName The SAML service name.
* @buttonColor The color of the custom OAuth button (just stylizing). * @serviceNameColor The SAML service name color (just stylizing).
* @buttonColor The SAML button color (just stylizing).
* @see [enableOauthView] * @see [enableOauthView]
*/ */
fun addSamlServiceButton(serviceName: String, serviceNameColor: Int, buttonColor: Int) fun addSamlServiceButton(
samlUrl: String,
samlToken: String,
serviceName: String,
serviceNameColor: Int,
buttonColor: Int
)
/** /**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)). * Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
...@@ -221,4 +241,9 @@ interface LoginView : LoadingView, MessageView { ...@@ -221,4 +241,9 @@ interface LoginView : LoadingView, MessageView {
* Alerts the user about a wrong inputted password. * Alerts the user about a wrong inputted password.
*/ */
fun alertWrongPassword() fun alertWrongPassword()
/**
* Save credentials via google smart lock
*/
fun saveSmartLockCredentials(loginCredential: Credential?)
} }
\ No newline at end of file
...@@ -5,6 +5,7 @@ import chat.rocket.android.R ...@@ -5,6 +5,7 @@ import chat.rocket.android.R
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.login.ui.LoginFragment import chat.rocket.android.authentication.login.ui.LoginFragment
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.authentication.resetpassword.ui.ResetPasswordFragment
import chat.rocket.android.authentication.signup.ui.SignupFragment import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
...@@ -12,6 +13,7 @@ import chat.rocket.android.authentication.ui.newServerIntent ...@@ -12,6 +13,7 @@ import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.ui.changeServerIntent import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack import chat.rocket.android.util.extensions.addFragmentBackStack
import chat.rocket.android.util.extensions.toPreviousView
import chat.rocket.android.webview.ui.webViewIntent import chat.rocket.android.webview.ui.webViewIntent
class AuthenticationNavigator(internal val activity: AuthenticationActivity) { class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
...@@ -28,6 +30,10 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) { ...@@ -28,6 +30,10 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
} }
} }
fun toPreviousView() {
activity.toPreviousView()
}
fun toTwoFA(username: String, password: String) { fun toTwoFA(username: String, password: String) {
activity.addFragmentBackStack("TwoFAFragment", R.id.fragment_container) { activity.addFragmentBackStack("TwoFAFragment", R.id.fragment_container) {
TwoFAFragment.newInstance(username, password) TwoFAFragment.newInstance(username, password)
...@@ -40,6 +46,12 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) { ...@@ -40,6 +46,12 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
} }
} }
fun toForgotPassword() {
activity.addFragmentBackStack("ResetPasswordFragment", R.id.fragment_container) {
ResetPasswordFragment.newInstance()
}
}
fun toWebPage(url: String) { fun toWebPage(url: String) {
activity.startActivity(activity.webViewIntent(url)) activity.startActivity(activity.webViewIntent(url))
activity.overridePendingTransition(R.anim.slide_up, R.anim.hold) activity.overridePendingTransition(R.anim.slide_up, R.anim.hold)
......
...@@ -16,7 +16,8 @@ import kotlinx.android.synthetic.main.fragment_authentication_register_username. ...@@ -16,7 +16,8 @@ import kotlinx.android.synthetic.main.fragment_authentication_register_username.
import javax.inject.Inject import javax.inject.Inject
class RegisterUsernameFragment : Fragment(), RegisterUsernameView { class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
@Inject lateinit var presenter: RegisterUsernamePresenter @Inject
lateinit var presenter: RegisterUsernamePresenter
private lateinit var userId: String private lateinit var userId: String
private lateinit var authToken: String private lateinit var authToken: String
...@@ -41,7 +42,11 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView { ...@@ -41,7 +42,11 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
authToken = arguments?.getString(AUTH_TOKEN) ?: "" 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 onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_authentication_register_username)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
......
package chat.rocket.android.authentication.resetpassword.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordView
import chat.rocket.android.authentication.resetpassword.ui.ResetPasswordFragment
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 ResetPasswordFragmentModule {
@Provides
fun resetPasswordView(frag: ResetPasswordFragment): ResetPasswordView {
return frag
}
@Provides
fun provideLifecycleOwner(frag: ResetPasswordFragment): 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.resetpassword.di
import chat.rocket.android.authentication.resetpassword.ui.ResetPasswordFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class ResetPasswordFragmentProvider {
@ContributesAndroidInjector(modules = [ResetPasswordFragmentModule::class])
abstract fun provideResetPasswordFragment(): ResetPasswordFragment
}
\ No newline at end of file
package chat.rocket.android.authentication.resetpassword.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.isEmail
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatInvalidResponseException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.forgotPassword
import javax.inject.Inject
class ResetPasswordPresenter @Inject constructor(
private val view: ResetPasswordView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
factory: RocketChatClientFactory,
serverInteractor: GetCurrentServerInteractor
) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
fun resetPassword(email: String) {
when {
email.isBlank() -> view.alertBlankEmail()
!email.isEmail() -> view.alertInvalidEmail()
else -> launchUI(strategy) {
view.showLoading()
try {
retryIO("forgotPassword(email = $email)") {
client.forgotPassword(email)
}
navigator.toPreviousView()
view.emailSent()
} catch (exception: RocketChatException) {
if (exception is RocketChatInvalidResponseException) {
view.updateYourServerVersion()
} else {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
} finally {
view.hideLoading()
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.authentication.resetpassword.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface ResetPasswordView : LoadingView, MessageView {
/**
* Alerts the user about a blank email.
*/
fun alertBlankEmail()
/**
* Alerts the user about a invalid email.
*/
fun alertInvalidEmail()
/**
* Shows a successful email sent message.
*/
fun emailSent()
/**
* Shows a message to update the server version in order to use an app feature.
*/
fun updateYourServerVersion()
}
\ No newline at end of file
package chat.rocket.android.authentication.resetpassword.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 android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordPresenter
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordView
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_reset_password.*
import javax.inject.Inject
class ResetPasswordFragment : Fragment(), ResetPasswordView {
@Inject
lateinit var presenter: ResetPasswordPresenter
companion object {
fun newInstance() = ResetPasswordFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_authentication_reset_password)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.apply {
text_email.requestFocus()
showKeyboard(text_email)
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart()
}
setupOnClickListener()
}
override fun alertBlankEmail() {
ui {
vibrateShakeAndRequestFocusForTextEmail()
}
}
override fun alertInvalidEmail() {
ui {
vibrateShakeAndRequestFocusForTextEmail()
showMessage(R.string.msg_invalid_email)
}
}
override fun emailSent() {
showToast(R.string.msg_check_your_email_to_reset_your_password, Toast.LENGTH_LONG)
}
override fun updateYourServerVersion() {
showMessage(R.string.msg_update_app_version_in_order_to_continue)
}
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 emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, it)
DrawableHelper.wrapDrawable(emailDrawable)
DrawableHelper.tintDrawable(emailDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_email, emailDrawable)
}
}
private fun enableUserInput() {
button_reset_password.isEnabled = true
text_email.isEnabled = true
}
private fun disableUserInput() {
button_reset_password.isEnabled = false
text_email.isEnabled = true
}
private fun vibrateShakeAndRequestFocusForTextEmail() {
vibrateSmartPhone()
text_email.shake()
text_email.requestFocus()
}
private fun setupOnClickListener() {
button_reset_password.setOnClickListener {
presenter.resetPassword(text_email.textContent)
}
}
}
\ No newline at end of file
...@@ -32,6 +32,7 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -32,6 +32,7 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
} }
fun connect(server: String) { fun connect(server: String) {
//code that leads to login screen (smart lock will be implemented after this)
connectToServer(server) { connectToServer(server) {
navigator.toLogin() navigator.toLogin()
} }
...@@ -64,6 +65,7 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -64,6 +65,7 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
} }
fun deepLink(deepLinkInfo: LoginDeepLinkInfo) { fun deepLink(deepLinkInfo: LoginDeepLinkInfo) {
//code that leads to login screen (smart lock will be implemented after this)
connectToServer(deepLinkInfo.url) { connectToServer(deepLinkInfo.url) {
navigator.toLogin(deepLinkInfo) navigator.toLogin(deepLinkInfo)
} }
......
package chat.rocket.android.authentication.server.presentation package chat.rocket.android.authentication.server.presentation
import okhttp3.HttpUrl
interface VersionCheckView { interface VersionCheckView {
/** /**
* Alerts the user about the server version not meeting the recommended server version. * Alerts the user about the server version not meeting the recommended server version.
...@@ -26,4 +28,9 @@ interface VersionCheckView { ...@@ -26,4 +28,9 @@ interface VersionCheckView {
* Alters the user this protocol is invalid. This is optional. * Alters the user this protocol is invalid. This is optional.
*/ */
fun errorInvalidProtocol() {} fun errorInvalidProtocol() {}
/**
* Updates the server URL after a URL redirection
*/
fun updateServerUrl(url: HttpUrl) {}
} }
\ No newline at end of file
...@@ -20,6 +20,7 @@ import chat.rocket.android.util.extensions.* ...@@ -20,6 +20,7 @@ import chat.rocket.android.util.extensions.*
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_server.* import kotlinx.android.synthetic.main.fragment_authentication_server.*
import okhttp3.HttpUrl
import javax.inject.Inject import javax.inject.Inject
class ServerFragment : Fragment(), ServerView { class ServerFragment : Fragment(), ServerView {
...@@ -41,6 +42,7 @@ class ServerFragment : Fragment(), ServerView { ...@@ -41,6 +42,7 @@ class ServerFragment : Fragment(), ServerView {
} }
private var protocol = "https://" private var protocol = "https://"
private var ignoreChange = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -72,22 +74,27 @@ class ServerFragment : Fragment(), ServerView { ...@@ -72,22 +74,27 @@ class ServerFragment : Fragment(), ServerView {
protocol = "https://" protocol = "https://"
} }
1 -> { 1 -> {
ui{ if (ignoreChange) {
AlertDialog.Builder(it) protocol = "http://"
.setTitle(R.string.msg_warning) } else {
.setMessage(R.string.msg_http_insecure) ui {
.setPositiveButton(R.string.msg_proceed) { _, _ -> AlertDialog.Builder(it)
protocol = "http://" .setTitle(R.string.msg_warning)
} .setMessage(R.string.msg_http_insecure)
.setNegativeButton(R.string.msg_cancel) { _, _ -> .setPositiveButton(R.string.msg_proceed) { _, _ ->
text_server_protocol.setSelection(0) protocol = "http://"
} }
.setCancelable(false) .setNegativeButton(R.string.msg_cancel) { _, _ ->
.create() text_server_protocol.setSelection(0)
.show() }
.setCancelable(false)
.create()
.show()
}
} }
} }
} }
ignoreChange = false
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
...@@ -174,13 +181,23 @@ class ServerFragment : Fragment(), ServerView { ...@@ -174,13 +181,23 @@ class ServerFragment : Fragment(), ServerView {
showMessage(R.string.msg_invalid_server_protocol) showMessage(R.string.msg_invalid_server_protocol)
} }
override fun updateServerUrl(url: HttpUrl) {
if (activity != null && view != null) {
if (url.scheme() == "https") text_server_protocol.setSelection(0) else text_server_protocol.setSelection(1)
protocol = "${url.scheme()}://"
val serverUrl = url.toString().removePrefix("${url.scheme()}://")
text_server_url.textContent = serverUrl
}
}
private fun performConnect() { private fun performConnect() {
ui { ui {
deepLinkInfo?.let { deepLinkInfo?.let {
presenter.deepLink(it) presenter.deepLink(it)
}.ifNull { }.ifNull {
val url = text_server_url.textContent.ifEmpty(text_server_url.hintContent) val url = text_server_url.textContent.ifEmpty(text_server_url.hintContent)
presenter.connect("${protocol}${url.sanitize()}") presenter.connect("$protocol${url.sanitize()}")
} }
} }
} }
......
...@@ -6,12 +6,7 @@ import chat.rocket.android.infrastructure.LocalRepository ...@@ -6,12 +6,7 @@ import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.*
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.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
...@@ -20,17 +15,20 @@ import chat.rocket.core.internal.rest.login ...@@ -20,17 +15,20 @@ import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.signup import chat.rocket.core.internal.rest.signup
import chat.rocket.core.model.Myself import chat.rocket.core.model.Myself
import com.google.android.gms.auth.api.credentials.Credential
import javax.inject.Inject import javax.inject.Inject
class SignupPresenter @Inject constructor(private val view: SignupView, class SignupPresenter @Inject constructor(
private val strategy: CancelStrategy, private val view: SignupView,
private val navigator: AuthenticationNavigator, private val strategy: CancelStrategy,
private val localRepository: LocalRepository, private val navigator: AuthenticationNavigator,
private val serverInteractor: GetCurrentServerInteractor, private val localRepository: LocalRepository,
private val factory: RocketChatClientFactory, private val serverInteractor: GetCurrentServerInteractor,
private val saveAccountInteractor: SaveAccountInteractor, private val factory: RocketChatClientFactory,
private val getAccountsInteractor: GetAccountsInteractor, private val saveAccountInteractor: SaveAccountInteractor,
settingsInteractor: GetSettingsInteractor) { private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor
) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!) private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
...@@ -66,6 +64,10 @@ class SignupPresenter @Inject constructor(private val view: SignupView, ...@@ -66,6 +64,10 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username) localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me) saveAccount(me)
registerPushToken() registerPushToken()
val loginCredentials = Credential.Builder(email)
.setPassword(password)
.build()
view.saveSmartLockCredentials(loginCredentials)
navigator.toChatList() navigator.toChatList()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
exception.message?.let { exception.message?.let {
......
...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.signup.presentation ...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.signup.presentation
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import com.google.android.gms.auth.api.credentials.Credential
interface SignupView : LoadingView, MessageView { interface SignupView : LoadingView, MessageView {
...@@ -24,4 +25,9 @@ interface SignupView : LoadingView, MessageView { ...@@ -24,4 +25,9 @@ interface SignupView : LoadingView, MessageView {
* Alerts the user about a blank email. * Alerts the user about a blank email.
*/ */
fun alertBlankEmail() fun alertBlankEmail()
/**
* Save credentials via google smart lock
*/
fun saveSmartLockCredentials(loginCredential: Credential)
} }
\ No newline at end of file
package chat.rocket.android.authentication.signup.ui package chat.rocket.android.authentication.signup.ui
import DrawableHelper import DrawableHelper
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.view.* import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Toast import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.login.ui.googleApiClient
import chat.rocket.android.authentication.signup.presentation.SignupPresenter import chat.rocket.android.authentication.signup.presentation.SignupPresenter
import chat.rocket.android.authentication.signup.presentation.SignupView import chat.rocket.android.authentication.signup.presentation.SignupView
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import com.google.android.gms.auth.api.Auth
import com.google.android.gms.auth.api.credentials.Credential
import com.google.android.gms.common.api.ResolvingResultCallbacks
import com.google.android.gms.common.api.Status
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.* import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal const val SAVE_CREDENTIALS = 1
class SignupFragment : Fragment(), SignupView { class SignupFragment : Fragment(), SignupView {
@Inject lateinit var presenter: SignupPresenter
@Inject
lateinit var presenter: SignupPresenter
private lateinit var credentialsToBeSaved: Credential
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)) { if (KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)) {
bottom_container.setVisible(false) bottom_container.setVisible(false)
...@@ -40,7 +56,11 @@ class SignupFragment : Fragment(), SignupView { ...@@ -40,7 +56,11 @@ class SignupFragment : Fragment(), SignupView {
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_authentication_sign_up, container, false) override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_authentication_sign_up, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
...@@ -54,7 +74,12 @@ class SignupFragment : Fragment(), SignupView { ...@@ -54,7 +74,12 @@ class SignupFragment : Fragment(), SignupView {
setUpNewUserAgreementListener() setUpNewUserAgreementListener()
button_sign_up.setOnClickListener { button_sign_up.setOnClickListener {
presenter.signup(text_username.textContent, text_username.textContent, text_password.textContent, text_email.textContent) presenter.signup(
text_username.textContent,
text_username.textContent,
text_password.textContent,
text_email.textContent
)
} }
} }
...@@ -95,6 +120,44 @@ class SignupFragment : Fragment(), SignupView { ...@@ -95,6 +120,44 @@ class SignupFragment : Fragment(), SignupView {
} }
} }
override fun saveSmartLockCredentials(loginCredential: Credential) {
credentialsToBeSaved = loginCredential
googleApiClient.let {
if (it.isConnected) {
saveCredentials()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SAVE_CREDENTIALS) {
if (resultCode == RESULT_OK) {
Toast.makeText(
context,
getString(R.string.message_credentials_saved_successfully),
Toast.LENGTH_SHORT
).show()
} else {
Timber.e("ERROR: Cancelled by user")
}
}
}
private fun saveCredentials() {
activity?.let {
Auth.CredentialsApi.save(googleApiClient, credentialsToBeSaved).setResultCallback(
object : ResolvingResultCallbacks<Status>(it, SAVE_CREDENTIALS) {
override fun onSuccess(status: Status) {
Timber.d("save:SUCCESS:$status")
}
override fun onUnresolvableFailure(status: Status) {
Timber.e("save:FAILURE:$status")
}
})
}
}
override fun showLoading() { override fun showLoading() {
ui { ui {
enableUserInput(false) enableUserInput(false)
...@@ -127,7 +190,8 @@ class SignupFragment : Fragment(), SignupView { ...@@ -127,7 +190,8 @@ class SignupFragment : Fragment(), SignupView {
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
ui { ui {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, it) val personDrawable =
DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, it)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_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 lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, it)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, it) val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, it)
...@@ -135,14 +199,22 @@ class SignupFragment : Fragment(), SignupView { ...@@ -135,14 +199,22 @@ class SignupFragment : Fragment(), SignupView {
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable) val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.wrapDrawables(drawables) DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, it, R.color.colorDrawableTintGrey) DrawableHelper.tintDrawables(drawables, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_password, text_email), drawables) DrawableHelper.compoundDrawables(
arrayOf(
text_name,
text_username,
text_password,
text_email
), drawables
)
} }
} }
private fun setUpNewUserAgreementListener() { private fun setUpNewUserAgreementListener() {
val termsOfService = getString(R.string.action_terms_of_service) val termsOfService = getString(R.string.action_terms_of_service)
val privacyPolicy = getString(R.string.action_privacy_policy) val privacyPolicy = getString(R.string.action_privacy_policy)
val newUserAgreement = String.format(getString(R.string.msg_new_user_agreement), termsOfService, privacyPolicy) val newUserAgreement =
String.format(getString(R.string.msg_new_user_agreement), termsOfService, privacyPolicy)
text_new_user_agreement.text = newUserAgreement text_new_user_agreement.text = newUserAgreement
...@@ -158,7 +230,11 @@ class SignupFragment : Fragment(), SignupView { ...@@ -158,7 +230,11 @@ class SignupFragment : Fragment(), SignupView {
} }
} }
TextHelper.addLink(text_new_user_agreement, arrayOf(termsOfService, privacyPolicy), arrayOf(termsOfServiceListener, privacyPolicyListener)) TextHelper.addLink(
text_new_user_agreement,
arrayOf(termsOfService, privacyPolicy),
arrayOf(termsOfServiceListener, privacyPolicyListener)
)
} }
private fun enableUserInput(value: Boolean) { private fun enableUserInput(value: Boolean) {
......
...@@ -21,8 +21,10 @@ import kotlinx.coroutines.experimental.launch ...@@ -21,8 +21,10 @@ import kotlinx.coroutines.experimental.launch
import javax.inject.Inject import javax.inject.Inject
class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector { class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject
@Inject lateinit var presenter: AuthenticationPresenter lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var presenter: AuthenticationPresenter
val job = Job() val job = Job()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
...@@ -43,6 +45,14 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -43,6 +45,14 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
if (currentFragment != null) {
currentFragment.onActivityResult(requestCode, resultCode, data)
}
}
override fun onDestroy() { override fun onDestroy() {
job.cancel() job.cancel()
super.onDestroy() super.onDestroy()
......
...@@ -14,10 +14,9 @@ class AudioAttachmentViewHolder(itemView: View, ...@@ -14,10 +14,9 @@ class AudioAttachmentViewHolder(itemView: View,
init { init {
with(itemView) { with(itemView) {
setupActionMenu(attachment_container)
image_attachment.setVisible(false) image_attachment.setVisible(false)
audio_video_attachment.setVisible(true) audio_video_attachment.setVisible(true)
setupActionMenu(attachment_container)
setupActionMenu(audio_video_attachment)
} }
} }
......
...@@ -19,12 +19,9 @@ class AuthorAttachmentViewHolder(itemView: View, ...@@ -19,12 +19,9 @@ class AuthorAttachmentViewHolder(itemView: View,
init { init {
with(itemView) { with(itemView) {
setupActionMenu(author_attachment_container) setupActionMenu(author_attachment_container)
setupActionMenu(text_fields)
setupActionMenu(text_author_name)
} }
} }
override fun bindViews(data: AuthorAttachmentViewModel) { override fun bindViews(data: AuthorAttachmentViewModel) {
with(itemView) { with(itemView) {
data.icon?.let { icon -> data.icon?.let { icon ->
......
...@@ -3,6 +3,8 @@ package chat.rocket.android.chatroom.adapter ...@@ -3,6 +3,8 @@ package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
...@@ -16,13 +18,12 @@ import com.google.android.flexbox.FlexboxLayoutManager ...@@ -16,13 +18,12 @@ import com.google.android.flexbox.FlexboxLayoutManager
import ru.whalemare.sheetmenu.extension.inflate import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList import ru.whalemare.sheetmenu.extension.toList
abstract class BaseViewHolder<T : BaseViewModel<*>>( abstract class BaseViewHolder<T : BaseViewModel<*>>(
itemView: View, itemView: View,
private val listener: ActionsListener, private val listener: ActionsListener,
var reactionListener: EmojiReactionListener? = null var reactionListener: EmojiReactionListener? = null
) : RecyclerView.ViewHolder(itemView), ) : RecyclerView.ViewHolder(itemView),
MenuItem.OnMenuItemClickListener { MenuItem.OnMenuItemClickListener {
var data: T? = null var data: T? = null
init { init {
...@@ -74,23 +75,36 @@ abstract class BaseViewHolder<T : BaseViewModel<*>>( ...@@ -74,23 +75,36 @@ abstract class BaseViewHolder<T : BaseViewModel<*>>(
fun onActionSelected(item: MenuItem, message: Message) fun onActionSelected(item: MenuItem, message: Message)
} }
val longClickListener = { view: View -> private val onClickListener = { view: View ->
if (data?.message?.isSystemMessage() == false) { if (data?.message?.isSystemMessage() == false) {
val menuItems = view.context.inflate(R.menu.message_actions).toList() data?.message?.let {
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply { val menuItems = view.context.inflate(R.menu.message_actions).toList()
val isPinned = data?.message?.pinned ?: false menuItems.find { it.itemId == R.id.action_message_unpin }?.apply {
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin) setTitle(if (it.pinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = isPinned isChecked = it.pinned
}
menuItems.find { it.itemId == R.id.action_message_star }?.apply {
val isStarred = it.starred?.isNotEmpty() ?: false
setTitle(if (isStarred) R.string.action_msg_unstar else R.string.action_msg_star)
isChecked = isStarred
}
val adapter = ActionListAdapter(menuItems, this@BaseViewHolder)
BottomSheetMenu(adapter).show(view.context)
} }
val adapter = ActionListAdapter(menuItems, this@BaseViewHolder)
BottomSheetMenu(adapter).show(view.context)
} }
true
} }
internal fun setupActionMenu(view: View) { internal fun setupActionMenu(view: View) {
if (listener.isActionsEnabled()) { if (listener.isActionsEnabled()) {
view.setOnLongClickListener(longClickListener) view.setOnClickListener(onClickListener)
if (view is ViewGroup) {
for (child in view.children) {
if (child !is RecyclerView && child.id != R.id.recycler_view_reactions) {
setupActionMenu(child)
}
}
}
} }
} }
......
...@@ -14,13 +14,12 @@ import timber.log.Timber ...@@ -14,13 +14,12 @@ import timber.log.Timber
import java.security.InvalidParameterException import java.security.InvalidParameterException
class ChatRoomAdapter( class ChatRoomAdapter(
private val roomType: String, private val roomType: String? = null,
private val roomName: String, private val roomName: String? = null,
private val presenter: ChatRoomPresenter?, private val presenter: ChatRoomPresenter? = null,
private val enableActions: Boolean = true, private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null private val reactionListener: EmojiReactionListener? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() { ) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseViewModel<*>>() private val dataSet = ArrayList<BaseViewModel<*>>()
init { init {
...@@ -61,6 +60,16 @@ class ChatRoomAdapter( ...@@ -61,6 +60,16 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_color_attachment) val view = parent.inflate(R.layout.item_color_attachment)
ColorAttachmentViewHolder(view, actionsListener, reactionListener) ColorAttachmentViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.GENERIC_FILE_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_file_attachment)
GenericFileAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.MESSAGE_REPLY -> {
val view = parent.inflate(R.layout.item_message_reply)
MessageReplyViewHolder(view, actionsListener, reactionListener) { roomName, permalink ->
presenter?.openDirectMessage(roomName, permalink)
}
}
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
} }
...@@ -94,14 +103,26 @@ class ChatRoomAdapter( ...@@ -94,14 +103,26 @@ class ChatRoomAdapter(
} }
when (holder) { when (holder) {
is MessageViewHolder -> holder.bind(dataSet[position] as MessageViewModel) is MessageViewHolder ->
is ImageAttachmentViewHolder -> holder.bind(dataSet[position] as ImageAttachmentViewModel) holder.bind(dataSet[position] as MessageViewModel)
is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel) is ImageAttachmentViewHolder ->
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel) holder.bind(dataSet[position] as ImageAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel) is AudioAttachmentViewHolder ->
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel) holder.bind(dataSet[position] as AudioAttachmentViewModel)
is AuthorAttachmentViewHolder -> holder.bind(dataSet[position] as AuthorAttachmentViewModel) is VideoAttachmentViewHolder ->
is ColorAttachmentViewHolder -> holder.bind(dataSet[position] as ColorAttachmentViewModel) 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)
is GenericFileAttachmentViewHolder ->
holder.bind(dataSet[position] as GenericFileAttachmentViewModel)
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyViewModel)
} }
} }
...@@ -142,7 +163,7 @@ class ChatRoomAdapter( ...@@ -142,7 +163,7 @@ class ChatRoomAdapter(
} }
fun updateItem(message: BaseViewModel<*>) { fun updateItem(message: BaseViewModel<*>) {
var index = dataSet.indexOfLast { it.messageId == message.messageId } val index = dataSet.indexOfLast { it.messageId == message.messageId }
val indexOfNext = dataSet.indexOfFirst { it.messageId == message.messageId } val indexOfNext = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index") Timber.d("index: $index")
if (index > -1) { if (index > -1) {
...@@ -175,26 +196,44 @@ class ChatRoomAdapter( ...@@ -175,26 +196,44 @@ class ChatRoomAdapter(
} }
} }
val actionsListener = object : BaseViewHolder.ActionsListener { private val actionsListener = object : BaseViewHolder.ActionsListener {
override fun isActionsEnabled(): Boolean = enableActions override fun isActionsEnabled(): Boolean = enableActions
override fun onActionSelected(item: MenuItem, message: Message) { override fun onActionSelected(item: MenuItem, message: Message) {
message.apply { message.apply {
when (item.itemId) { when (item.itemId) {
R.id.action_menu_msg_delete -> presenter?.deleteMessage(roomId, id) R.id.action_message_reply -> {
R.id.action_menu_msg_quote -> presenter?.citeMessage(roomType, roomName, id, false) if (roomName != null && roomType != null) {
R.id.action_menu_msg_reply -> presenter?.citeMessage(roomType, roomName, id, true) presenter?.citeMessage(roomName, roomType, id, true)
R.id.action_menu_msg_copy -> presenter?.copyMessage(id) }
R.id.action_menu_msg_edit -> presenter?.editMessage(roomId, id, message.message) }
R.id.action_menu_msg_pin_unpin -> { R.id.action_message_quote -> {
with(item) { if (roomName != null && roomType != null) {
if (!isChecked) { presenter?.citeMessage(roomName, roomType, id, false)
presenter?.pinMessage(id) }
} else { }
presenter?.unpinMessage(id) R.id.action_message_copy -> {
} presenter?.copyMessage(id)
}
R.id.action_message_edit -> {
presenter?.editMessage(roomId, id, message.message)
}
R.id.action_message_star -> {
if (!item.isChecked) {
presenter?.starMessage(id)
} else {
presenter?.unstarMessage(id)
}
}
R.id.action_message_unpin -> {
if (!item.isChecked) {
presenter?.pinMessage(id)
} else {
presenter?.unpinMessage(id)
} }
} }
R.id.action_message_delete -> presenter?.deleteMessage(roomId, id)
R.id.action_menu_msg_react -> presenter?.showReactions(id) R.id.action_menu_msg_react -> presenter?.showReactions(id)
else -> TODO("Not implemented") else -> TODO("Not implemented")
} }
......
...@@ -20,7 +20,6 @@ class ColorAttachmentViewHolder(itemView: View, ...@@ -20,7 +20,6 @@ class ColorAttachmentViewHolder(itemView: View,
init { init {
with(itemView) { with(itemView) {
setupActionMenu(attachment_text)
setupActionMenu(color_attachment_container) setupActionMenu(color_attachment_container)
attachment_text.movementMethod = LinkMovementMethod() attachment_text.movementMethod = LinkMovementMethod()
} }
......
package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.net.Uri
import android.view.View
import androidx.core.net.toUri
import chat.rocket.android.chatroom.viewmodel.GenericFileAttachmentViewModel
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_file_attachment.view.*
class GenericFileAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<GenericFileAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(file_attachment_container)
}
}
override fun bindViews(data: GenericFileAttachmentViewModel) {
with(itemView) {
text_file_name.content = data.attachmentTitle
text_file_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, data.attachmentUrl.toUri()))
}
}
}
}
\ No newline at end of file
...@@ -2,21 +2,20 @@ package chat.rocket.android.chatroom.adapter ...@@ -2,21 +2,20 @@ package chat.rocket.android.chatroom.adapter
import android.view.View import android.view.View
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import com.facebook.drawee.backends.pipeline.Fresco import chat.rocket.android.helper.ImageHelper
import com.facebook.drawee.interfaces.DraweeController
import chat.rocket.android.widget.emoji.EmojiReactionListener import chat.rocket.android.widget.emoji.EmojiReactionListener
import com.stfalcon.frescoimageviewer.ImageViewer import com.facebook.drawee.backends.pipeline.Fresco
import kotlinx.android.synthetic.main.message_attachment.view.* import kotlinx.android.synthetic.main.message_attachment.view.*
class ImageAttachmentViewHolder(itemView: View, class ImageAttachmentViewHolder(
listener: ActionsListener, itemView: View,
reactionListener: EmojiReactionListener? = null) listener: ActionsListener,
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) { reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
init { init {
with(itemView) { with(itemView) {
setupActionMenu(attachment_container) setupActionMenu(attachment_container)
setupActionMenu(image_attachment)
} }
} }
...@@ -29,17 +28,13 @@ class ImageAttachmentViewHolder(itemView: View, ...@@ -29,17 +28,13 @@ class ImageAttachmentViewHolder(itemView: View,
}.build() }.build()
image_attachment.controller = controller image_attachment.controller = controller
file_name.text = data.attachmentTitle file_name.text = data.attachmentTitle
image_attachment.setOnClickListener { view -> image_attachment.setOnClickListener {
// TODO - implement a proper image viewer with a proper Transition ImageHelper.openImage(
val builder = ImageViewer.createPipelineDraweeControllerBuilder() it.context,
.setAutoPlayAnimations(true) data.attachmentUrl,
ImageViewer.Builder(view.context, listOf(data.attachmentUrl)) data.attachmentTitle.toString()
.setStartPosition(0) )
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
.show()
} }
} }
} }
} }
\ No newline at end of file
...@@ -4,7 +4,7 @@ import android.text.method.LinkMovementMethod ...@@ -4,7 +4,7 @@ import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel import chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message.view.* import kotlinx.android.synthetic.main.item_message_attachment.view.*
class MessageAttachmentViewHolder( class MessageAttachmentViewHolder(
itemView: View, itemView: View,
...@@ -14,8 +14,8 @@ class MessageAttachmentViewHolder( ...@@ -14,8 +14,8 @@ class MessageAttachmentViewHolder(
init { init {
with(itemView) { with(itemView) {
setupActionMenu(attachment_container)
text_content.movementMethod = LinkMovementMethod() text_content.movementMethod = LinkMovementMethod()
setupActionMenu(text_content)
} }
} }
......
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageReplyViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message_reply.view.*
class MessageReplyViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
private val replyCallback: (roomName: String, permalink: String) -> Unit
) : BaseViewHolder<MessageReplyViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(itemView)
}
}
override fun bindViews(data: MessageReplyViewModel) {
with(itemView) {
button_message_reply.setOnClickListener {
with(data.rawData) {
replyCallback.invoke(roomName, permalink)
}
}
}
}
}
\ No newline at end of file
...@@ -3,8 +3,11 @@ package chat.rocket.android.chatroom.adapter ...@@ -3,8 +3,11 @@ package chat.rocket.android.chatroom.adapter
import android.graphics.Color import android.graphics.Color
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.isSystemMessage
import kotlinx.android.synthetic.main.avatar.view.* import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.* import kotlinx.android.synthetic.main.item_message.view.*
...@@ -16,8 +19,8 @@ class MessageViewHolder( ...@@ -16,8 +19,8 @@ class MessageViewHolder(
init { init {
with(itemView) { with(itemView) {
setupActionMenu(message_container)
text_content.movementMethod = LinkMovementMethod() text_content.movementMethod = LinkMovementMethod()
setupActionMenu(text_content)
} }
} }
...@@ -33,6 +36,10 @@ class MessageViewHolder( ...@@ -33,6 +36,10 @@ class MessageViewHolder(
text_content.setTextColor( text_content.setTextColor(
if (data.isTemporary) Color.GRAY else Color.BLACK if (data.isTemporary) Color.GRAY else Color.BLACK
) )
data.message.let {
text_edit_indicator.isVisible = !it.isSystemMessage() && it.editedBy != null
image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false
}
} }
} }
} }
\ No newline at end of file
...@@ -5,6 +5,7 @@ import android.net.Uri ...@@ -5,6 +5,7 @@ import android.net.Uri
import android.view.View import android.view.View
import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel
import chat.rocket.android.util.extensions.content import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_url_preview.view.* import kotlinx.android.synthetic.main.message_url_preview.view.*
...@@ -32,10 +33,17 @@ class UrlPreviewViewHolder(itemView: View, ...@@ -32,10 +33,17 @@ class UrlPreviewViewHolder(itemView: View,
text_title.content = data.title text_title.content = data.title
text_description.content = data.description ?: "" text_description.content = data.description ?: ""
url_preview_layout.setOnClickListener { view -> url_preview_layout.setOnClickListener(onClickListener)
view.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.rawData.url))) text_host.setOnClickListener(onClickListener)
} text_title.setOnClickListener(onClickListener)
image_preview.setOnClickListener(onClickListener)
text_description.setOnClickListener(onClickListener)
} }
} }
private val onClickListener = { view: View ->
if (data != null) {
view.openTabbedUrl(Uri.parse(data!!.rawData.url))
}
}
} }
\ No newline at end of file
...@@ -14,10 +14,9 @@ class VideoAttachmentViewHolder(itemView: View, ...@@ -14,10 +14,9 @@ class VideoAttachmentViewHolder(itemView: View,
init { init {
with(itemView) { with(itemView) {
setupActionMenu(attachment_container)
image_attachment.setVisible(false) image_attachment.setVisible(false)
audio_video_attachment.setVisible(true) audio_video_attachment.setVisible(true)
setupActionMenu(attachment_container)
setupActionMenu(audio_video_attachment)
} }
} }
......
package chat.rocket.android.chatroom.domain
data class MessageReply(
val roomName: String,
val permalink: String
)
\ No newline at end of file
...@@ -2,15 +2,33 @@ package chat.rocket.android.chatroom.presentation ...@@ -2,15 +2,33 @@ package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.members.ui.newInstance import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.server.ui.changeServerIntent import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack import chat.rocket.android.util.extensions.addFragmentBackStack
class ChatRoomNavigator(internal val activity: ChatRoomActivity) { class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
fun toMembersList(chatRoomId: String, chatRoomType: String) { fun toMembersList(chatRoomId: String) {
activity.addFragmentBackStack("MembersFragment", R.id.fragment_container) { activity.addFragmentBackStack("MembersFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomType) chat.rocket.android.members.ui.newInstance(chatRoomId)
}
}
fun toPinnedMessageList(chatRoomId: String) {
activity.addFragmentBackStack("PinnedMessages", R.id.fragment_container) {
chat.rocket.android.pinnedmessages.ui.newInstance(chatRoomId)
}
}
fun toFavoriteMessageList(chatRoomId: String) {
activity.addFragmentBackStack("FavoriteMessages", R.id.fragment_container) {
chat.rocket.android.favoritemessages.ui.newInstance(chatRoomId)
}
}
fun toFileList(chatRoomId: String) {
activity.addFragmentBackStack("Files", R.id.fragment_container) {
chat.rocket.android.files.ui.newInstance(chatRoomId)
} }
} }
...@@ -18,4 +36,17 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) { ...@@ -18,4 +36,17 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
activity.startActivity(activity.changeServerIntent()) activity.startActivity(activity.changeServerIntent())
activity.finish() activity.finish()
} }
fun toDirectMessage(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean,
isChatRoomCreator: Boolean,
chatRoomMessage: String) {
activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
} }
\ No newline at end of file
...@@ -8,6 +8,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewMod ...@@ -8,6 +8,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewMod
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
interface ChatRoomView : LoadingView, MessageView { interface ChatRoomView : LoadingView, MessageView {
...@@ -25,10 +26,22 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -25,10 +26,22 @@ interface ChatRoomView : LoadingView, MessageView {
*/ */
fun sendMessage(text: String) fun sendMessage(text: String)
/**
* Shows the username(s) of the user(s) who is/are typing in the chat room.
*
* @param usernameList The list of username to show.
*/
fun showTypingStatus(usernameList: ArrayList<String>)
/**
* Hides the typing status view.
*/
fun hideTypingStatusView()
/** /**
* Perform file selection with the mime type [filter] * Perform file selection with the mime type [filter]
*/ */
fun showFileSelection(filter: Array<String>) fun showFileSelection(filter: Array<String>?)
/** /**
* Uploads a file to a chat room. * Uploads a file to a chat room.
...@@ -109,8 +122,10 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -109,8 +122,10 @@ interface ChatRoomView : LoadingView, MessageView {
fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>) fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>)
/** /**
* This user has joined the chat callback. * This user has joined the chat callback.
*
* @param userCanPost Whether the user can post a message or not.
*/ */
fun onJoined() fun onJoined(userCanPost: Boolean)
fun showReactionsPopup(messageId: String) fun showReactionsPopup(messageId: String)
...@@ -120,4 +135,15 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -120,4 +135,15 @@ interface ChatRoomView : LoadingView, MessageView {
* @param commands The list of available commands. * @param commands The list of available commands.
*/ */
fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>) fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>)
/**
* Communicate whether it's a broadcast channel and if current user can post to it.
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
/**
* Open a DM with the user in the given [chatRoom] and pass the [permalink] for the message
* to reply.
*/
fun openDirectMessage(chatRoom: ChatRoom, permalink: String)
} }
\ No newline at end of file
...@@ -60,13 +60,22 @@ class MessageService : JobService() { ...@@ -60,13 +60,22 @@ class MessageService : JobService() {
) )
messageRepository.save(message.copy(isTemporary = false)) messageRepository.save(message.copy(isTemporary = false))
Timber.d("Sent scheduled message given by id: ${message.id}") Timber.d("Sent scheduled message given by id: ${message.id}")
} catch (ex: RocketChatException) { } catch (ex: Exception) {
Timber.e(ex) Timber.e(ex)
if (ex.message?.contains("E11000", true) == true) { // TODO - remove the generic message when we implement :userId:/message subscription
// XXX: Temporary solution. We need proper error codes from the api. if (ex is IllegalStateException) {
messageRepository.save(message.copy(isTemporary = false)) Timber.d(ex, "Probably a read-only problem...")
// TODO: For now we are only going to reschedule when api is fixed.
messageRepository.removeById(message.id)
jobFinished(params, false)
} else {
// some other error
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)
} }
jobFinished(params, true)
} }
} }
} }
......
...@@ -6,6 +6,8 @@ import android.content.Intent ...@@ -6,6 +6,8 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.text.SpannableStringBuilder
import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
...@@ -27,39 +29,42 @@ fun Context.chatRoomIntent( ...@@ -27,39 +29,42 @@ fun Context.chatRoomIntent(
chatRoomType: String, chatRoomType: String,
isChatRoomReadOnly: Boolean, isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long, chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean = true isChatRoomSubscribed: Boolean = true,
isChatRoomCreator: Boolean = false,
chatRoomMessage: String? = null
): Intent { ): Intent {
return Intent(this, ChatRoomActivity::class.java).apply { return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId) putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName) putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType) putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
putExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly) putExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen) putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putExtra(INTENT_CHAT_IS_SUBSCRIBED, isChatRoomSubscribed) putExtra(INTENT_CHAT_IS_SUBSCRIBED, isChatRoomSubscribed)
putExtra(INTENT_CHAT_ROOM_IS_CREATOR, isChatRoomCreator)
putExtra(INTENT_CHAT_ROOM_MESSAGE, chatRoomMessage)
} }
} }
private const val INTENT_CHAT_ROOM_ID = "chat_room_id" private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name" private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type" private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only" private const val INTENT_CHAT_ROOM_IS_READ_ONLY = "chat_room_is_read_only"
private const val INTENT_CHAT_ROOM_IS_CREATOR = "chat_room_is_creator"
private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen" private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val INTENT_CHAT_IS_SUBSCRIBED = "is_chat_room_subscribed" private const val INTENT_CHAT_IS_SUBSCRIBED = "is_chat_room_subscribed"
private const val INTENT_CHAT_ROOM_MESSAGE = "chat_room_message"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
// TODO - workaround for now... We will move to a single activity // TODO - workaround for now... We will move to a single activity
@Inject lateinit var serverInteractor: GetCurrentServerInteractor @Inject
@Inject lateinit var navigator: ChatRoomNavigator lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var managerFactory: ConnectionManagerFactory @Inject
lateinit var navigator: ChatRoomNavigator
private lateinit var chatRoomId: String @Inject
private lateinit var chatRoomName: String lateinit var managerFactory: ConnectionManagerFactory
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private var isChatRoomSubscribed: Boolean = true
private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this) AndroidInjection.inject(this)
...@@ -75,28 +80,33 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -75,28 +80,33 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
return return
} }
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID) val chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" } requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
chatRoomName = intent.getStringExtra(INTENT_CHAT_ROOM_NAME) val chatRoomName = intent.getStringExtra(INTENT_CHAT_ROOM_NAME)
requireNotNull(chatRoomName) { "no chat_room_name provided in Intent extras" } requireNotNull(chatRoomName) { "no chat_room_name provided in Intent extras" }
chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE) val chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" } requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, true) val isChatRoomReadOnly = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true)
requireNotNull(chatRoomType) { "no is_chat_room_read_only provided in Intent extras" }
setupToolbar() val isChatRoomCreator = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_CREATOR, false)
val chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1) val isChatRoomSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true)
isChatRoomSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true) val chatRoomMessage = intent.getStringExtra(INTENT_CHAT_ROOM_MESSAGE)
setupToolbar()
if (supportFragmentManager.findFragmentByTag("ChatRoomFragment") == null) { if (supportFragmentManager.findFragmentByTag(TAG_CHAT_ROOM_FRAGMENT) == null) {
addFragment("ChatRoomFragment", R.id.fragment_container) { addFragment(TAG_CHAT_ROOM_FRAGMENT, R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen, newInstance(
isChatRoomSubscribed) chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage
)
} }
} }
} }
...@@ -109,51 +119,46 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -109,51 +119,46 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector return fragmentDispatchingAndroidInjector
} }
fun showRoomTypeIcon(showRoomTypeIcon: Boolean) {
if (showRoomTypeIcon) {
val roomType = roomTypeOf(chatRoomType)
val drawable = when (roomType) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, this)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, this)
}
is RoomType.DirectMessage -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, this)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.white)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
}
} else {
text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
}
private fun setupToolbar() { private fun setupToolbar() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
toolbar.setNavigationOnClickListener { finishActivity() }
}
fun showToolbarTitle(title: String) {
text_room_name.textContent = title
}
showRoomTypeIcon(true) fun showToolbarChatRoomIcon(chatRoomType: String) {
val drawable = when (roomTypeOf(chatRoomType)) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_hashtag_black_12dp, this)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_12_dp, this)
}
else -> null
}
toolbar.setNavigationOnClickListener { drawable?.let {
finishActivity() val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.colorWhite)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
} }
} }
fun setupToolbarTitle(toolbarTitle: String) { fun hideToolbarChatRoomIcon() {
text_room_name.textContent = toolbarTitle text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
} }
private fun finishActivity() { private fun finishActivity() {
super.onBackPressed() super.onBackPressed()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit) overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
} }
companion object {
const val TAG_CHAT_ROOM_FRAGMENT = "ChatRoomFragment"
}
} }
\ No newline at end of file
package chat.rocket.android.chatroom.ui
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.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject
private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
class PinnedMessagesActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pinned_messages)
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
chatRoomName = intent.getStringExtra(INTENT_CHAT_ROOM_NAME)
requireNotNull(chatRoomName) { "no chat_room_name provided in Intent extras" }
chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
setupToolbar()
addFragment("PinnedMessagesFragment", R.id.fragment_container) {
newPinnedMessagesFragment(chatRoomId, chatRoomName, chatRoomType)
}
}
override fun onBackPressed() = finishActivity()
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = getString(R.string.title_pinned_messages)
toolbar.setNavigationOnClickListener {
finishActivity()
}
}
private fun finishActivity() {
super.onBackPressed()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
}
}
\ No newline at end of file
...@@ -7,8 +7,10 @@ import chat.rocket.android.util.extensions.setVisible ...@@ -7,8 +7,10 @@ import chat.rocket.android.util.extensions.setVisible
/** /**
* An adapter for bottomsheet menu that lists all the actions that could be taken over a chat message. * An adapter for bottomsheet menu that lists all the actions that could be taken over a chat message.
*/ */
class ActionListAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuItem.OnMenuItemClickListener) : class ActionListAdapter(
ListBottomSheetAdapter(menuItems = menuItems, callback = callback) { menuItems: List<MenuItem> = emptyList(),
callback: MenuItem.OnMenuItemClickListener
) : ListBottomSheetAdapter(menuItems = menuItems, callback = callback) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = menuItems[position] val item = menuItems[position]
...@@ -24,8 +26,12 @@ class ActionListAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuI ...@@ -24,8 +26,12 @@ class ActionListAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuI
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
callback?.onMenuItemClick(item) callback?.onMenuItemClick(item)
} }
val deleteTextColor = holder.itemView.context.resources.getColor(R.color.red) val deleteTextColor = holder.itemView.context.resources.getColor(R.color.colorRed)
val color = if (item.itemId == R.id.action_menu_msg_delete) deleteTextColor else textColors.get(item.itemId) val color = if (item.itemId == R.id.action_message_delete) {
deleteTextColor
} else {
textColors.get(item.itemId)
}
holder.textTitle.setTextColor(color) holder.textTitle.setTextColor(color)
} }
} }
\ No newline at end of file
...@@ -23,7 +23,9 @@ interface BaseViewModel<out T> { ...@@ -23,7 +23,9 @@ interface BaseViewModel<out T> {
AUDIO_ATTACHMENT(5), AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6), MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7), AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8) COLOR_ATTACHMENT(8),
GENERIC_FILE_ATTACHMENT(9),
MESSAGE_REPLY(10)
} }
} }
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
data class GenericFileAttachmentViewModel(
override val message: Message,
override val rawData: GenericFileAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<GenericFileAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.GENERIC_FILE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_file_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.android.chatroom.domain.MessageReply
import chat.rocket.core.model.Message
data class MessageReplyViewModel(
override val rawData: MessageReply,
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>?,
override var preview: Message?,
override var isTemporary: Boolean = false,
override val message: Message
) : BaseViewModel<MessageReply> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_REPLY.viewType
override val layoutId: Int
get() = R.layout.item_message_reply
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.core.model.ChatRoomRole
data class RoomViewModel(
val roles: List<ChatRoomRole>,
val isBroadcast: Boolean = false,
val isRoom: Boolean = false
)
\ No newline at end of file
...@@ -16,22 +16,22 @@ import chat.rocket.android.R ...@@ -16,22 +16,22 @@ import chat.rocket.android.R
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.checkIfMyself import chat.rocket.android.infrastructure.checkIfMyself
import chat.rocket.android.server.domain.PublicSettings import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.showLastMessage
import chat.rocket.android.server.domain.useRealName import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.item_chat.view.* import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.* import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class ChatRoomsAdapter(private val context: Context, class ChatRoomsAdapter(
private val settings: PublicSettings, private val context: Context,
private val localRepository: LocalRepository, private val settings: PublicSettings,
private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() { private val localRepository: LocalRepository,
private val listener: (ChatRoom) -> Unit
) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() {
var dataSet: MutableList<ChatRoom> = ArrayList() var dataSet: MutableList<ChatRoom> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = ViewHolder(parent.inflate(R.layout.item_chat)) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = ViewHolder(parent.inflate(R.layout.item_chat))
...@@ -51,24 +51,31 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -51,24 +51,31 @@ class ChatRoomsAdapter(private val context: Context,
bindAvatar(chatRoom, image_avatar) bindAvatar(chatRoom, image_avatar)
bindName(chatRoom, text_chat_name) bindName(chatRoom, text_chat_name)
bindIcon(chatRoom, image_chat_icon) bindIcon(chatRoom, image_chat_icon)
bindLastMessageDateTime(chatRoom, text_last_message_date_time) if (settings.showLastMessage()) {
bindLastMessage(chatRoom, text_last_message) text_last_message.setVisible(true)
text_last_message_date_time.setVisible(true)
bindLastMessageDateTime(chatRoom, text_last_message_date_time)
bindLastMessage(chatRoom, text_last_message)
} else {
text_last_message.setVisible(false)
text_last_message_date_time.setVisible(false)
}
bindUnreadMessages(chatRoom, text_total_unread_messages) bindUnreadMessages(chatRoom, text_total_unread_messages)
if (chatRoom.alert || chatRoom.unread > 0) { if (chatRoom.alert || chatRoom.unread > 0) {
text_chat_name.setTextColor(ContextCompat.getColor(context, text_chat_name.setTextColor(ContextCompat.getColor(context,
R.color.colorPrimaryText)) R.color.colorPrimaryText))
text_last_message_date_time.setTextColor(ContextCompat.getColor(context, text_last_message_date_time.setTextColor(ContextCompat.getColor(context,
R.color.colorAccent)) R.color.colorAccent))
text_last_message.setTextColor(ContextCompat.getColor(context, text_last_message.setTextColor(ContextCompat.getColor(context,
android.R.color.primary_text_light)) android.R.color.primary_text_light))
} else { } else {
text_chat_name.setTextColor(ContextCompat.getColor(context, text_chat_name.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText)) R.color.colorSecondaryText))
text_last_message_date_time.setTextColor(ContextCompat.getColor(context, text_last_message_date_time.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText)) R.color.colorSecondaryText))
text_last_message.setTextColor(ContextCompat.getColor(context, text_last_message.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText)) R.color.colorSecondaryText))
} }
setOnClickListener { listener(chatRoom) } setOnClickListener { listener(chatRoom) }
...@@ -85,11 +92,11 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -85,11 +92,11 @@ class ChatRoomsAdapter(private val context: Context,
private fun bindIcon(chatRoom: ChatRoom, imageView: ImageView) { private fun bindIcon(chatRoom: ChatRoom, imageView: ImageView) {
val drawable = when (chatRoom.type) { val drawable = when (chatRoom.type) {
is RoomType.Channel -> DrawableHelper.getDrawableFromId( is RoomType.Channel -> DrawableHelper.getDrawableFromId(
R.drawable.ic_hashtag_12dp, R.drawable.ic_hashtag_black_12dp,
context context
) )
is RoomType.PrivateGroup -> DrawableHelper.getDrawableFromId( is RoomType.PrivateGroup -> DrawableHelper.getDrawableFromId(
R.drawable.ic_lock_12_dp, R.drawable.ic_lock_black_12_dp,
context context
) )
is RoomType.DirectMessage -> DrawableHelper.getUserStatusDrawable( is RoomType.DirectMessage -> DrawableHelper.getUserStatusDrawable(
...@@ -112,7 +119,15 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -112,7 +119,15 @@ class ChatRoomsAdapter(private val context: Context,
} }
private fun bindName(chatRoom: ChatRoom, textView: TextView) { private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.textContent = chatRoom.name textView.textContent = chatRoomName(chatRoom)
}
private fun chatRoomName(chatRoom: ChatRoom): String {
return if (settings.useRealName()) {
chatRoom.fullName ?: chatRoom.name
} else {
chatRoom.name
}
} }
private fun bindLastMessageDateTime(chatRoom: ChatRoom, textView: TextView) { private fun bindLastMessageDateTime(chatRoom: ChatRoom, textView: TextView) {
......
...@@ -14,6 +14,7 @@ import android.support.v7.widget.SearchView ...@@ -14,6 +14,7 @@ import android.support.v7.widget.SearchView
import android.view.* import android.view.*
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.RadioGroup import android.widget.RadioGroup
import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView import chat.rocket.android.chatrooms.presentation.ChatRoomsView
...@@ -36,11 +37,14 @@ import timber.log.Timber ...@@ -36,11 +37,14 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class ChatRoomsFragment : Fragment(), ChatRoomsView { class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject lateinit var presenter: ChatRoomsPresenter @Inject
@Inject lateinit var serverInteractor: GetCurrentServerInteractor lateinit var presenter: ChatRoomsPresenter
@Inject lateinit var settingsRepository: SettingsRepository @Inject
@Inject lateinit var localRepository: LocalRepository lateinit var serverInteractor: GetCurrentServerInteractor
private lateinit var preferences: SharedPreferences @Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var localRepository: LocalRepository
private var searchView: SearchView? = null private var searchView: SearchView? = null
private val handler = Handler() private val handler = Handler()
...@@ -55,7 +59,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -55,7 +59,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
setHasOptionsMenu(true) setHasOptionsMenu(true)
preferences = context?.getSharedPreferences("temp", Context.MODE_PRIVATE)!!
} }
override fun onDestroy() { override fun onDestroy() {
...@@ -135,9 +138,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -135,9 +138,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}) })
val dialogSort = AlertDialog.Builder(context) val dialogSort = AlertDialog.Builder(context)
.setTitle(R.string.dialog_sort_title) .setTitle(R.string.dialog_sort_title)
.setView(dialogLayout) .setView(dialogLayout)
.setPositiveButton("Done", { dialog, _ -> dialog.dismiss() }) .setPositiveButton("Done", { dialog, _ -> dialog.dismiss() })
dialogSort.show() dialogSort.show()
} }
...@@ -145,9 +148,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -145,9 +148,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private fun invalidateQueryOnSearch(){ private fun invalidateQueryOnSearch() {
searchView?.let { searchView?.let {
if (!searchView!!.isIconified){ if (!searchView!!.isIconified) {
queryChatRoomsByName(searchView!!.query.toString()) queryChatRoomsByName(searchView!!.query.toString())
} }
} }
...@@ -162,7 +165,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -162,7 +165,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
/*val diff = async(CommonPool) { /*val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet)) DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet))
}.await()*/ }.await()*/
text_no_search.isVisible = newDataSet.isEmpty()
if (isActive) { if (isActive) {
adapter.baseAdapter.updateRooms(newDataSet) adapter.baseAdapter.updateRooms(newDataSet)
// TODO - fix crash to re-enable diff.dispatchUpdatesTo(adapter) // TODO - fix crash to re-enable diff.dispatchUpdatesTo(adapter)
...@@ -178,7 +181,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -178,7 +181,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
ui { text_no_data_to_display.setVisible(true) } ui { text_no_data_to_display.setVisible(true) }
} }
override fun showLoading(){ override fun showLoading() {
ui { view_loading.setVisible(true) } ui { view_loading.setVisible(true) }
} }
...@@ -235,18 +238,18 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -235,18 +238,18 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
ui { ui {
recycler_view.layoutManager = LinearLayoutManager(it, LinearLayoutManager.VERTICAL, false) recycler_view.layoutManager = LinearLayoutManager(it, LinearLayoutManager.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(it, recycler_view.addItemDecoration(DividerItemDecoration(it,
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start), resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start),
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end))) resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)))
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
// TODO - use a ViewModel Mapper instead of using settings on the adapter // TODO - use a ViewModel Mapper instead of using settings on the adapter
val baseAdapter = ChatRoomsAdapter(it, val baseAdapter = ChatRoomsAdapter(it,
settingsRepository.get(serverInteractor.get()!!), localRepository) { settingsRepository.get(serverInteractor.get()!!), localRepository) { chatRoom ->
chatRoom -> presenter.loadChatRoom(chatRoom) presenter.loadChatRoom(chatRoom)
} }
sectionedAdapter = SimpleSectionedRecyclerViewAdapter(it, sectionedAdapter = SimpleSectionedRecyclerViewAdapter(it,
R.layout.item_chatroom_header, R.id.text_chatroom_header, baseAdapter) R.layout.item_chatroom_header, R.id.text_chatroom_header, baseAdapter)
recycler_view.adapter = sectionedAdapter recycler_view.adapter = sectionedAdapter
} }
} }
......
...@@ -3,20 +3,22 @@ package chat.rocket.android.dagger.module ...@@ -3,20 +3,22 @@ package chat.rocket.android.dagger.module
import chat.rocket.android.authentication.di.AuthenticationModule import chat.rocket.android.authentication.di.AuthenticationModule
import chat.rocket.android.authentication.login.di.LoginFragmentProvider import chat.rocket.android.authentication.login.di.LoginFragmentProvider
import chat.rocket.android.authentication.registerusername.di.RegisterUsernameFragmentProvider import chat.rocket.android.authentication.registerusername.di.RegisterUsernameFragmentProvider
import chat.rocket.android.authentication.resetpassword.di.ResetPasswordFragmentProvider
import chat.rocket.android.authentication.server.di.ServerFragmentProvider import chat.rocket.android.authentication.server.di.ServerFragmentProvider
import chat.rocket.android.authentication.signup.di.SignupFragmentProvider import chat.rocket.android.authentication.signup.di.SignupFragmentProvider
import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider
import chat.rocket.android.chatroom.di.ChatRoomModule import chat.rocket.android.chatroom.di.ChatRoomModule
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider import chat.rocket.android.chatroom.di.FavoriteMessagesFragmentProvider
import chat.rocket.android.chatroom.ui.ChatRoomActivity import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.PinnedMessagesActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.files.di.FilesFragmentProvider
import chat.rocket.android.main.di.MainModule import chat.rocket.android.main.di.MainModule
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.members.di.MembersFragmentProvider import chat.rocket.android.members.di.MembersFragmentProvider
import chat.rocket.android.pinnedmessages.di.PinnedMessagesFragmentProvider
import chat.rocket.android.profile.di.ProfileFragmentProvider import chat.rocket.android.profile.di.ProfileFragmentProvider
import chat.rocket.android.server.di.ChangeServerModule import chat.rocket.android.server.di.ChangeServerModule
import chat.rocket.android.server.ui.ChangeServerActivity import chat.rocket.android.server.ui.ChangeServerActivity
...@@ -29,32 +31,40 @@ import dagger.android.ContributesAndroidInjector ...@@ -29,32 +31,40 @@ import dagger.android.ContributesAndroidInjector
abstract class ActivityBuilder { abstract class ActivityBuilder {
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [AuthenticationModule::class, @ContributesAndroidInjector(
ServerFragmentProvider::class, modules = [AuthenticationModule::class,
LoginFragmentProvider::class, ServerFragmentProvider::class,
RegisterUsernameFragmentProvider::class, LoginFragmentProvider::class,
SignupFragmentProvider::class, RegisterUsernameFragmentProvider::class,
TwoFAFragmentProvider::class ResetPasswordFragmentProvider::class,
]) SignupFragmentProvider::class,
TwoFAFragmentProvider::class
]
)
abstract fun bindAuthenticationActivity(): AuthenticationActivity abstract fun bindAuthenticationActivity(): AuthenticationActivity
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [MainModule::class, @ContributesAndroidInjector(
ChatRoomsFragmentProvider::class, modules = [MainModule::class,
ProfileFragmentProvider::class ChatRoomsFragmentProvider::class,
]) ProfileFragmentProvider::class
]
)
abstract fun bindMainActivity(): MainActivity abstract fun bindMainActivity(): MainActivity
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [ChatRoomModule::class, @ContributesAndroidInjector(
ChatRoomFragmentProvider::class, modules = [
MembersFragmentProvider::class]) ChatRoomModule::class,
ChatRoomFragmentProvider::class,
MembersFragmentProvider::class,
PinnedMessagesFragmentProvider::class,
FavoriteMessagesFragmentProvider::class,
FilesFragmentProvider::class
]
)
abstract fun bindChatRoomActivity(): ChatRoomActivity abstract fun bindChatRoomActivity(): ChatRoomActivity
@PerActivity
@ContributesAndroidInjector(modules = [PinnedMessagesFragmentProvider::class])
abstract fun bindPinnedMessagesActivity(): PinnedMessagesActivity
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [PasswordFragmentProvider::class]) @ContributesAndroidInjector(modules = [PasswordFragmentProvider::class])
abstract fun bindPasswordActivity(): PasswordActivity abstract fun bindPasswordActivity(): PasswordActivity
......
...@@ -14,12 +14,10 @@ import chat.rocket.android.app.RocketChatDatabase ...@@ -14,12 +14,10 @@ import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.chatroom.service.MessageService import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.dagger.qualifier.ForMessages import chat.rocket.android.dagger.qualifier.ForMessages
import chat.rocket.android.helper.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository import chat.rocket.android.infrastructure.SharedPreferencesLocalRepository
import chat.rocket.android.push.GroupedPush import chat.rocket.android.push.GroupedPush
import chat.rocket.android.push.PushManager import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
...@@ -38,20 +36,16 @@ import chat.rocket.core.internal.ReactionsAdapter ...@@ -38,20 +36,16 @@ import chat.rocket.core.internal.ReactionsAdapter
import com.facebook.drawee.backends.pipeline.DraweeConfig import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.core.ImagePipelineConfig
import com.facebook.imagepipeline.listener.RequestListener
import com.facebook.imagepipeline.listener.RequestLoggingListener import com.facebook.imagepipeline.listener.RequestLoggingListener
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.Job
import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import ru.noties.markwon.SpannableConfiguration import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.il.AsyncDrawableLoader
import ru.noties.markwon.spans.SpannableTheme import ru.noties.markwon.spans.SpannableTheme
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton import javax.inject.Singleton
...@@ -113,35 +107,18 @@ class AppModule { ...@@ -113,35 +107,18 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideOkHttpClient(logger: HttpLoggingInterceptor): OkHttpClient { fun provideOkHttpClient(logger: HttpLoggingInterceptor): OkHttpClient {
return OkHttpClient.Builder().apply { return OkHttpClient.Builder()
addInterceptor(logger) .addInterceptor(logger)
connectTimeout(15, TimeUnit.SECONDS) .connectTimeout(15, TimeUnit.SECONDS)
readTimeout(20, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS)
writeTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS)
}.build() .build()
}
@Provides
@ForFresco
@Singleton
fun provideFrescoAuthInterceptor(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
return FrescoAuthInterceptor(tokenRepository, currentServerInteractor)
}
@Provides
@ForFresco
@Singleton
fun provideFrescoOkHttpClient(okHttpClient: OkHttpClient, @ForFresco authInterceptor: Interceptor): OkHttpClient {
return okHttpClient.newBuilder().apply {
//addInterceptor(authInterceptor)
}.build()
} }
@Provides @Provides
@Singleton @Singleton
fun provideImagePipelineConfig(context: Context, @ForFresco okHttpClient: OkHttpClient): ImagePipelineConfig { fun provideImagePipelineConfig(context: Context, okHttpClient: OkHttpClient): ImagePipelineConfig {
val listeners = HashSet<RequestListener>() val listeners = setOf(RequestLoggingListener())
listeners.add(RequestLoggingListener())
return OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient) return OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient)
.setRequestListeners(listeners) .setRequestListeners(listeners)
...@@ -169,6 +146,7 @@ class AppModule { ...@@ -169,6 +146,7 @@ class AppModule {
} }
@Provides @Provides
@Singleton
fun provideSharedPreferences(context: Application) = fun provideSharedPreferences(context: Application) =
context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE) context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
...@@ -180,8 +158,8 @@ class AppModule { ...@@ -180,8 +158,8 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideLocalRepository(prefs: SharedPreferences): LocalRepository { fun provideLocalRepository(prefs: SharedPreferences, moshi: Moshi): LocalRepository {
return SharedPrefsLocalRepository(prefs) return SharedPreferencesLocalRepository(prefs, moshi)
} }
@Provides @Provides
...@@ -196,6 +174,12 @@ class AppModule { ...@@ -196,6 +174,12 @@ class AppModule {
return SharedPreferencesSettingsRepository(localRepository) return SharedPreferencesSettingsRepository(localRepository)
} }
@Provides
@Singleton
fun providePermissionsRepository(localRepository: LocalRepository, moshi: Moshi): PermissionsRepository {
return SharedPreferencesPermissionsRepository(localRepository, moshi)
}
@Provides @Provides
@Singleton @Singleton
fun provideRoomRepository(): RoomRepository { fun provideRoomRepository(): RoomRepository {
...@@ -261,14 +245,9 @@ class AppModule { ...@@ -261,14 +245,9 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideConfiguration(context: Application, client: OkHttpClient): SpannableConfiguration { fun provideConfiguration(context: Application): SpannableConfiguration {
val res = context.resources val res = context.resources
return SpannableConfiguration.builder(context) return SpannableConfiguration.builder(context)
.asyncDrawableLoader(AsyncDrawableLoader.builder()
.client(client)
.executorService(Executors.newCachedThreadPool())
.resources(res)
.build())
.theme(SpannableTheme.builder() .theme(SpannableTheme.builder()
.linkColor(res.getColor(R.color.colorAccent)) .linkColor(res.getColor(R.color.colorAccent))
.build()) .build())
...@@ -276,15 +255,9 @@ class AppModule { ...@@ -276,15 +255,9 @@ class AppModule {
} }
@Provides @Provides
@Singleton fun provideMessageParser(context: Application, configuration: SpannableConfiguration, serverInteractor: GetCurrentServerInteractor, settingsInteractor: GetSettingsInteractor): MessageParser {
fun provideMessageParser(context: Application, configuration: SpannableConfiguration): MessageParser { val url = serverInteractor.get()!!
return MessageParser(context, configuration) return MessageParser(context, configuration, settingsInteractor.get(url))
}
@Provides
@Singleton
fun providePermissionInteractor(settingsRepository: SettingsRepository, serverRepository: CurrentServerRepository): GetPermissionsInteractor {
return GetPermissionsInteractor(settingsRepository, serverRepository)
} }
@Provides @Provides
......
...@@ -3,7 +3,21 @@ package chat.rocket.android.dagger.module ...@@ -3,7 +3,21 @@ package chat.rocket.android.dagger.module
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository import chat.rocket.android.infrastructure.SharedPreferencesLocalRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
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.internal.AttachmentAdapterFactory
import chat.rocket.core.internal.ReactionsAdapter
import com.squareup.moshi.Moshi
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import javax.inject.Singleton import javax.inject.Singleton
...@@ -11,14 +25,53 @@ import javax.inject.Singleton ...@@ -11,14 +25,53 @@ import javax.inject.Singleton
@Module @Module
class LocalModule { class LocalModule {
@Provides
@Singleton
fun providePlatformLogger(): PlatformLogger {
return TimberLogger
}
@Provides
@Singleton
fun provideCurrentServerRepository(prefs: SharedPreferences): CurrentServerRepository {
return SharedPrefsCurrentServerRepository(prefs)
}
@Provides
@Singleton
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())
)
.add(ReactionsAdapter())
.build()
}
@Provides @Provides
fun provideSharedPreferences(context: Context): SharedPreferences { fun provideSharedPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE) return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
} }
@Provides @Provides
@Singleton @Singleton
fun provideLocalRepository(prefs: SharedPreferences): LocalRepository { fun provideLocalRepository(sharedPreferences: SharedPreferences, moshi: Moshi): LocalRepository {
return SharedPrefsLocalRepository(prefs) return SharedPreferencesLocalRepository(sharedPreferences, moshi)
} }
} }
\ No newline at end of file
package chat.rocket.android.dagger.qualifier
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ForFresco
\ No newline at end of file
package chat.rocket.android.chatroom.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesView
import chat.rocket.android.favoritemessages.ui.FavoriteMessagesFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class FavoriteMessagesFragmentModule {
@Provides
fun provideLifecycleOwner(frag: FavoriteMessagesFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
@Provides
fun provideFavoriteMessagesView(frag: FavoriteMessagesFragment): FavoriteMessagesView {
return frag
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.di
import chat.rocket.android.favoritemessages.ui.FavoriteMessagesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class FavoriteMessagesFragmentProvider {
@ContributesAndroidInjector(modules = [FavoriteMessagesFragmentModule::class])
abstract fun provideFavoriteMessageFragment(): FavoriteMessagesFragment
}
\ No newline at end of file
package chat.rocket.android.favoritemessages.presentation
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getFavoriteMessages
import timber.log.Timber
import javax.inject.Inject
class FavoriteMessagesPresenter @Inject constructor(
private val view: FavoriteMessagesView,
private val strategy: CancelStrategy,
private val roomsInteractor: ChatRoomsInteractor,
private val mapper: ViewModelMapper,
val serverInteractor: GetCurrentServerInteractor,
val factory: RocketChatClientFactory
) {
private val serverUrl = serverInteractor.get()!!
private val client = factory.create(serverUrl)
private var offset: Int = 0
/**
* Loads all favorite messages for room. the given room id.
*
* @param roomId The id of the room to get its favorite messages.
*/
fun loadFavoriteMessages(roomId: String) {
launchUI(strategy) {
try {
view.showLoading()
roomsInteractor.getById(serverUrl, roomId)?.let {
val favoriteMessages = client.getFavoriteMessages(roomId, it.type, offset)
val messageList = mapper.map(favoriteMessages.result)
view.showFavoriteMessages(messageList)
offset += 1 * 30
}.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server.")
}
} catch (exception: RocketChatException) {
Timber.e(exception)
} finally {
view.hideLoading()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.favoritemessages.presentation
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface FavoriteMessagesView : MessageView, LoadingView {
/**
* Shows the list of favorite messages for the current room.
*
* @param favoriteMessages The list of favorite messages to show.
*/
fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>)
}
\ No newline at end of file
package chat.rocket.android.favoritemessages.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesPresenter
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesView
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.inflate
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_favorite_messages.*
import javax.inject.Inject
fun newInstance(chatRoomId: String): Fragment {
return FavoriteMessagesFragment().apply {
arguments = Bundle(1).apply {
putString(INTENT_CHAT_ROOM_ID, chatRoomId)
}
}
}
private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
private lateinit var chatRoomId: String
private lateinit var adapter: ChatRoomAdapter
@Inject
lateinit var presenter: FavoriteMessagesPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(INTENT_CHAT_ROOM_ID)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_favorite_messages)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
presenter.loadFavoriteMessages(chatRoomId)
}
override fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>) {
ui {
if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(enableActions = false)
recycler_view.adapter = adapter
val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view.layoutManager = linearLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator()
if (favoriteMessages.size > 10) {
recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView?
) {
presenter.loadFavoriteMessages(chatRoomId)
}
})
}
no_messages_view.isVisible = favoriteMessages.isEmpty()
}
adapter.appendData(favoriteMessages)
}
}
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))
override fun showLoading() {
ui { view_loading.isVisible = true }
}
override fun hideLoading() {
ui { view_loading.isVisible = false }
}
private fun setupToolbar() {
(activity as ChatRoomActivity).let {
it.showToolbarTitle(getString(R.string.title_favorite_messages))
it.hideToolbarChatRoomIcon()
}
}
}
\ No newline at end of file
package chat.rocket.android.files.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.files.viewmodel.FileViewModel
import chat.rocket.android.util.extensions.inflate
import kotlinx.android.synthetic.main.item_generic_attachment.view.*
class FilesAdapter(private val listener: (FileViewModel) -> Unit) :
RecyclerView.Adapter<FilesAdapter.ViewHolder>() {
private var dataSet: List<FileViewModel> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FilesAdapter.ViewHolder =
ViewHolder(parent.inflate(R.layout.item_generic_attachment))
override fun onBindViewHolder(holder: FilesAdapter.ViewHolder, position: Int) =
holder.bind(dataSet[position], listener)
override fun getItemCount(): Int = dataSet.size
fun prependData(dataSet: List<FileViewModel>) {
this.dataSet = dataSet
notifyItemRangeInserted(0, dataSet.size)
}
fun appendData(dataSet: List<FileViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet += dataSet
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(fileViewModel: FileViewModel, listener: (FileViewModel) -> Unit) {
with(itemView) {
when {
fileViewModel.isImage -> {
image_file_thumbnail.setImageURI(fileViewModel.url)
image_file_media_thumbnail.isVisible = false
image_file_thumbnail.isVisible = true
}
fileViewModel.isMedia -> {
image_file_media_thumbnail.setImageDrawable(
context.resources.getDrawable(
R.drawable.ic_play_arrow_black_24dp, null
)
)
image_file_thumbnail.isVisible = false
image_file_media_thumbnail.isVisible = true
}
else -> {
image_file_media_thumbnail.setImageDrawable(
context.resources.getDrawable(
R.drawable.ic_insert_drive_file_black_24dp, null
)
)
image_file_thumbnail.isVisible = false
image_file_media_thumbnail.isVisible = true
}
}
text_file_name.text = fileViewModel.name
text_uploader.text = fileViewModel.uploader
text_upload_date.text = fileViewModel.uploadDate
setOnClickListener { listener(fileViewModel) }
}
}
}
}
\ No newline at end of file
package chat.rocket.android.files.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.files.presentation.FilesView
import chat.rocket.android.files.ui.FilesFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class FilesFragmentModule {
@Provides
fun provideLifecycleOwner(frag: FilesFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
@Provides
fun provideFilesView(frag: FilesFragment): FilesView {
return frag
}
}
\ No newline at end of file
package chat.rocket.android.files.di
import chat.rocket.android.files.ui.FilesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class FilesFragmentProvider {
@ContributesAndroidInjector(modules = [FilesFragmentModule::class])
abstract fun provideFilesFragment(): FilesFragment
}
\ No newline at end of file
package chat.rocket.android.files.viewmodel
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.baseUrl
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.GenericAttachment
import javax.inject.Inject
class FileViewModelMapper @Inject constructor(
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor,
private val tokenRepository: TokenRepository
) {
private var settings: Map<String, Value<Any>> =
getSettingsInteractor.get(serverInteractor.get()!!)
private val baseUrl = settings.baseUrl()
fun mapToViewModelList(fileList: List<GenericAttachment>): List<FileViewModel> {
return fileList.map { FileViewModel(it, settings, tokenRepository, baseUrl) }
}
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -2,7 +2,6 @@ package chat.rocket.android.main.viewmodel ...@@ -2,7 +2,6 @@ package chat.rocket.android.main.viewmodel
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
data class NavHeaderViewModel( data class NavHeaderViewModel(
val userDisplayName: String?, val userDisplayName: String?,
val userStatus: UserStatus?, val userStatus: UserStatus?,
......
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.
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.
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