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

Merge pull request #1314 from RocketChat/beta

[RELEASE] Merge beta in master
parents 5e5e565d 9da37707
...@@ -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 2021 versionCode 2024
versionName "2.2.0" versionName "2.3.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
} }
...@@ -26,12 +26,19 @@ android { ...@@ -26,12 +26,19 @@ android {
keyAlias System.getenv("KEY_ALIAS") keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD") keyPassword System.getenv("KEY_PASSWORD")
} }
debug {
storeFile project.rootProject.file('debug.keystore').getCanonicalFile()
storePassword "android"
keyAlias "androiddebugkey"
keyPassword "android"
}
} }
buildTypes { buildTypes {
release { release {
buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"' buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"'
buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.63.0"' buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.64.2"'
signingConfig signingConfigs.release signingConfig signingConfigs.release
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
...@@ -39,7 +46,8 @@ android { ...@@ -39,7 +46,8 @@ android {
debug { debug {
buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"' buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"'
buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.63.0"' buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.64.2"'
signingConfig signingConfigs.debug
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
} }
} }
...@@ -73,7 +81,8 @@ dependencies { ...@@ -73,7 +81,8 @@ dependencies {
kapt libraries.daggerProcessor kapt libraries.daggerProcessor
kapt libraries.daggerAndroidApt kapt libraries.daggerAndroidApt
implementation libraries.playServicesGcm implementation libraries.fcm
implementation libraries.playServicesAuth
implementation libraries.room implementation libraries.room
kapt libraries.roomProcessor kapt libraries.roomProcessor
......
This diff is collapsed.
...@@ -3,25 +3,17 @@ ...@@ -3,25 +3,17 @@
package="chat.rocket.android"> package="chat.rocket.android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<application <application
android:name=".app.RocketChatApplication" android:name=".app.RocketChatApplication"
android:allowBackup="true" android:allowBackup="true"
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
...@@ -39,8 +31,10 @@ ...@@ -39,8 +31,10 @@
</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" />
...@@ -90,18 +84,6 @@ ...@@ -90,18 +84,6 @@
android:name=".settings.about.ui.AboutActivity" android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
<receiver <receiver
android:name=".push.DirectReplyReceiver" android:name=".push.DirectReplyReceiver"
android:enabled="true" android:enabled="true"
...@@ -118,13 +100,16 @@ ...@@ -118,13 +100,16 @@
<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.FirebaseMessagingService"
android:exported="false"> android:enabled="true"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" /> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<service <service
android:name=".chatroom.service.MessageService" android:name=".chatroom.service.MessageService"
android:exported="true" android:exported="true"
......
...@@ -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 {
......
...@@ -17,6 +17,7 @@ import chat.rocket.common.model.Token ...@@ -17,6 +17,7 @@ import chat.rocket.common.model.Token
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.* import chat.rocket.core.internal.rest.*
import com.google.android.gms.auth.api.credentials.Credential
import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.delay
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
...@@ -149,13 +150,20 @@ class LoginPresenter @Inject constructor( ...@@ -149,13 +150,20 @@ class LoginPresenter @Inject constructor(
client.settingsOauth().services client.settingsOauth().services
} }
if (services.isNotEmpty()) { if (services.isNotEmpty()) {
val state = "{\"loginStyle\":\"popup\",\"credentialToken\":\"${generateRandomString(40)}\",\"isCordova\":true}".encodeToBase64() val state =
"{\"loginStyle\":\"popup\",\"credentialToken\":\"${generateRandomString(40)}\",\"isCordova\":true}".encodeToBase64()
var totalSocialAccountsEnabled = 0 var totalSocialAccountsEnabled = 0
if (settings.isFacebookAuthenticationEnabled()) { if (settings.isFacebookAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_FACEBOOK) val clientId = getOauthClientId(services, SERVICE_NAME_FACEBOOK)
if (clientId != null) { if (clientId != null) {
view.setupFacebookButtonListener(OauthHelper.getFacebookOauthUrl(clientId, currentServer, state), state) view.setupFacebookButtonListener(
OauthHelper.getFacebookOauthUrl(
clientId,
currentServer,
state
), state
)
view.enableLoginByFacebook() view.enableLoginByFacebook()
totalSocialAccountsEnabled++ totalSocialAccountsEnabled++
} }
...@@ -163,7 +171,12 @@ class LoginPresenter @Inject constructor( ...@@ -163,7 +171,12 @@ class LoginPresenter @Inject constructor(
if (settings.isGithubAuthenticationEnabled()) { if (settings.isGithubAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_GITHUB) val clientId = getOauthClientId(services, SERVICE_NAME_GITHUB)
if (clientId != null) { if (clientId != null) {
view.setupGithubButtonListener(OauthHelper.getGithubOauthUrl(clientId, state), state) view.setupGithubButtonListener(
OauthHelper.getGithubOauthUrl(
clientId,
state
), state
)
view.enableLoginByGithub() view.enableLoginByGithub()
totalSocialAccountsEnabled++ totalSocialAccountsEnabled++
} }
...@@ -171,7 +184,13 @@ class LoginPresenter @Inject constructor( ...@@ -171,7 +184,13 @@ class LoginPresenter @Inject constructor(
if (settings.isGoogleAuthenticationEnabled()) { if (settings.isGoogleAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_GOOGLE) val clientId = getOauthClientId(services, SERVICE_NAME_GOOGLE)
if (clientId != null) { if (clientId != null) {
view.setupGoogleButtonListener(OauthHelper.getGoogleOauthUrl(clientId, currentServer, state), state) view.setupGoogleButtonListener(
OauthHelper.getGoogleOauthUrl(
clientId,
currentServer,
state
), state
)
view.enableLoginByGoogle() view.enableLoginByGoogle()
totalSocialAccountsEnabled++ totalSocialAccountsEnabled++
} }
...@@ -179,7 +198,13 @@ class LoginPresenter @Inject constructor( ...@@ -179,7 +198,13 @@ class LoginPresenter @Inject constructor(
if (settings.isLinkedinAuthenticationEnabled()) { if (settings.isLinkedinAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_LINKEDIN) val clientId = getOauthClientId(services, SERVICE_NAME_LINKEDIN)
if (clientId != null) { if (clientId != null) {
view.setupLinkedinButtonListener(OauthHelper.getLinkedinOauthUrl(clientId, currentServer, state), state) view.setupLinkedinButtonListener(
OauthHelper.getLinkedinOauthUrl(
clientId,
currentServer,
state
), state
)
view.enableLoginByLinkedin() view.enableLoginByLinkedin()
totalSocialAccountsEnabled++ totalSocialAccountsEnabled++
} }
...@@ -303,6 +328,9 @@ class LoginPresenter @Inject constructor( ...@@ -303,6 +328,9 @@ class LoginPresenter @Inject constructor(
saveAccount(username) saveAccount(username)
saveToken(token) saveToken(token)
registerPushToken() registerPushToken()
if (loginType == TYPE_LOGIN_USER_EMAIL) {
view.saveSmartLockCredentials(usernameOrEmail, password)
}
navigator.toChatList() navigator.toChatList()
} else if (loginType == TYPE_LOGIN_OAUTH) { } else if (loginType == TYPE_LOGIN_OAUTH) {
navigator.toRegisterUsername(token.userId, token.authToken) navigator.toRegisterUsername(token.userId, token.authToken)
...@@ -333,7 +361,7 @@ class LoginPresenter @Inject constructor( ...@@ -333,7 +361,7 @@ class LoginPresenter @Inject constructor(
}.toString() }.toString()
} }
private fun getCustomOauthServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> { private fun getCustomOauthServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> {
return listMap.filter { map -> map["custom"] == true } return listMap.filter { map -> map["custom"] == true }
} }
......
...@@ -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 {
...@@ -223,4 +224,9 @@ interface LoginView : LoadingView, MessageView { ...@@ -223,4 +224,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()
/**
* Saves Google Smart Lock credentials.
*/
fun saveSmartLockCredentials(id: String, password: String)
} }
\ No newline at end of file
...@@ -22,8 +22,7 @@ import chat.rocket.android.R ...@@ -22,8 +22,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.presentation.LoginPresenter import chat.rocket.android.authentication.login.presentation.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.*
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.webview.cas.ui.INTENT_CAS_TOKEN import chat.rocket.android.webview.cas.ui.INTENT_CAS_TOKEN
import chat.rocket.android.webview.cas.ui.casWebViewIntent import chat.rocket.android.webview.cas.ui.casWebViewIntent
...@@ -31,12 +30,13 @@ import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_SECRET ...@@ -31,12 +30,13 @@ import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_SECRET
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN
import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import com.google.android.gms.auth.api.credentials.*
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.* import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject import javax.inject.Inject
internal const val REQUEST_CODE_FOR_CAS = 1 internal const val REQUEST_CODE_FOR_CAS = 4
internal const val REQUEST_CODE_FOR_OAUTH = 2 internal const val REQUEST_CODE_FOR_OAUTH = 5
class LoginFragment : Fragment(), LoginView { class LoginFragment : Fragment(), LoginView {
@Inject @Inject
...@@ -47,6 +47,7 @@ class LoginFragment : Fragment(), LoginView { ...@@ -47,6 +47,7 @@ class LoginFragment : Fragment(), LoginView {
} }
private var isGlobalLayoutListenerSetUp = false private var isGlobalLayoutListenerSetUp = false
private var deepLinkInfo: LoginDeepLinkInfo? = null private var deepLinkInfo: LoginDeepLinkInfo? = null
private val credentialsClient by lazy { Credentials.getClient(requireActivity()) }
companion object { companion object {
private const val DEEP_LINK_INFO = "DeepLinkInfo" private const val DEEP_LINK_INFO = "DeepLinkInfo"
...@@ -95,21 +96,42 @@ class LoginFragment : Fragment(), LoginView { ...@@ -95,21 +96,42 @@ class LoginFragment : Fragment(), LoginView {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_FOR_CAS) { if (data != null) {
data?.apply { when (requestCode) {
presenter.authenticateWithCas(getStringExtra(INTENT_CAS_TOKEN)) REQUEST_CODE_FOR_MULTIPLE_ACCOUNTS_RESOLUTION -> {
} onCredentialRetrieved(data.getParcelableExtra(Credential.EXTRA_KEY))
} else if (requestCode == REQUEST_CODE_FOR_OAUTH) { }
data?.apply { REQUEST_CODE_FOR_SIGN_IN_REQUIRED -> {
presenter.authenticateWithOauth( //use the hints to autofill sign in forms to reduce the info to be filled.
getStringExtra(INTENT_OAUTH_CREDENTIAL_TOKEN), val credential: Credential = data.getParcelableExtra(Credential.EXTRA_KEY)
getStringExtra(INTENT_OAUTH_CREDENTIAL_SECRET) text_username_or_email.setText(credential.id)
) text_password.setText(credential.password)
}
REQUEST_CODE_FOR_SAVE_RESOLUTION -> {
showMessage(getString(R.string.message_credentials_saved_successfully))
}
REQUEST_CODE_FOR_CAS -> {
presenter.authenticateWithCas(data.getStringExtra(INTENT_CAS_TOKEN))
}
REQUEST_CODE_FOR_OAUTH -> {
presenter.authenticateWithOauth(
data.getStringExtra(INTENT_OAUTH_CREDENTIAL_TOKEN),
data.getStringExtra(INTENT_OAUTH_CREDENTIAL_SECRET)
)
}
} }
} }
} }
} }
override fun onResume() {
super.onResume()
image_key.setOnClickListener {
requestStoredCredentials()
image_key.isVisible = false
}
}
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
ui { ui {
val personDrawable = val personDrawable =
...@@ -126,6 +148,24 @@ class LoginFragment : Fragment(), LoginView { ...@@ -126,6 +148,24 @@ class LoginFragment : Fragment(), LoginView {
} }
} }
private fun requestStoredCredentials() {
activity?.let {
SmartLockHelper.requestStoredCredentials(credentialsClient, it)?.let {
onCredentialRetrieved(it)
}
}
}
private fun onCredentialRetrieved(credential: Credential) {
presenter.authenticateWithUserAndPassword(credential.id, credential.password.toString())
}
override fun saveSmartLockCredentials(id: String, password: String) {
activity?.let {
SmartLockHelper.save(credentialsClient, it, id, password)
}
}
override fun showLoading() { override fun showLoading() {
ui { ui {
view_loading.isVisible = true view_loading.isVisible = true
...@@ -158,6 +198,7 @@ class LoginFragment : Fragment(), LoginView { ...@@ -158,6 +198,7 @@ class LoginFragment : Fragment(), LoginView {
ui { ui {
text_username_or_email.isVisible = true text_username_or_email.isVisible = true
text_password.isVisible = true text_password.isVisible = true
image_key.isVisible = true
} }
} }
......
...@@ -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)
} }
......
...@@ -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
...@@ -22,15 +17,17 @@ import chat.rocket.core.internal.rest.signup ...@@ -22,15 +17,17 @@ import chat.rocket.core.internal.rest.signup
import chat.rocket.core.model.Myself import chat.rocket.core.model.Myself
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 +63,7 @@ class SignupPresenter @Inject constructor(private val view: SignupView, ...@@ -66,6 +63,7 @@ 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()
view.saveSmartLockCredentials(username, password)
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()
/**
* Saves Google Smart Lock credentials.
*/
fun saveSmartLockCredentials(id: String, password: String)
} }
\ 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
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.widget.Toast import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.R.string.message_credentials_saved_successfully
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.SmartLockHelper
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.credentials.Credentials
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 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 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 +50,11 @@ class SignupFragment : Fragment(), SignupView { ...@@ -40,7 +50,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 +68,12 @@ class SignupFragment : Fragment(), SignupView { ...@@ -54,7 +68,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 +114,16 @@ class SignupFragment : Fragment(), SignupView { ...@@ -95,6 +114,16 @@ class SignupFragment : Fragment(), SignupView {
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
if (data != null) {
if (requestCode == SAVE_CREDENTIALS) {
showMessage(getString(message_credentials_saved_successfully))
}
}
}
}
override fun showLoading() { override fun showLoading() {
ui { ui {
enableUserInput(false) enableUserInput(false)
...@@ -125,9 +154,16 @@ class SignupFragment : Fragment(), SignupView { ...@@ -125,9 +154,16 @@ class SignupFragment : Fragment(), SignupView {
showMessage(getString(R.string.msg_generic_error)) showMessage(getString(R.string.msg_generic_error))
} }
override fun saveSmartLockCredentials(id: String, password: String) {
activity?.let {
SmartLockHelper.save(Credentials.getClient(it), it, id, password)
}
}
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 +171,22 @@ class SignupFragment : Fragment(), SignupView { ...@@ -135,14 +171,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 +202,11 @@ class SignupFragment : Fragment(), SignupView { ...@@ -158,7 +202,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()
......
...@@ -5,21 +5,7 @@ import android.view.MenuItem ...@@ -5,21 +5,7 @@ import android.view.MenuItem
import android.view.ViewGroup import android.view.ViewGroup
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.ui.chatRoomIntent import chat.rocket.android.chatroom.viewmodel.*
import chat.rocket.android.chatroom.viewmodel.AudioAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.AuthorAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.BaseFileAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.ColorAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.GenericFileAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.MessageReplyViewModel
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel
import chat.rocket.android.chatroom.viewmodel.VideoAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.toViewType
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.widget.emoji.EmojiReactionListener import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
...@@ -28,13 +14,12 @@ import timber.log.Timber ...@@ -28,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 {
...@@ -178,7 +163,7 @@ class ChatRoomAdapter( ...@@ -178,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) {
...@@ -219,10 +204,14 @@ class ChatRoomAdapter( ...@@ -219,10 +204,14 @@ class ChatRoomAdapter(
message.apply { message.apply {
when (item.itemId) { when (item.itemId) {
R.id.action_message_reply -> { R.id.action_message_reply -> {
presenter?.citeMessage(roomName, roomType, id, true) if (roomName != null && roomType != null) {
presenter?.citeMessage(roomName, roomType, id, true)
}
} }
R.id.action_message_quote -> { R.id.action_message_quote -> {
presenter?.citeMessage(roomName, roomType, id, false) if (roomName != null && roomType != null) {
presenter?.citeMessage(roomName, roomType, id, false)
}
} }
R.id.action_message_copy -> { R.id.action_message_copy -> {
presenter?.copyMessage(id) presenter?.copyMessage(id)
......
...@@ -3,6 +3,7 @@ package chat.rocket.android.chatroom.adapter ...@@ -3,6 +3,7 @@ package chat.rocket.android.chatroom.adapter
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import androidx.core.net.toUri
import chat.rocket.android.chatroom.viewmodel.GenericFileAttachmentViewModel import chat.rocket.android.chatroom.viewmodel.GenericFileAttachmentViewModel
import chat.rocket.android.util.extensions.content import chat.rocket.android.util.extensions.content
import chat.rocket.android.widget.emoji.EmojiReactionListener import chat.rocket.android.widget.emoji.EmojiReactionListener
...@@ -25,7 +26,7 @@ class GenericFileAttachmentViewHolder(itemView: View, ...@@ -25,7 +26,7 @@ class GenericFileAttachmentViewHolder(itemView: View,
text_file_name.content = data.attachmentTitle text_file_name.content = data.attachmentTitle
text_file_name.setOnClickListener { text_file_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.attachmentUrl))) it.context.startActivity(Intent(Intent.ACTION_VIEW, data.attachmentUrl.toUri()))
} }
} }
} }
......
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import android.Manifest
import android.app.Activity
import android.graphics.Color
import android.graphics.Typeface
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Environment
import android.support.design.widget.AppBarLayout
import android.support.v7.widget.Toolbar
import android.text.TextUtils
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.Gravity
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.setPadding
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import chat.rocket.android.helper.AndroidPermissionsHelper import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.widget.emoji.EmojiReactionListener import chat.rocket.android.widget.emoji.EmojiReactionListener
import com.facebook.binaryresource.FileBinaryResource
import com.facebook.cache.common.CacheKey
import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imageformat.ImageFormatChecker
import com.facebook.imagepipeline.cache.DefaultCacheKeyFactory
import com.facebook.imagepipeline.core.ImagePipelineFactory
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.* import kotlinx.android.synthetic.main.message_attachment.view.*
import timber.log.Timber
import java.io.File
class ImageAttachmentViewHolder( class ImageAttachmentViewHolder(
itemView: View, itemView: View,
listener: ActionsListener, listener: ActionsListener,
reactionListener: EmojiReactionListener? = null reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) { ) : BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
private var cacheKey: CacheKey? = null
init { init {
with(itemView) { with(itemView) {
...@@ -59,138 +28,13 @@ class ImageAttachmentViewHolder( ...@@ -59,138 +28,13 @@ class ImageAttachmentViewHolder(
}.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(
// TODO - We should definitely write our own ImageViewer
var imageViewer: ImageViewer? = null
val request =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(data.attachmentUrl))
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.DISK_CACHE)
.build()
cacheKey = DefaultCacheKeyFactory.getInstance()
.getEncodedCacheKey(request, null)
val pad = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_padding)
val lparams = AppBarLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val toolbar = Toolbar(context).also {
it.inflateMenu(R.menu.image_actions)
it.overflowIcon?.setTint(Color.WHITE)
it.setOnMenuItemClickListener {
return@setOnMenuItemClickListener when (it.itemId) {
R.id.action_save_image -> saveImage()
else -> super.onMenuItemClick(it)
}
}
val titleSize = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_title)
val titleTextView = TextView(context).also {
it.text = data.attachmentTitle
it.setTextColor(Color.WHITE)
it.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize.toFloat())
it.ellipsize = TextUtils.TruncateAt.END
it.setSingleLine()
it.typeface = Typeface.DEFAULT_BOLD
it.setPadding(pad)
}
val backArrowView = ImageView(context).also {
it.setImageResource(R.drawable.ic_arrow_back_white_24dp)
it.setOnClickListener { imageViewer?.onDismiss() }
it.setPadding(0, pad, pad, pad)
}
val layoutParams = AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.WRAP_CONTENT,
AppBarLayout.LayoutParams.WRAP_CONTENT
)
it.addView(backArrowView, layoutParams)
it.addView(titleTextView, layoutParams)
}
val appBarLayout = AppBarLayout(context).also {
it.layoutParams = lparams
it.setBackgroundColor(Color.BLACK)
it.addView(
toolbar, AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
)
}
val builder = ImageViewer.createPipelineDraweeControllerBuilder()
.setImageRequest(request)
.setAutoPlayAnimations(true)
imageViewer = ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
.setOverlayView(appBarLayout)
.setStartPosition(0)
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
.show()
}
}
}
private fun saveImage(): Boolean {
if (!canWriteToExternalStorage()) {
checkWritingPermission()
return false
}
if (ImagePipelineFactory.getInstance().mainFileCache.hasKey(cacheKey)) {
val context = itemView.context
val resource = ImagePipelineFactory.getInstance().mainFileCache.getResource(cacheKey)
val cachedFile = (resource as FileBinaryResource).file
val imageFormat = ImageFormatChecker.getImageFormat(resource.openStream())
val imageDir = "${Environment.DIRECTORY_PICTURES}/Rocket.Chat Images/"
val imagePath = Environment.getExternalStoragePublicDirectory(imageDir)
val imageFile =
File(imagePath, "${cachedFile.nameWithoutExtension}.${imageFormat.fileExtension}")
imagePath.mkdirs()
imageFile.createNewFile()
try {
cachedFile.copyTo(imageFile, true)
MediaScannerConnection.scanFile(
context, context,
arrayOf(imageFile.absolutePath), data.attachmentUrl,
null data.attachmentTitle.toString()
) { path, uri -> )
Timber.i("Scanned $path:")
Timber.i("-> uri=$uri")
}
} catch (ex: Exception) {
Timber.e(ex)
val message = context.getString(R.string.msg_image_saved_failed)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} finally {
val message = context.getString(R.string.msg_image_saved_successfully)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} }
} }
return true
}
private fun canWriteToExternalStorage(): Boolean {
return AndroidPermissionsHelper.checkPermission(
itemView.context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
private fun checkWritingPermission() {
val context = itemView.context
if (context is ContextThemeWrapper && context.baseContext is Activity) {
val activity = context.baseContext as Activity
AndroidPermissionsHelper.requestPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
AndroidPermissionsHelper.WRITE_EXTERNAL_STORAGE_CODE
)
}
} }
} }
\ No newline at end of file
...@@ -37,7 +37,7 @@ class MessageViewHolder( ...@@ -37,7 +37,7 @@ class MessageViewHolder(
if (data.isTemporary) Color.GRAY else Color.BLACK if (data.isTemporary) Color.GRAY else Color.BLACK
) )
data.message.let { data.message.let {
text_edit_indicator.isVisible = it.isSystemMessage() && it.editedBy != null text_edit_indicator.isVisible = !it.isSystemMessage() && it.editedBy != null
image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false
} }
} }
......
...@@ -3,27 +3,32 @@ package chat.rocket.android.chatroom.presentation ...@@ -3,27 +3,32 @@ 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.chatroom.ui.chatRoomIntent import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.members.ui.newInstance
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, chatRoomType: String) { fun toPinnedMessageList(chatRoomId: String) {
activity.addFragmentBackStack("PinnedMessages", R.id.fragment_container) { activity.addFragmentBackStack("PinnedMessages", R.id.fragment_container) {
chat.rocket.android.pinnedmessages.ui.newInstance(chatRoomId, chatRoomType) chat.rocket.android.pinnedmessages.ui.newInstance(chatRoomId)
} }
} }
fun toFavoriteMessageList(chatRoomId: String, chatRoomType: String) { fun toFavoriteMessageList(chatRoomId: String) {
activity.addFragmentBackStack("FavoriteMessages", R.id.fragment_container) { activity.addFragmentBackStack("FavoriteMessages", R.id.fragment_container) {
chat.rocket.android.favoritemessages.ui.newInstance(chatRoomId, chatRoomType) 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)
} }
} }
......
...@@ -181,9 +181,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -181,9 +181,7 @@ class ChatRoomPresenter @Inject constructor(
} }
subscribeTypingStatus() subscribeTypingStatus()
if (offset == 0L) { subscribeState()
subscribeState()
}
} }
} }
...@@ -229,10 +227,9 @@ class ChatRoomPresenter @Inject constructor( ...@@ -229,10 +227,9 @@ class ChatRoomPresenter @Inject constructor(
) )
try { try {
messagesRepository.save(newMessage) messagesRepository.save(newMessage)
val message = client.sendMessage(id, chatRoomId, text)
view.showNewMessage(mapper.map(newMessage, RoomViewModel( view.showNewMessage(mapper.map(newMessage, RoomViewModel(
roles = chatRoles, isBroadcast = chatIsBroadcast))) roles = chatRoles, isBroadcast = chatIsBroadcast)))
message client.sendMessage(id, chatRoomId, text)
} catch (ex: Exception) { } catch (ex: Exception) {
// Ok, not very beautiful, but the backend sends us a not valid response // Ok, not very beautiful, but the backend sends us a not valid response
// When someone sends a message on a read-only channel, so we just ignore it // When someone sends a message on a read-only channel, so we just ignore it
...@@ -324,7 +321,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -324,7 +321,7 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
private fun subscribeState() { private suspend fun subscribeState() {
Timber.d("Subscribing to Status changes") Timber.d("Subscribing to Status changes")
lastState = manager.state lastState = manager.state
manager.addStatusChannel(stateChannel) manager.addStatusChannel(stateChannel)
...@@ -638,14 +635,17 @@ class ChatRoomPresenter @Inject constructor( ...@@ -638,14 +635,17 @@ class ChatRoomPresenter @Inject constructor(
} }
} }
fun toMembersList(chatRoomId: String, chatRoomType: String) = fun toMembersList(chatRoomId: String) =
navigator.toMembersList(chatRoomId, chatRoomType) navigator.toMembersList(chatRoomId)
fun toPinnedMessageList(chatRoomId: String) =
navigator.toPinnedMessageList(chatRoomId)
fun toPinnedMessageList(chatRoomId: String, chatRoomType: String) = fun toFavoriteMessageList(chatRoomId: String) =
navigator.toPinnedMessageList(chatRoomId, chatRoomType) navigator.toFavoriteMessageList(chatRoomId)
fun toFavoriteMessageList(chatRoomId: String, chatRoomType: String) = fun toFileList(chatRoomId: String) =
navigator.toFavoriteMessageList(chatRoomId, chatRoomType) navigator.toFileList(chatRoomId)
fun loadChatRooms() { fun loadChatRooms() {
launchUI(strategy) { launchUI(strategy) {
...@@ -787,12 +787,14 @@ class ChatRoomPresenter @Inject constructor( ...@@ -787,12 +787,14 @@ class ChatRoomPresenter @Inject constructor(
} }
private suspend fun subscribeTypingStatus() { private suspend fun subscribeTypingStatus() {
client.subscribeTypingStatus(chatRoomId.toString()) { _, id -> launch(CommonPool + strategy.jobs) {
typingStatusSubscriptionId = id client.subscribeTypingStatus(chatRoomId.toString()) { _, id ->
} typingStatusSubscriptionId = id
}
for (typingStatus in client.typingStatusChannel) { for (typingStatus in client.typingStatusChannel) {
processTypingStatus(typingStatus) processTypingStatus(typingStatus)
}
} }
} }
...@@ -834,6 +836,7 @@ class ChatRoomPresenter @Inject constructor( ...@@ -834,6 +836,7 @@ class ChatRoomPresenter @Inject constructor(
launchUI(strategy) { launchUI(strategy) {
val viewModelStreamedMessage = mapper.map(streamedMessage, RoomViewModel( val viewModelStreamedMessage = mapper.map(streamedMessage, RoomViewModel(
roles = chatRoles, isBroadcast = chatIsBroadcast)) roles = chatRoles, isBroadcast = chatIsBroadcast))
val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId) val roomMessages = messagesRepository.getByRoomId(streamedMessage.roomId)
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id } val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index > -1) { if (index > -1) {
......
...@@ -64,7 +64,7 @@ class MessageService : JobService() { ...@@ -64,7 +64,7 @@ class MessageService : JobService() {
Timber.e(ex) Timber.e(ex)
// TODO - remove the generic message when we implement :userId:/message subscription // TODO - remove the generic message when we implement :userId:/message subscription
if (ex is IllegalStateException) { if (ex is IllegalStateException) {
Timber.d(ex, "Probably a read-only problem...") Timber.e(ex, "Probably a read-only problem...")
// TODO: For now we are only going to reschedule when api is fixed. // TODO: For now we are only going to reschedule when api is fixed.
messageRepository.removeById(message.id) messageRepository.removeById(message.id)
jobFinished(params, false) jobFinished(params, false)
......
package chat.rocket.android.chatroom.ui package chat.rocket.android.chatroom.ui
import DrawableHelper
import android.content.Context import android.content.Context
import android.content.Intent 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
...@@ -63,14 +66,6 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -63,14 +66,6 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject @Inject
lateinit var managerFactory: ConnectionManagerFactory lateinit var managerFactory: ConnectionManagerFactory
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private var isChatRoomSubscribed: Boolean = true
private var isChatRoomCreator: Boolean = false
private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this) AndroidInjection.inject(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -85,33 +80,33 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -85,33 +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_CHAT_ROOM_IS_READ_ONLY, true) val isChatRoomReadOnly = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true)
requireNotNull(isChatRoomReadOnly) { "no chat_room_is_read_only provided in Intent extras" }
isChatRoomCreator = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_CREATOR, false) val isChatRoomCreator = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_CREATOR, false)
requireNotNull(isChatRoomCreator) { "no chat_room_is_creator provided in Intent extras" }
val chatRoomMessage = intent.getStringExtra(INTENT_CHAT_ROOM_MESSAGE) val chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
setupToolbar() val isChatRoomSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true)
chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1) val chatRoomMessage = intent.getStringExtra(INTENT_CHAT_ROOM_MESSAGE)
isChatRoomSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true) setupToolbar()
if (supportFragmentManager.findFragmentByTag(TAG_CHAT_ROOM_FRAGMENT) == null) { if (supportFragmentManager.findFragmentByTag(TAG_CHAT_ROOM_FRAGMENT) == null) {
addFragment(TAG_CHAT_ROOM_FRAGMENT, R.id.fragment_container) { addFragment(TAG_CHAT_ROOM_FRAGMENT, R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen, newInstance(
isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage) chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage
)
} }
} }
} }
...@@ -128,43 +123,34 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -128,43 +123,34 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp) toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
text_room_name.textContent = chatRoomName
showRoomTypeIcon(true)
toolbar.setNavigationOnClickListener { finishActivity() } toolbar.setNavigationOnClickListener { finishActivity() }
} }
fun showRoomTypeIcon(showRoomTypeIcon: Boolean) { fun showToolbarTitle(title: String) {
if (showRoomTypeIcon) { text_room_name.textContent = title
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 { fun showToolbarChatRoomIcon(chatRoomType: String) {
val wrappedDrawable = DrawableHelper.wrapDrawable(it) val drawable = when (roomTypeOf(chatRoomType)) {
val mutableDrawable = wrappedDrawable.mutate() is RoomType.Channel -> {
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.white) DrawableHelper.getDrawableFromId(R.drawable.ic_hashtag_black_12dp, this)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
} }
} else { is RoomType.PrivateGroup -> {
text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0) DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_12_dp, this)
}
else -> null
} }
}
drawable?.let {
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() {
......
...@@ -13,22 +13,12 @@ import android.support.v4.app.Fragment ...@@ -13,22 +13,12 @@ import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.view.*
import androidx.core.text.bold import androidx.core.text.bold
import androidx.core.view.isVisible import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter import chat.rocket.android.chatroom.adapter.*
import chat.rocket.android.chatroom.adapter.CommandSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.PeopleSuggestionsAdapter
import chat.rocket.android.chatroom.adapter.RoomSuggestionsAdapter
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.BaseViewModel import chat.rocket.android.chatroom.viewmodel.BaseViewModel
...@@ -39,26 +29,10 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewMod ...@@ -39,26 +29,10 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewMod
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.asObservable import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.circularRevealOrUnreveal import chat.rocket.android.widget.emoji.*
import chat.rocket.android.util.extensions.fadeIn import chat.rocket.common.model.RoomType
import chat.rocket.android.util.extensions.fadeOut import chat.rocket.common.model.roomTypeOf
import chat.rocket.android.util.extensions.hideKeyboard
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.isAtBottom
import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.widget.emoji.ComposerEditText
import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiKeyboardListener
import chat.rocket.android.widget.emoji.EmojiKeyboardPopup
import chat.rocket.android.widget.emoji.EmojiListenerAdapter
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiPickerPopup
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
...@@ -187,8 +161,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -187,8 +161,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
setupFab() setupFab()
setupSuggestionsView() setupSuggestionsView()
setupActionSnackbar() setupActionSnackbar()
activity?.apply { (activity as ChatRoomActivity).let {
(this as? ChatRoomActivity)?.showRoomTypeIcon(true) it.showToolbarTitle(chatRoomName)
it.showToolbarChatRoomIcon(chatRoomType)
} }
} }
...@@ -230,13 +205,16 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -230,13 +205,16 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_members_list -> { R.id.action_members_list -> {
presenter.toMembersList(chatRoomId, chatRoomType) presenter.toMembersList(chatRoomId)
} }
R.id.action_pinned_messages -> { R.id.action_pinned_messages -> {
presenter.toPinnedMessageList(chatRoomId, chatRoomType) presenter.toPinnedMessageList(chatRoomId)
} }
R.id.action_favorite_messages -> { R.id.action_favorite_messages -> {
presenter.toFavoriteMessageList(chatRoomId, chatRoomType) presenter.toFavoriteMessageList(chatRoomId)
}
R.id.action_files -> {
presenter.toFileList(chatRoomId)
} }
} }
return true return true
...@@ -267,7 +245,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -267,7 +245,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (recycler_view.adapter == null) { if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter( adapter = ChatRoomAdapter(
chatRoomType, chatRoomName, presenter, chatRoomType,
chatRoomName,
presenter,
reactionListener = this@ChatRoomFragment reactionListener = this@ChatRoomFragment
) )
recycler_view.adapter = adapter recycler_view.adapter = adapter
...@@ -289,12 +269,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -289,12 +269,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean) { override fun onRoomUpdated(
userCanPost: Boolean,
channelIsBroadcast: Boolean,
userCanMod: Boolean
) {
// TODO: We should rely solely on the user being able to post, but we cannot guarantee // TODO: We should rely solely on the user being able to post, but we cannot guarantee
// that the "(channels|groups).roles" endpoint is supported by the server in use. // that the "(channels|groups).roles" endpoint is supported by the server in use.
setupMessageComposer(userCanPost) ui {
isBroadcastChannel = channelIsBroadcast setupMessageComposer(userCanPost)
if (isBroadcastChannel && !userCanMod) activity?.invalidateOptionsMenu() isBroadcastChannel = channelIsBroadcast
if (isBroadcastChannel && !userCanMod) activity?.invalidateOptionsMenu()
}
} }
override fun openDirectMessage(chatRoom: ChatRoom, permalink: String) { override fun openDirectMessage(chatRoom: ChatRoom, permalink: String) {
...@@ -613,15 +599,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -613,15 +599,15 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
handler.postDelayed(dismissStatus, 2000) handler.postDelayed(dismissStatus, 2000)
} }
is State.Disconnected -> connection_status_text.text = is State.Disconnected -> connection_status_text.text =
getString(R.string.status_disconnected) getString(R.string.status_disconnected)
is State.Connecting -> connection_status_text.text = is State.Connecting -> connection_status_text.text =
getString(R.string.status_connecting) getString(R.string.status_connecting)
is State.Authenticating -> connection_status_text.text = is State.Authenticating -> connection_status_text.text =
getString(R.string.status_authenticating) getString(R.string.status_authenticating)
is State.Disconnecting -> connection_status_text.text = is State.Disconnecting -> connection_status_text.text =
getString(R.string.status_disconnecting) getString(R.string.status_disconnecting)
is State.Waiting -> connection_status_text.text = is State.Waiting -> connection_status_text.text =
getString(R.string.status_waiting, state.seconds) getString(R.string.status_waiting, state.seconds)
} }
} }
} }
...@@ -666,19 +652,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -666,19 +652,18 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (isChatRoomReadOnly && !canPost) { if (isChatRoomReadOnly && !canPost) {
text_room_is_read_only.setVisible(true) text_room_is_read_only.setVisible(true)
input_container.setVisible(false) input_container.setVisible(false)
} else if (!isSubscribed) { } else if (!isSubscribed && roomTypeOf(chatRoomType) != RoomType.DIRECT_MESSAGE) {
input_container.setVisible(false) input_container.setVisible(false)
button_join_chat.setVisible(true) button_join_chat.setVisible(true)
button_join_chat.setOnClickListener { presenter.joinChat(chatRoomId) } button_join_chat.setOnClickListener { presenter.joinChat(chatRoomId) }
} else { } else {
button_send.alpha = 0f
button_send.setVisible(false) button_send.setVisible(false)
button_show_attachment_options.alpha = 1f button_show_attachment_options.alpha = 1f
button_show_attachment_options.setVisible(true) button_show_attachment_options.setVisible(true)
subscribeComposeTextMessage() subscribeComposeTextMessage()
emojiKeyboardPopup = emojiKeyboardPopup =
EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container)) EmojiKeyboardPopup(activity!!, activity!!.findViewById(R.id.fragment_container))
emojiKeyboardPopup.listener = this emojiKeyboardPopup.listener = this
text_message.listener = object : ComposerEditText.ComposerEditTextListener { text_message.listener = object : ComposerEditText.ComposerEditTextListener {
override fun onKeyboardOpened() { override fun onKeyboardOpened() {
...@@ -807,14 +792,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -807,14 +792,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private fun setupComposeButtons(charSequence: CharSequence) { private fun setupComposeButtons(charSequence: CharSequence) {
if (charSequence.isNotEmpty() && playComposeMessageButtonsAnimation) { if (charSequence.isNotEmpty() && playComposeMessageButtonsAnimation) {
button_show_attachment_options.fadeOut(1F, 0F, 120) button_show_attachment_options.setVisible(false)
button_send.fadeIn(0F, 1F, 120) button_send.setVisible(true)
playComposeMessageButtonsAnimation = false playComposeMessageButtonsAnimation = false
} }
if (charSequence.isEmpty()) { if (charSequence.isEmpty()) {
button_send.fadeOut(1F, 0F, 120) button_send.setVisible(false)
button_show_attachment_options.fadeIn(0F, 1F, 120) button_show_attachment_options.setVisible(true)
playComposeMessageButtonsAnimation = true playComposeMessageButtonsAnimation = true
} }
} }
...@@ -844,6 +829,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -844,6 +829,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
private fun setupToolbar(toolbarTitle: String) { private fun setupToolbar(toolbarTitle: String) {
(activity as ChatRoomActivity).setupToolbarTitle(toolbarTitle) (activity as ChatRoomActivity).showToolbarTitle(toolbarTitle)
} }
} }
\ No newline at end of file
...@@ -26,7 +26,7 @@ class ActionListAdapter( ...@@ -26,7 +26,7 @@ class ActionListAdapter(
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_message_delete) { val color = if (item.itemId == R.id.action_message_delete) {
deleteTextColor deleteTextColor
} else { } else {
......
...@@ -4,19 +4,19 @@ import chat.rocket.android.R ...@@ -4,19 +4,19 @@ import chat.rocket.android.R
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
data class MessageViewModel( data class MessageViewModel(
override val message: Message, override val message: Message,
override val rawData: Message, override val rawData: Message,
override val messageId: String, override val messageId: String,
override val avatar: String, override val avatar: String,
override val time: CharSequence, override val time: CharSequence,
override val senderName: CharSequence, override val senderName: CharSequence,
override val content: CharSequence, override val content: CharSequence,
override val isPinned: Boolean, override val isPinned: Boolean,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null, override var preview: Message? = null,
var isFirstUnread: Boolean, var isFirstUnread: Boolean,
override var isTemporary: Boolean = false override var isTemporary: Boolean = false
) : BaseMessageViewModel<Message> { ) : BaseMessageViewModel<Message> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType get() = BaseViewModel.ViewType.MESSAGE.viewType
......
package chat.rocket.android.chatroom.viewmodel package chat.rocket.android.chatroom.viewmodel
data class ReactionViewModel( data class ReactionViewModel(
val messageId: String, val messageId: String,
val shortname: String, val shortname: String,
val unicode: CharSequence, val unicode: CharSequence,
val count: Int, val count: Int,
val usernames: List<String> = emptyList() val usernames: List<String> = emptyList()
) )
\ No newline at end of file
...@@ -65,70 +65,131 @@ class ViewModelMapper @Inject constructor( ...@@ -65,70 +65,131 @@ class ViewModelMapper @Inject constructor(
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY) private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
private val secondaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText) private val secondaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
suspend fun map(message: Message, roomViewModel: RoomViewModel = RoomViewModel( suspend fun map(
roles = emptyList(), isBroadcast = true)): List<BaseViewModel<*>> { message: Message,
roomViewModel: RoomViewModel = RoomViewModel(roles = emptyList(), isBroadcast = true)
): List<BaseViewModel<*>> {
return translate(message, roomViewModel) return translate(message, roomViewModel)
} }
suspend fun map(messages: List<Message>, roomViewModel: RoomViewModel = RoomViewModel( suspend fun map(
roles = emptyList(), isBroadcast = true)): List<BaseViewModel<*>> = withContext(CommonPool) { messages: List<Message>,
val list = ArrayList<BaseViewModel<*>>(messages.size) roomViewModel: RoomViewModel = RoomViewModel(roles = emptyList(), isBroadcast = true),
asNotReversed: Boolean = false
messages.forEach { ): List<BaseViewModel<*>> =
list.addAll(translate(it, roomViewModel)) withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>(messages.size)
messages.forEach {
list.addAll(
if (asNotReversed) translateAsNotReversed(it, roomViewModel)
else translate(it, roomViewModel)
)
}
return@withContext list
} }
return@withContext list private suspend fun translate(
} message: Message,
roomViewModel: RoomViewModel
): List<BaseViewModel<*>> =
withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>()
private suspend fun translate(message: Message, roomViewModel: RoomViewModel) message.urls?.forEach {
: List<BaseViewModel<*>> = withContext(CommonPool) { val url = mapUrl(message, it)
val list = ArrayList<BaseViewModel<*>>() url?.let { list.add(url) }
}
message.urls?.forEach { message.attachments?.forEach {
val url = mapUrl(message, it) val attachment = mapAttachment(message, it)
url?.let { list.add(url) } attachment?.let { list.add(attachment) }
} }
message.attachments?.forEach { mapMessage(message).let {
val attachment = mapAttachment(message, it) if (list.isNotEmpty()) {
attachment?.let { list.add(attachment) } it.preview = list.first().preview
} }
list.add(it)
}
mapMessage(message).let { for (i in list.size - 1 downTo 0) {
if (list.isNotEmpty()) { val next = if (i - 1 < 0) null else list[i - 1]
it.preview = list.first().preview list[i].nextDownStreamMessage = next
}
if (isBroadcastReplyAvailable(roomViewModel, message)) {
roomsInteractor.getById(currentServer, message.roomId)?.let { chatRoom ->
val replyViewModel = mapMessageReply(message, chatRoom)
list.first().nextDownStreamMessage = replyViewModel
list.add(0, replyViewModel)
}
} }
list.add(it)
}
for (i in list.size - 1 downTo 0) { return@withContext list
val next = if (i - 1 < 0) null else list[i - 1]
list[i].nextDownStreamMessage = next
} }
if (isBroadcastReplyAvailable(roomViewModel, message)) { private suspend fun translateAsNotReversed(
roomsInteractor.getById(currentServer, message.roomId)?.let { chatRoom -> message: Message,
val replyViewModel = mapMessageReply(message, chatRoom) roomViewModel: RoomViewModel
list.first().nextDownStreamMessage = replyViewModel ): List<BaseViewModel<*>> =
list.add(0, replyViewModel) withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>()
mapMessage(message).let {
if (list.isNotEmpty()) {
it.preview = list.first().preview
}
list.add(it)
}
message.attachments?.forEach {
val attachment = mapAttachment(message, it)
attachment?.let {
list.add(attachment)
}
} }
}
return@withContext list message.urls?.forEach {
} val url = mapUrl(message, it)
url?.let {
list.add(url)
}
}
for (i in list.size - 1 downTo 0) {
val next = if (i - 1 < 0) null else list[i - 1]
list[i].nextDownStreamMessage = next
}
if (isBroadcastReplyAvailable(roomViewModel, message)) {
roomsInteractor.getById(currentServer, message.roomId)?.let { chatRoom ->
val replyViewModel = mapMessageReply(message, chatRoom)
list.first().nextDownStreamMessage = replyViewModel
list.add(0, replyViewModel)
}
}
list.dropLast(1).forEach {
it.reactions = emptyList()
}
list.last().reactions = getReactions(message)
list.last().nextDownStreamMessage = null
return@withContext list
}
private fun isBroadcastReplyAvailable(roomViewModel: RoomViewModel, message: Message): Boolean { private fun isBroadcastReplyAvailable(roomViewModel: RoomViewModel, message: Message): Boolean {
val senderUsername = message.sender?.username val senderUsername = message.sender?.username
return roomViewModel.isRoom && roomViewModel.isBroadcast && return roomViewModel.isRoom && roomViewModel.isBroadcast &&
!message.isSystemMessage() && !message.isSystemMessage() &&
senderUsername != currentUsername senderUsername != currentUsername
} }
private fun mapMessageReply(message: Message, chatRoom: ChatRoom): MessageReplyViewModel { private fun mapMessageReply(message: Message, chatRoom: ChatRoom): MessageReplyViewModel {
val name = message.sender?.name val name = message.sender?.name
val roomName = if (settings.useRealName() && name != null) name else message.sender?.username val roomName =
?: "" if (settings.useRealName() && name != null) name else message.sender?.username ?: ""
val permalink = messageHelper.createPermalink(message, chatRoom) val permalink = messageHelper.createPermalink(message, chatRoom)
return MessageReplyViewModel( return MessageReplyViewModel(
messageId = message.id, messageId = message.id,
......
...@@ -34,6 +34,7 @@ import chat.rocket.common.model.SimpleUser ...@@ -34,6 +34,7 @@ import chat.rocket.common.model.SimpleUser
import chat.rocket.common.model.User import chat.rocket.common.model.User
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.model.Subscription import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.createDirectMessage
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.realtime.socket.model.StreamMessage import chat.rocket.core.internal.realtime.socket.model.StreamMessage
import chat.rocket.core.internal.realtime.socket.model.Type import chat.rocket.core.internal.realtime.socket.model.Type
...@@ -124,8 +125,19 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -124,8 +125,19 @@ class ChatRoomsPresenter @Inject constructor(
if (myself?.username == null) { if (myself?.username == null) {
view.showMessage(R.string.msg_generic_error) view.showMessage(R.string.msg_generic_error)
} else { } else {
val id = if (isDirectMessage && !chatRoom.open) {
retryIO("createDirectMessage(${chatRoom.name})") {
client.createDirectMessage(chatRoom.name)
}
val fromTo = mutableListOf(myself.id, chatRoom.id).apply {
sort()
}
fromTo.joinToString("")
} else {
chatRoom.id
}
val isChatRoomOwner = chatRoom.user?.username == myself.username || isDirectMessage val isChatRoomOwner = chatRoom.user?.username == myself.username || isDirectMessage
navigator.toChatRoom(chatRoom.id, roomName, navigator.toChatRoom(id, roomName,
chatRoom.type.toString(), chatRoom.readonly ?: false, chatRoom.type.toString(), chatRoom.readonly ?: false,
chatRoom.lastSeen ?: -1, chatRoom.lastSeen ?: -1,
chatRoom.open, isChatRoomOwner) chatRoom.open, isChatRoomOwner)
...@@ -210,7 +222,7 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -210,7 +222,7 @@ class ChatRoomsPresenter @Inject constructor(
} else { } else {
null null
}, },
name = it.name ?: "", name = it.username ?: it.name ?: "",
fullName = it.name, fullName = it.name,
readonly = false, readonly = false,
updatedAt = null, updatedAt = null,
...@@ -640,4 +652,10 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -640,4 +652,10 @@ class ChatRoomsPresenter @Inject constructor(
manager.removeRoomsAndSubscriptionsChannel(subscriptionsChannel) manager.removeRoomsAndSubscriptionsChannel(subscriptionsChannel)
manager.removeActiveUserChannel(activeUserChannel) manager.removeActiveUserChannel(activeUserChannel)
} }
fun goToChatRoomWithId(chatRoomId: String) {
launchUI(strategy) {
chatRoomsInteractor.getById(currentServer, chatRoomId)?.let { loadChatRoom(it) }
}
}
} }
\ No newline at end of file
...@@ -92,11 +92,11 @@ class ChatRoomsAdapter( ...@@ -92,11 +92,11 @@ class ChatRoomsAdapter(
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(
......
package chat.rocket.android.chatrooms.ui package chat.rocket.android.chatrooms.ui
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
...@@ -11,7 +9,12 @@ import android.support.v7.util.DiffUtil ...@@ -11,7 +9,12 @@ import android.support.v7.util.DiffUtil
import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView import android.support.v7.widget.SearchView
import android.view.* import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.RadioGroup import android.widget.RadioGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
...@@ -24,7 +27,12 @@ import chat.rocket.android.helper.SharedPreferenceHelper ...@@ -24,7 +27,12 @@ import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.widget.DividerItemDecoration import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
...@@ -36,6 +44,8 @@ import kotlinx.coroutines.experimental.NonCancellable.isActive ...@@ -36,6 +44,8 @@ import kotlinx.coroutines.experimental.NonCancellable.isActive
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
private const val BUNDLE_CHAT_ROOM_ID = "BUNDLE_CHAT_ROOM_ID"
class ChatRoomsFragment : Fragment(), ChatRoomsView { class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject @Inject
lateinit var presenter: ChatRoomsPresenter lateinit var presenter: ChatRoomsPresenter
...@@ -50,15 +60,30 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -50,15 +60,30 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
private var listJob: Job? = null private var listJob: Job? = null
private var sectionedAdapter: SimpleSectionedRecyclerViewAdapter? = null private var sectionedAdapter: SimpleSectionedRecyclerViewAdapter? = null
private var chatRoomId: String? = null
companion object { companion object {
fun newInstance() = ChatRoomsFragment() fun newInstance(chatRoomId: String? = null): ChatRoomsFragment {
return ChatRoomsFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
}
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
setHasOptionsMenu(true) setHasOptionsMenu(true)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomId?.let {
presenter.goToChatRoomWithId(it)
chatRoomId = null
}
}
} }
override fun onDestroy() { override fun onDestroy() {
......
...@@ -11,13 +11,14 @@ import chat.rocket.android.authentication.ui.AuthenticationActivity ...@@ -11,13 +11,14 @@ 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.FavoriteMessagesFragmentProvider import chat.rocket.android.chatroom.di.FavoriteMessagesFragmentProvider
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider
import chat.rocket.android.chatroom.ui.ChatRoomActivity import chat.rocket.android.chatroom.ui.ChatRoomActivity
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
...@@ -30,21 +31,25 @@ import dagger.android.ContributesAndroidInjector ...@@ -30,21 +31,25 @@ 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,
ResetPasswordFragmentProvider::class, RegisterUsernameFragmentProvider::class,
SignupFragmentProvider::class, ResetPasswordFragmentProvider::class,
TwoFAFragmentProvider::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
...@@ -54,7 +59,8 @@ abstract class ActivityBuilder { ...@@ -54,7 +59,8 @@ abstract class ActivityBuilder {
ChatRoomFragmentProvider::class, ChatRoomFragmentProvider::class,
MembersFragmentProvider::class, MembersFragmentProvider::class,
PinnedMessagesFragmentProvider::class, PinnedMessagesFragmentProvider::class,
FavoriteMessagesFragmentProvider::class FavoriteMessagesFragmentProvider::class,
FilesFragmentProvider::class
] ]
) )
abstract fun bindChatRoomActivity(): ChatRoomActivity abstract fun bindChatRoomActivity(): ChatRoomActivity
......
...@@ -2,10 +2,10 @@ package chat.rocket.android.dagger.module ...@@ -2,10 +2,10 @@ package chat.rocket.android.dagger.module
import chat.rocket.android.chatroom.di.MessageServiceProvider import chat.rocket.android.chatroom.di.MessageServiceProvider
import chat.rocket.android.chatroom.service.MessageService import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.push.FirebaseMessagingService
import chat.rocket.android.push.FirebaseTokenService import chat.rocket.android.push.FirebaseTokenService
import chat.rocket.android.push.GcmListenerService import chat.rocket.android.push.di.FirebaseMessagingServiceProvider
import chat.rocket.android.push.di.FirebaseTokenServiceProvider import chat.rocket.android.push.di.FirebaseTokenServiceProvider
import chat.rocket.android.push.di.GcmListenerServiceProvider
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
...@@ -14,8 +14,8 @@ import dagger.android.ContributesAndroidInjector ...@@ -14,8 +14,8 @@ import dagger.android.ContributesAndroidInjector
@ContributesAndroidInjector(modules = [FirebaseTokenServiceProvider::class]) @ContributesAndroidInjector(modules = [FirebaseTokenServiceProvider::class])
abstract fun bindFirebaseTokenService(): FirebaseTokenService abstract fun bindFirebaseTokenService(): FirebaseTokenService
@ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class]) @ContributesAndroidInjector(modules = [FirebaseMessagingServiceProvider::class])
abstract fun bindGcmListenerService(): GcmListenerService abstract fun bindGcmListenerService(): FirebaseMessagingService
@ContributesAndroidInjector(modules = [MessageServiceProvider::class]) @ContributesAndroidInjector(modules = [MessageServiceProvider::class])
abstract fun bindMessageService(): MessageService abstract fun bindMessageService(): MessageService
......
...@@ -15,37 +15,36 @@ import javax.inject.Inject ...@@ -15,37 +15,36 @@ import javax.inject.Inject
class FavoriteMessagesPresenter @Inject constructor( class FavoriteMessagesPresenter @Inject constructor(
private val view: FavoriteMessagesView, private val view: FavoriteMessagesView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: ChatRoomsInteractor, private val roomsInteractor: ChatRoomsInteractor,
private val mapper: ViewModelMapper, private val mapper: ViewModelMapper,
factory: RocketChatClientFactory val serverInteractor: GetCurrentServerInteractor,
val factory: RocketChatClientFactory
) { ) {
private val client = factory.create(serverInteractor.get()!!) private val serverUrl = serverInteractor.get()!!
private val client = factory.create(serverUrl)
private var offset: Int = 0 private var offset: Int = 0
/** /**
* Loads all favorite messages for room. the given room id. * Loads all favorite messages for the given room id.
* *
* @param roomId The id of the room to get its favorite messages. * @param roomId The id of the room to get favorite messages from.
*/ */
fun loadFavoriteMessages(roomId: String) { fun loadFavoriteMessages(roomId: String) {
launchUI(strategy) { launchUI(strategy) {
try { try {
val serverUrl = serverInteractor.get()!! view.showLoading()
val chatRoom = roomsInteractor.getById(serverUrl, roomId) roomsInteractor.getById(serverUrl, roomId)?.let {
chatRoom?.let { room -> val favoriteMessages = client.getFavoriteMessages(roomId, it.type, offset)
view.showLoading() val messageList = mapper.map(favoriteMessages.result, asNotReversed = true)
val favoriteMessages =
client.getFavoriteMessages(roomId, room.type, offset)
offset = favoriteMessages.offset.toInt()
val messageList = mapper.map(favoriteMessages.result)
view.showFavoriteMessages(messageList) view.showFavoriteMessages(messageList)
view.hideLoading() offset += 1 * 30
}.ifNull { }.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server.") Timber.e("Couldn't find a room with id: $roomId at current server.")
} }
} catch (e: RocketChatException) { } catch (exception: RocketChatException) {
Timber.e(e) Timber.e(exception)
} finally {
view.hideLoading()
} }
} }
} }
......
...@@ -23,21 +23,18 @@ import dagger.android.support.AndroidSupportInjection ...@@ -23,21 +23,18 @@ import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_favorite_messages.* import kotlinx.android.synthetic.main.fragment_favorite_messages.*
import javax.inject.Inject import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment { fun newInstance(chatRoomId: String): Fragment {
return FavoriteMessagesFragment().apply { return FavoriteMessagesFragment().apply {
arguments = Bundle(1).apply { arguments = Bundle(1).apply {
putString(INTENT_CHAT_ROOM_ID, chatRoomId) putString(INTENT_CHAT_ROOM_ID, chatRoomId)
putString(INTENT_CHAT_ROOM_TYPE, chatRoomType)
} }
} }
} }
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_TYPE = "chat_room_type"
class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView { class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
private lateinit var adapter: ChatRoomAdapter private lateinit var adapter: ChatRoomAdapter
@Inject @Inject
lateinit var presenter: FavoriteMessagesPresenter lateinit var presenter: FavoriteMessagesPresenter
...@@ -49,7 +46,6 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView { ...@@ -49,7 +46,6 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
val bundle = arguments val bundle = arguments
if (bundle != null) { if (bundle != null) {
chatRoomId = bundle.getString(INTENT_CHAT_ROOM_ID) chatRoomId = bundle.getString(INTENT_CHAT_ROOM_ID)
chatRoomType = bundle.getString(INTENT_CHAT_ROOM_TYPE)
} else { } else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" } requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
} }
...@@ -70,13 +66,13 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView { ...@@ -70,13 +66,13 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
override fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>) { override fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>) {
ui { ui {
if (recycler_view.adapter == null) { if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, "", null, false) adapter = ChatRoomAdapter(enableActions = false)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val linearLayoutManager = val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view.layoutManager = linearLayoutManager recycler_view.layoutManager = linearLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
if (favoriteMessages.size > 10) { if (favoriteMessages.size >= 30) {
recycler_view.addOnScrollListener(object : recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) { EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore( override fun onLoadMore(
...@@ -114,6 +110,9 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView { ...@@ -114,6 +110,9 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
} }
private fun setupToolbar() { private fun setupToolbar() {
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_favorite_messages)) (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.presentation
import androidx.core.net.toUri
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.files.viewmodel.FileViewModel
import chat.rocket.android.files.viewmodel.FileViewModelMapper
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.getFiles
import timber.log.Timber
import javax.inject.Inject
class FilesPresenter @Inject constructor(
private val view: FilesView,
private val strategy: CancelStrategy,
private val roomsInteractor: ChatRoomsInteractor,
private val mapper: FileViewModelMapper,
val serverInteractor: GetCurrentServerInteractor,
val factory: RocketChatClientFactory
) {
private val serverUrl = serverInteractor.get()!!
private val client = factory.create(serverUrl)
private var offset: Int = 0
/**
* Load all files for the given room id.
*
* @param roomId The id of the room to get files from.
*/
fun loadFiles(roomId: String) {
launchUI(strategy) {
try {
view.showLoading()
roomsInteractor.getById(serverUrl, roomId)?.let {
val files = client.getFiles(roomId, it.type, offset)
val filesViewModel = mapper.mapToViewModelList(files.result)
view.showFiles(filesViewModel, files.total)
offset += 1 * 30
}.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server.")
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
Timber.e(exception)
} finally {
view.hideLoading()
}
}
}
fun openFile(fileViewModel: FileViewModel) {
when {
fileViewModel.isImage -> fileViewModel.url?.let {
view.openImage(it, fileViewModel.name ?: "")
}
fileViewModel.isMedia -> fileViewModel.url?.let {
view.playMedia(it)
}
else -> fileViewModel.url?.let {
view.openDocument(it.toUri())
}
}
}
}
\ No newline at end of file
package chat.rocket.android.files.presentation
import android.net.Uri
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.files.viewmodel.FileViewModel
interface FilesView : MessageView, LoadingView {
/**
* Show list of files for the current room.
*
* @param dataSet The data set to show.
* @param total The total number of files.
*/
fun showFiles(dataSet: List<FileViewModel>, total: Long)
/**
* Plays a media file (audio/video).
*
* @param url The file url to play its media.
*/
fun playMedia(url: String)
/**
* Opens an image file
*
* @param url The file url to open its image.
* @param name The file name.
*/
fun openImage(url: String, name: String)
/**
* Opens a document file (.pdf, .txt and so on).
*
* @param uri The file uri to open its document.
*/
fun openDocument(uri: Uri)
}
\ No newline at end of file
package chat.rocket.android.files.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.Fragment
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.ui.ChatRoomActivity
import chat.rocket.android.files.adapter.FilesAdapter
import chat.rocket.android.files.presentation.FilesPresenter
import chat.rocket.android.files.presentation.FilesView
import chat.rocket.android.files.viewmodel.FileViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.widget.DividerItemDecoration
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_files.*
import javax.inject.Inject
fun newInstance(chatRoomId: String): Fragment {
return FilesFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
}
}
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
class FilesFragment : Fragment(), FilesView {
@Inject
lateinit var presenter: FilesPresenter
private val adapter: FilesAdapter =
FilesAdapter { fileViewModel -> presenter.openFile(fileViewModel) }
private val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
private lateinit var chatRoomId: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_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_files)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
presenter.loadFiles(chatRoomId)
}
override fun showFiles(dataSet: List<FileViewModel>, total: Long) {
setupToolbar(total)
if (adapter.itemCount == 0) {
adapter.prependData(dataSet)
if (dataSet.size >= 30) {
recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView?
) {
presenter.loadFiles(chatRoomId)
}
})
}
} else {
adapter.appendData(dataSet)
}
}
override fun playMedia(url: String) {
ui {
PlayerActivity.play(it, url)
}
}
override fun openImage(url: String, name: String) {
ui {
ImageHelper.openImage(root_layout.context, url, name)
}
}
override fun openDocument(uri: Uri) {
ui {
startActivity(Intent(Intent.ACTION_VIEW, uri))
}
}
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 setupRecyclerView() {
ui {
recycler_view.layoutManager = linearLayoutManager
recycler_view.addItemDecoration(DividerItemDecoration(it))
recycler_view.adapter = adapter
}
}
private fun setupToolbar(totalFiles: Long) {
(activity as ChatRoomActivity).let {
it.showToolbarTitle(getString(R.string.title_files_total, totalFiles))
it.hideToolbarChatRoomIcon()
}
}
}
\ No newline at end of file
package chat.rocket.android.files.viewmodel
import DateTimeHelper
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.fileUrl
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.GenericAttachment
class FileViewModel(
private val genericAttachment: GenericAttachment,
private val settings: Map<String, Value<Any>>,
private val tokenRepository: TokenRepository,
private val baseUrl: String
) {
val name: String?
val uploader: String?
val uploadDate: String?
val url: String?
val isMedia: Boolean
val isImage: Boolean
init {
name = getFileName()
uploader = getUserDisplayName()
uploadDate = getFileUploadDate()
url = getFileUrl()
isMedia = isFileMedia()
isImage = isFileImage()
}
private fun getFileName(): String? {
return genericAttachment.name
}
private fun getUserDisplayName(): String {
val username = "@${genericAttachment.user?.username}"
val realName = genericAttachment.user?.name
val uploaderName = if (settings.useRealName()) realName else username
return uploaderName ?: username
}
private fun getFileUploadDate(): String {
genericAttachment.uploadedAt?.let {
return DateTimeHelper.getDateTime(DateTimeHelper.getLocalDateTime(it))
}
return ""
}
private fun getFileUrl(): String? {
val token = tokenRepository.get(baseUrl)
if (token != null) {
genericAttachment.path?.let {
return baseUrl.fileUrl(it, token)
}
}
return ""
}
private fun isFileMedia(): Boolean {
genericAttachment.type?.let {
return it.contains("audio") || it.contains("video")
}
return false
}
private fun isFileImage(): Boolean {
genericAttachment.type?.let {
return it.contains("image")
}
return false
}
}
\ 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
package chat.rocket.android.helper
import android.Manifest
import android.app.Activity
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.media.MediaScannerConnection
import android.os.Environment
import android.support.design.widget.AppBarLayout
import android.support.v7.widget.Toolbar
import android.text.TextUtils
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.net.toUri
import androidx.core.view.setPadding
import chat.rocket.android.R
import com.facebook.binaryresource.FileBinaryResource
import com.facebook.cache.common.CacheKey
import com.facebook.imageformat.ImageFormatChecker
import com.facebook.imagepipeline.cache.DefaultCacheKeyFactory
import com.facebook.imagepipeline.core.ImagePipelineFactory
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.stfalcon.frescoimageviewer.ImageViewer
import timber.log.Timber
import java.io.File
object ImageHelper {
private var cacheKey: CacheKey? = null
// TODO - implement a proper image viewer with a proper Transition
// TODO - We should definitely write our own ImageViewer
fun openImage(context: Context, imageUrl: String, imageName: String) {
var imageViewer: ImageViewer? = null
val request =
ImageRequestBuilder.newBuilderWithSource(imageUrl.toUri())
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.DISK_CACHE)
.build()
cacheKey = DefaultCacheKeyFactory.getInstance()
.getEncodedCacheKey(request, null)
val pad = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_padding)
val lparams = AppBarLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val toolbar = Toolbar(context).also {
it.inflateMenu(R.menu.image_actions)
it.setOnMenuItemClickListener {
return@setOnMenuItemClickListener when (it.itemId) {
R.id.action_save_image -> saveImage(context)
else -> true
}
}
val titleSize = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_title)
val titleTextView = TextView(context).also {
it.text = imageName
it.setTextColor(Color.WHITE)
it.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize.toFloat())
it.ellipsize = TextUtils.TruncateAt.END
it.setSingleLine()
it.typeface = Typeface.DEFAULT_BOLD
it.setPadding(pad)
}
val backArrowView = ImageView(context).also {
it.setImageResource(R.drawable.ic_arrow_back_white_24dp)
it.setOnClickListener { imageViewer?.onDismiss() }
it.setPadding(0, pad, pad, pad)
}
val layoutParams = AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.WRAP_CONTENT,
AppBarLayout.LayoutParams.WRAP_CONTENT
)
it.addView(backArrowView, layoutParams)
it.addView(titleTextView, layoutParams)
}
val appBarLayout = AppBarLayout(context).also {
it.layoutParams = lparams
it.setBackgroundColor(Color.BLACK)
it.addView(
toolbar, AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
)
}
val builder = ImageViewer.createPipelineDraweeControllerBuilder()
.setImageRequest(request)
.setAutoPlayAnimations(true)
imageViewer = ImageViewer.Builder(context, listOf(imageUrl))
.setOverlayView(appBarLayout)
.setStartPosition(0)
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
.show()
}
private fun saveImage(context: Context): Boolean {
if (!canWriteToExternalStorage(context)) {
checkWritingPermission(context)
return false
}
if (ImagePipelineFactory.getInstance().mainFileCache.hasKey(cacheKey)) {
val resource = ImagePipelineFactory.getInstance().mainFileCache.getResource(cacheKey)
val cachedFile = (resource as FileBinaryResource).file
val imageFormat = ImageFormatChecker.getImageFormat(resource.openStream())
val imageDir = "${Environment.DIRECTORY_PICTURES}/Rocket.Chat Images/"
val imagePath = Environment.getExternalStoragePublicDirectory(imageDir)
val imageFile =
File(imagePath, "${cachedFile.nameWithoutExtension}.${imageFormat.fileExtension}")
imagePath.mkdirs()
imageFile.createNewFile()
try {
cachedFile.copyTo(imageFile, true)
MediaScannerConnection.scanFile(
context,
arrayOf(imageFile.absolutePath),
null
) { path, uri ->
Timber.i("Scanned $path:")
Timber.i("-> uri=$uri")
}
} catch (ex: Exception) {
Timber.e(ex)
val message = context.getString(R.string.msg_image_saved_failed)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} finally {
val message = context.getString(R.string.msg_image_saved_successfully)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
return true
}
private fun canWriteToExternalStorage(context: Context): Boolean {
return AndroidPermissionsHelper.checkPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
private fun checkWritingPermission(context: Context) {
if (context is ContextThemeWrapper && context.baseContext is Activity) {
val activity = context.baseContext as Activity
AndroidPermissionsHelper.requestPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
AndroidPermissionsHelper.WRITE_EXTERNAL_STORAGE_CODE
)
}
}
}
\ No newline at end of file
...@@ -93,7 +93,7 @@ class MessageParser @Inject constructor( ...@@ -93,7 +93,7 @@ class MessageParser @Inject constructor(
private val othersTextColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme) private val othersTextColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme) private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme)
private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.white, context.theme) private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.colorWhite, context.theme)
private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme) private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val mentionPadding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat() private val mentionPadding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat() private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
......
package chat.rocket.android.helper
import android.app.Activity
import android.content.IntentSender
import android.support.v4.app.FragmentActivity
import com.google.android.gms.auth.api.credentials.*
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.ResolvableApiException
import timber.log.Timber
const val REQUEST_CODE_FOR_SIGN_IN_REQUIRED = 1
const val REQUEST_CODE_FOR_MULTIPLE_ACCOUNTS_RESOLUTION = 2
const val REQUEST_CODE_FOR_SAVE_RESOLUTION = 3
/**
* This class handles some cases of Google Smart Lock for passwords like the request to retrieve
* credentials, to retrieve sign-in hints and to store the credentials.
*
* See https://developers.google.com/identity/smartlock-passwords/android/overview for futher
* information.
*/
object SmartLockHelper {
/**
* Requests for stored Google Smart Lock credentials.
* Note that in case of exception it will try to start a sign in
* ([REQUEST_CODE_FOR_SIGN_IN_REQUIRED]) or "multiple account"
* ([REQUEST_CODE_FOR_MULTIPLE_ACCOUNTS_RESOLUTION]) resolution.
*
* @param credentialsClient The credential client.
* @param activity The activity.
* @return null or the [Credential] result.
*/
fun requestStoredCredentials(
credentialsClient: CredentialsClient,
activity: Activity
): Credential? {
var credential: Credential? = null
val credentialRequest = CredentialRequest.Builder()
.setPasswordLoginSupported(true)
.build()
credentialsClient.request(credentialRequest)
.addOnCompleteListener {
when {
it.isSuccessful -> {
credential = it.result.credential
}
it.exception is ResolvableApiException -> {
val resolvableApiException = (it.exception as ResolvableApiException)
if (resolvableApiException.statusCode == CommonStatusCodes.SIGN_IN_REQUIRED) {
provideSignInHint(credentialsClient, activity)
} else {
// This is most likely the case where the user has multiple saved
// credentials and needs to pick one. This requires showing UI to
// resolve the read request.
resolveResult(
resolvableApiException,
REQUEST_CODE_FOR_MULTIPLE_ACCOUNTS_RESOLUTION,
activity
)
}
}
}
}
return credential
}
/**
* Saves a user credential to Google Smart Lock.
* Note that in case of exception it will try to start a save resolution,
* so the activity/fragment should expected for a request code
* ([REQUEST_CODE_FOR_SAVE_RESOLUTION]) on onActivityResult call.
*
* @param credentialsClient The credential client.
* @param activity The activity.
* @param id The user id credential.
* @param password The user password credential.
*/
fun save(
credentialsClient: CredentialsClient,
activity: FragmentActivity,
id: String,
password: String
) {
val credential = Credential.Builder(id)
.setPassword(password)
.build()
credentialsClient.save(credential)
.addOnCompleteListener {
val exception = it.exception
if (exception is ResolvableApiException) {
// Try to resolve the save request. This will prompt the user if
// the credential is new.
try {
exception.startResolutionForResult(
activity,
REQUEST_CODE_FOR_SAVE_RESOLUTION
)
} catch (e: IntentSender.SendIntentException) {
Timber.e("Failed to send resolution. Exception is: $e")
}
}
}
}
private fun provideSignInHint(credentialsClient: CredentialsClient, activity: Activity) {
val hintRequest = HintRequest.Builder()
.setHintPickerConfig(
CredentialPickerConfig.Builder()
.setShowCancelButton(true)
.build()
)
.setEmailAddressIdentifierSupported(true)
.build()
try {
val intent = credentialsClient.getHintPickerIntent(hintRequest)
activity.startIntentSenderForResult(
intent.intentSender,
REQUEST_CODE_FOR_SIGN_IN_REQUIRED,
null,
0,
0,
0,
null
)
} catch (e: IntentSender.SendIntentException) {
Timber.e("Could not start hint picker Intent. Exception is: $e")
}
}
private fun resolveResult(
exception: ResolvableApiException,
requestCode: Int,
activity: Activity
) {
try {
exception.startResolutionForResult(activity, requestCode)
} catch (e: IntentSender.SendIntentException) {
Timber.e("Failed to send resolution. Exception is: $e")
}
}
}
\ No newline at end of file
...@@ -12,9 +12,9 @@ import chat.rocket.android.util.extensions.addFragment ...@@ -12,9 +12,9 @@ import chat.rocket.android.util.extensions.addFragment
class MainNavigator(internal val activity: MainActivity) { class MainNavigator(internal val activity: MainActivity) {
fun toChatList() { fun toChatList(chatRoomId: String? = null) {
activity.addFragment("ChatRoomsFragment", R.id.fragment_container) { activity.addFragment("ChatRoomsFragment", R.id.fragment_container) {
ChatRoomsFragment.newInstance() ChatRoomsFragment.newInstance(chatRoomId)
} }
} }
...@@ -43,7 +43,7 @@ class MainNavigator(internal val activity: MainActivity) { ...@@ -43,7 +43,7 @@ class MainNavigator(internal val activity: MainActivity) {
} }
fun toNewServer(serverUrl: String? = null) { fun toNewServer(serverUrl: String? = null) {
activity.startActivity(activity.changeServerIntent(serverUrl)) activity.startActivity(activity.changeServerIntent(serverUrl = serverUrl))
activity.finish() activity.finish()
} }
......
...@@ -49,7 +49,7 @@ class MainPresenter @Inject constructor( ...@@ -49,7 +49,7 @@ class MainPresenter @Inject constructor(
private val userDataChannel = Channel<Myself>() private val userDataChannel = Channel<Myself>()
fun toChatList() = navigator.toChatList() fun toChatList(chatRoomId: String? = null) = navigator.toChatList(chatRoomId)
fun toUserProfile() = navigator.toUserProfile() fun toUserProfile() = navigator.toUserProfile()
...@@ -105,11 +105,12 @@ class MainPresenter @Inject constructor( ...@@ -105,11 +105,12 @@ class MainPresenter @Inject constructor(
disconnect() disconnect()
removeAccountInteractor.remove(currentServer) removeAccountInteractor.remove(currentServer)
tokenRepository.remove(currentServer) tokenRepository.remove(currentServer)
view.disableAutoSignIn()
navigator.toNewServer() navigator.toNewServer()
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error cleaning up the session...") Timber.d(ex, "Error cleaning up the session...")
} }
view.disableAutoSignIn()
navigator.toNewServer() navigator.toNewServer()
} }
} }
...@@ -177,6 +178,7 @@ class MainPresenter @Inject constructor( ...@@ -177,6 +178,7 @@ class MainPresenter @Inject constructor(
if (pushToken != null) { if (pushToken != null) {
try { try {
retryIO("unregisterPushToken") { client.unregisterPushToken(pushToken) } retryIO("unregisterPushToken") { client.unregisterPushToken(pushToken) }
view.invalidateToken(pushToken)
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error unregistering push token") Timber.d(ex, "Error unregistering push token")
} }
......
...@@ -24,4 +24,10 @@ interface MainView : MessageView, VersionCheckView { ...@@ -24,4 +24,10 @@ interface MainView : MessageView, VersionCheckView {
fun setupNavHeader(viewModel: NavHeaderViewModel, accounts: List<Account>) fun setupNavHeader(viewModel: NavHeaderViewModel, accounts: List<Account>)
fun closeServerSelection() fun closeServerSelection()
fun invalidateToken(token: String)
/**
* callback to disable auto sign in for google smart lock when the user logs out
*/
fun disableAutoSignIn()
} }
\ No newline at end of file
package chat.rocket.android.main.ui package chat.rocket.android.main.ui
import DrawableHelper
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
...@@ -11,19 +12,22 @@ import android.view.MenuItem ...@@ -11,19 +12,22 @@ import android.view.MenuItem
import android.view.View import android.view.View
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.main.adapter.Selector
import chat.rocket.android.main.adapter.AccountsAdapter import chat.rocket.android.main.adapter.AccountsAdapter
import chat.rocket.android.main.adapter.Selector
import chat.rocket.android.main.presentation.MainPresenter import chat.rocket.android.main.presentation.MainPresenter
import chat.rocket.android.main.presentation.MainView import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.viewmodel.NavHeaderViewModel import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.ui.INTENT_CHAT_ROOM_ID
import chat.rocket.android.util.extensions.fadeIn import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.rotateBy import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
import com.google.android.gms.gcm.GoogleCloudMessaging import com.google.firebase.iid.FirebaseInstanceId
import com.google.android.gms.iid.InstanceID import com.google.firebase.messaging.FirebaseMessaging
import com.google.android.gms.auth.api.Auth
import com.google.android.gms.common.api.GoogleApiClient
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
...@@ -37,39 +41,81 @@ import kotlinx.coroutines.experimental.launch ...@@ -37,39 +41,81 @@ import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupportFragmentInjector { class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupportFragmentInjector,
@Inject lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity> GoogleApiClient.ConnectionCallbacks {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject
@Inject lateinit var presenter: MainPresenter lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var presenter: MainPresenter
private var isFragmentAdded: Boolean = false private var isFragmentAdded: Boolean = false
private var expanded = false private var expanded = false
private lateinit var googleApiClient: GoogleApiClient
private val headerLayout by lazy { view_navigation.getHeaderView(0) } private val headerLayout by lazy { view_navigation.getHeaderView(0) }
private var chatRoomId: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this) AndroidInjection.inject(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
buildGoogleApiClient()
launch(CommonPool) { launch(CommonPool) {
try { try {
val token = InstanceID.getInstance(this@MainActivity).getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null) val token = FirebaseInstanceId.getInstance().token
Timber.d("GCM token: $token") Timber.d("FCM token: $token")
presenter.refreshToken(token) presenter.refreshToken(token)
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Missing play services...") Timber.d(ex, "Missing play services...")
} }
} }
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
presenter.connect() presenter.connect()
presenter.loadCurrentInfo() presenter.loadCurrentInfo()
setupToolbar() setupToolbar()
setupNavigationView() setupNavigationView()
} }
override fun onConnected(bundle: Bundle?) {
}
override fun onConnectionSuspended(errorCode: Int) {
}
private fun buildGoogleApiClient() {
googleApiClient = GoogleApiClient.Builder(this)
.enableAutoManage(this, {
Timber.d("ERROR: connection to client failed")
})
.addConnectionCallbacks(this)
.addApi(Auth.CREDENTIALS_API)
.build()
}
override fun onStart() {
super.onStart()
googleApiClient.let {
if (it.isConnected) {
Timber.d("Google api client connected successfully")
}
}
}
override fun disableAutoSignIn() {
googleApiClient.let {
if (it.isConnected) {
Auth.CredentialsApi.disableAutoSignIn(googleApiClient)
}
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (!isFragmentAdded) { if (!isFragmentAdded) {
presenter.toChatList() presenter.toChatList(chatRoomId)
isFragmentAdded = true isFragmentAdded = true
} }
} }
...@@ -119,19 +165,33 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp ...@@ -119,19 +165,33 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
override fun alertNotRecommendedVersion() { override fun alertNotRecommendedVersion() {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(getString(R.string.msg_ver_not_recommended, BuildConfig.RECOMMENDED_SERVER_VERSION)) .setMessage(
.setPositiveButton(R.string.msg_ok, null) getString(
.create() R.string.msg_ver_not_recommended,
.show() BuildConfig.RECOMMENDED_SERVER_VERSION
)
)
.setPositiveButton(R.string.msg_ok, null)
.create()
.show()
} }
override fun blockAndAlertNotRequiredVersion() { override fun blockAndAlertNotRequiredVersion() {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(getString(R.string.msg_ver_not_minimum, BuildConfig.REQUIRED_SERVER_VERSION)) .setMessage(
.setOnDismissListener { presenter.logout() } getString(
.setPositiveButton(R.string.msg_ok, null) R.string.msg_ver_not_minimum,
.create() BuildConfig.REQUIRED_SERVER_VERSION
.show() )
)
.setOnDismissListener { presenter.logout() }
.setPositiveButton(R.string.msg_ok, null)
.create()
.show()
}
override fun invalidateToken(token: String) {
FirebaseInstanceId.getInstance().deleteToken(token, FirebaseMessaging.INSTANCE_ID_SCOPE)
} }
private fun setupAccountsList(header: View, accounts: List<Account>) { private fun setupAccountsList(header: View, accounts: List<Account>) {
...@@ -176,7 +236,8 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp ...@@ -176,7 +236,8 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
override fun activityInjector(): AndroidInjector<Activity> = activityDispatchingAndroidInjector override fun activityInjector(): AndroidInjector<Activity> = activityDispatchingAndroidInjector
override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector override fun supportFragmentInjector(): AndroidInjector<Fragment> =
fragmentDispatchingAndroidInjector
private fun setupToolbar() { private fun setupToolbar() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
......
...@@ -3,39 +3,44 @@ package chat.rocket.android.members.presentation ...@@ -3,39 +3,44 @@ package chat.rocket.android.members.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.members.viewmodel.MemberViewModel import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.members.viewmodel.MemberViewModelMapper import chat.rocket.android.members.viewmodel.MemberViewModelMapper
import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.getMembers import chat.rocket.core.internal.rest.getMembers
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MembersPresenter @Inject constructor( class MembersPresenter @Inject constructor(
private val view: MembersView, private val view: MembersView,
private val navigator: MembersNavigator, private val navigator: MembersNavigator,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor, private val roomsInteractor: ChatRoomsInteractor,
factory: RocketChatClientFactory, private val mapper: MemberViewModelMapper,
private val mapper: MemberViewModelMapper val serverInteractor: GetCurrentServerInteractor,
val factory: RocketChatClientFactory
) { ) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!) private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl)
private var offset: Long = 0
fun loadChatRoomsMembers(chatRoomId: String, chatRoomType: String, offset: Long = 0) { fun loadChatRoomsMembers(roomId: String) {
launchUI(strategy) { launchUI(strategy) {
try { try {
view.showLoading() view.showLoading()
roomsInteractor.getById(serverUrl, roomId)?.let {
val members = retryIO("getMembers($chatRoomId, $chatRoomType, $offset)") { val members = client.getMembers(it.id, it.type, offset, 60)
client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 60) val memberViewModels = mapper.mapToViewModelList(members.result)
view.showMembers(memberViewModels, members.total)
offset += 1 * 60L
}.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server")
} }
val memberViewModels = mapper.mapToViewModelList(members.result) } catch (exception: RocketChatException) {
view.showMembers(memberViewModels, members.total) exception.message?.let {
} catch (ex: RocketChatException) {
ex.message?.let {
view.showMessage(it) view.showMessage(it)
}.ifNull { }.ifNull {
view.showGenericErrorMessage() view.showGenericErrorMessage()
......
...@@ -2,7 +2,6 @@ package chat.rocket.android.members.ui ...@@ -2,7 +2,6 @@ package chat.rocket.android.members.ui
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.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
...@@ -24,17 +23,15 @@ import dagger.android.support.AndroidSupportInjection ...@@ -24,17 +23,15 @@ import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_members.* import kotlinx.android.synthetic.main.fragment_members.*
import javax.inject.Inject import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment { fun newInstance(chatRoomId: String): Fragment {
return MembersFragment().apply { return MembersFragment().apply {
arguments = Bundle(1).apply { arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId) putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
} }
} }
} }
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id" private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
class MembersFragment : Fragment(), MembersView { class MembersFragment : Fragment(), MembersView {
@Inject @Inject
...@@ -43,9 +40,7 @@ class MembersFragment : Fragment(), MembersView { ...@@ -43,9 +40,7 @@ class MembersFragment : Fragment(), MembersView {
MembersAdapter { memberViewModel -> presenter.toMemberDetails(memberViewModel) } MembersAdapter { memberViewModel -> presenter.toMemberDetails(memberViewModel) }
private val linearLayoutManager = private val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -54,7 +49,6 @@ class MembersFragment : Fragment(), MembersView { ...@@ -54,7 +49,6 @@ class MembersFragment : Fragment(), MembersView {
val bundle = arguments val bundle = arguments
if (bundle != null) { if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID) chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
} else { } else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" } requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
} }
...@@ -68,9 +62,8 @@ class MembersFragment : Fragment(), MembersView { ...@@ -68,9 +62,8 @@ class MembersFragment : Fragment(), MembersView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupRecyclerView() setupRecyclerView()
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType) presenter.loadChatRoomsMembers(chatRoomId)
} }
override fun showMembers(dataSet: List<MemberViewModel>, total: Long) { override fun showMembers(dataSet: List<MemberViewModel>, total: Long) {
...@@ -86,7 +79,7 @@ class MembersFragment : Fragment(), MembersView { ...@@ -86,7 +79,7 @@ class MembersFragment : Fragment(), MembersView {
totalItemsCount: Int, totalItemsCount: Int,
recyclerView: RecyclerView? recyclerView: RecyclerView?
) { ) {
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType, page * 60L) presenter.loadChatRoomsMembers(chatRoomId)
} }
}) })
} }
...@@ -127,8 +120,9 @@ class MembersFragment : Fragment(), MembersView { ...@@ -127,8 +120,9 @@ class MembersFragment : Fragment(), MembersView {
} }
private fun setupToolbar(totalMembers: Long) { private fun setupToolbar(totalMembers: Long) {
(activity as ChatRoomActivity?)?.setupToolbarTitle( (activity as ChatRoomActivity).let {
getString(R.string.title_members, totalMembers) it.showToolbarTitle(getString(R.string.title_members, totalMembers))
) it.hideToolbarChatRoomIcon()
}
} }
} }
\ No newline at end of file
package chat.rocket.android.chatroom.di package chat.rocket.android.pinnedmessages.di
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentModule
import chat.rocket.android.pinnedmessages.ui.PinnedMessagesFragment import chat.rocket.android.pinnedmessages.ui.PinnedMessagesFragment
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
......
...@@ -16,37 +16,36 @@ import javax.inject.Inject ...@@ -16,37 +16,36 @@ import javax.inject.Inject
class PinnedMessagesPresenter @Inject constructor( class PinnedMessagesPresenter @Inject constructor(
private val view: PinnedMessagesView, private val view: PinnedMessagesView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: ChatRoomsInteractor, private val roomsInteractor: ChatRoomsInteractor,
private val mapper: ViewModelMapper, private val mapper: ViewModelMapper,
factory: RocketChatClientFactory val serverInteractor: GetCurrentServerInteractor,
val factory: RocketChatClientFactory
) { ) {
private val client = factory.create(serverInteractor.get()!!) private val serverUrl = serverInteractor.get()!!
private var pinnedMessagesListOffset: Int = 0 private val client = factory.create(serverUrl)
private var offset: Int = 0
/** /**
* Load all pinned messages for the given room id. * Loads all pinned messages for the given room id.
* *
* @param roomId The id of the room to get pinned messages from. * @param roomId The id of the room to get pinned messages from.
*/ */
fun loadPinnedMessages(roomId: String) { fun loadPinnedMessages(roomId: String) {
launchUI(strategy) { launchUI(strategy) {
try { try {
val serverUrl = serverInteractor.get()!! view.showLoading()
val chatRoom = roomsInteractor.getById(serverUrl, roomId) roomsInteractor.getById(serverUrl, roomId)?.let {
chatRoom?.let { room -> val pinnedMessages = client.getPinnedMessages(roomId, it.type, offset)
view.showLoading() val messageList = mapper.map(pinnedMessages.result, asNotReversed = true)
val pinnedMessages =
client.getPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
pinnedMessagesListOffset = pinnedMessages.offset.toInt()
val messageList = mapper.map(pinnedMessages.result.filterNot { it.isSystemMessage() })
view.showPinnedMessages(messageList) view.showPinnedMessages(messageList)
view.hideLoading() offset += 1 * 30
}.ifNull { }.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server.") Timber.e("Couldn't find a room with id: $roomId at current server.")
} }
} catch (e: RocketChatException) { } catch (exception: RocketChatException) {
Timber.e(e) Timber.e(exception)
} finally {
view.hideLoading()
} }
} }
} }
......
...@@ -23,22 +23,19 @@ import dagger.android.support.AndroidSupportInjection ...@@ -23,22 +23,19 @@ import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_pinned_messages.* import kotlinx.android.synthetic.main.fragment_pinned_messages.*
import javax.inject.Inject import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment { fun newInstance(chatRoomId: String): Fragment {
return PinnedMessagesFragment().apply { return PinnedMessagesFragment().apply {
arguments = Bundle(1).apply { arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId) putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
} }
} }
} }
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id" private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
class PinnedMessagesFragment : Fragment(), PinnedMessagesView { class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
private lateinit var adapter: ChatRoomAdapter private lateinit var adapter: ChatRoomAdapter
@Inject @Inject
lateinit var presenter: PinnedMessagesPresenter lateinit var presenter: PinnedMessagesPresenter
...@@ -50,7 +47,6 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -50,7 +47,6 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
val bundle = arguments val bundle = arguments
if (bundle != null) { if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID) chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
} else { } else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" } requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
} }
...@@ -66,20 +62,19 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -66,20 +62,19 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupToolbar() setupToolbar()
presenter.loadPinnedMessages(chatRoomId) presenter.loadPinnedMessages(chatRoomId)
} }
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) { override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
ui { ui {
if (recycler_view_pinned.adapter == null) { if (recycler_view_pinned.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, "", null, false) adapter = ChatRoomAdapter(enableActions = false)
recycler_view_pinned.adapter = adapter recycler_view_pinned.adapter = adapter
val linearLayoutManager = val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view_pinned.layoutManager = linearLayoutManager recycler_view_pinned.layoutManager = linearLayoutManager
recycler_view_pinned.itemAnimator = DefaultItemAnimator() recycler_view_pinned.itemAnimator = DefaultItemAnimator()
if (pinnedMessages.size > 10) { if (pinnedMessages.size >= 30) {
recycler_view_pinned.addOnScrollListener(object : recycler_view_pinned.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) { EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore( override fun onLoadMore(
...@@ -121,6 +116,9 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -121,6 +116,9 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
} }
private fun setupToolbar() { private fun setupToolbar() {
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_pinned_messages)) (activity as ChatRoomActivity).let {
it.showToolbarTitle(getString(R.string.title_pinned_messages))
it.hideToolbarChatRoomIcon()
}
} }
} }
\ No newline at end of file
...@@ -58,7 +58,8 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView, ...@@ -58,7 +58,8 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView,
if(avatarUrl!="") { if(avatarUrl!="") {
retryIO { client.setAvatar(avatarUrl) } retryIO { client.setAvatar(avatarUrl) }
} }
val user = retryIO { client.updateProfile(myselfId, email, name, username) } val user = retryIO { client.updateProfile(
userId = myselfId, email = email, name = name, username = username) }
view.showProfileUpdateSuccessfullyMessage() view.showProfileUpdateSuccessfullyMessage()
loadUserProfile() loadUserProfile()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
......
package chat.rocket.android.push package chat.rocket.android.push
import android.os.Bundle import androidx.core.os.bundleOf
import com.google.android.gms.gcm.GcmListenerService import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import javax.inject.Inject import javax.inject.Inject
class GcmListenerService : GcmListenerService() { class FirebaseMessagingService : FirebaseMessagingService() {
@Inject @Inject
lateinit var pushManager: PushManager lateinit var pushManager: PushManager
...@@ -15,9 +16,9 @@ class GcmListenerService : GcmListenerService() { ...@@ -15,9 +16,9 @@ class GcmListenerService : GcmListenerService() {
AndroidInjection.inject(this) AndroidInjection.inject(this)
} }
override fun onMessageReceived(from: String?, data: Bundle?) { override fun onMessageReceived(message: RemoteMessage) {
data?.let { message.data?.let {
pushManager.handle(data) pushManager.handle(bundleOf(*(it.map { Pair(it.key, it.value) }).toTypedArray()))
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.push package chat.rocket.android.push
import chat.rocket.android.R
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.registerPushToken
import com.google.android.gms.gcm.GoogleCloudMessaging import com.google.firebase.iid.FirebaseInstanceId
import com.google.android.gms.iid.InstanceID
import com.google.firebase.iid.FirebaseInstanceIdService import com.google.firebase.iid.FirebaseInstanceIdService
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
...@@ -31,21 +29,18 @@ class FirebaseTokenService : FirebaseInstanceIdService() { ...@@ -31,21 +29,18 @@ class FirebaseTokenService : FirebaseInstanceIdService() {
} }
override fun onTokenRefresh() { override fun onTokenRefresh() {
//TODO: We need to use the Cordova Project gcm_sender_id since it's the one configured on RC
// default push gateway. We should register this project's own project sender id into it.
try { try {
val gcmToken = InstanceID.getInstance(this) val fcmToken = FirebaseInstanceId.getInstance().token
.getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
val currentServer = getCurrentServerInteractor.get() val currentServer = getCurrentServerInteractor.get()
val client = currentServer?.let { factory.create(currentServer) } val client = currentServer?.let { factory.create(currentServer) }
gcmToken?.let { fcmToken?.let {
localRepository.save(LocalRepository.KEY_PUSH_TOKEN, gcmToken) localRepository.save(LocalRepository.KEY_PUSH_TOKEN, fcmToken)
client?.let { client?.let {
launch { launch {
try { try {
Timber.d("Registering push token: $gcmToken for ${client.url}") Timber.d("Registering push token: $fcmToken for ${client.url}")
retryIO("register push token") { client.registerPushToken(gcmToken) } retryIO("register push token") { client.registerPushToken(fcmToken) }
} catch (ex: RocketChatException) { } catch (ex: RocketChatException) {
Timber.e(ex, "Error registering push token") Timber.e(ex, "Error registering push token")
} }
...@@ -53,7 +48,7 @@ class FirebaseTokenService : FirebaseInstanceIdService() { ...@@ -53,7 +48,7 @@ class FirebaseTokenService : FirebaseInstanceIdService() {
} }
} }
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error refreshing Firebase TOKEN") Timber.e(ex, "Error refreshing Firebase TOKEN")
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.push.di package chat.rocket.android.push.di
import chat.rocket.android.dagger.module.AppModule import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.push.GcmListenerService import chat.rocket.android.push.FirebaseMessagingService
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
@Module abstract class GcmListenerServiceProvider { @Module abstract class FirebaseMessagingServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class]) @ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideGcmListenerService(): GcmListenerService abstract fun provideFirebaseMessagingService(): FirebaseMessagingService
} }
\ No newline at end of file
...@@ -103,5 +103,5 @@ fun PublicSettings.uploadMaxFileSize(): Int { ...@@ -103,5 +103,5 @@ fun PublicSettings.uploadMaxFileSize(): Int {
return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE
} }
fun PublicSettings.baseUrl(): String? = this[SITE_URL]?.value as String? fun PublicSettings.baseUrl(): String = this[SITE_URL]?.value as String
fun PublicSettings.siteName(): String? = this[SITE_NAME]?.value as String? fun PublicSettings.siteName(): String? = this[SITE_NAME]?.value as String?
\ No newline at end of file
...@@ -3,16 +3,16 @@ package chat.rocket.android.server.infraestructure ...@@ -3,16 +3,16 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.common.model.BaseRoom import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.User import chat.rocket.common.model.User
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.subscribeSubscriptions
import chat.rocket.core.internal.realtime.subscribeRooms
import chat.rocket.core.internal.realtime.subscribeUserData
import chat.rocket.core.internal.realtime.subscribeActiveUsers
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.realtime.socket.connect import chat.rocket.core.internal.realtime.socket.connect
import chat.rocket.core.internal.realtime.socket.disconnect import chat.rocket.core.internal.realtime.socket.disconnect
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.realtime.socket.model.StreamMessage import chat.rocket.core.internal.realtime.socket.model.StreamMessage
import chat.rocket.core.internal.realtime.subscribeActiveUsers
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.subscribeRooms
import chat.rocket.core.internal.realtime.subscribeSubscriptions
import chat.rocket.core.internal.realtime.subscribeUserData
import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.rest.chatRooms import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself import chat.rocket.core.model.Myself
......
...@@ -4,6 +4,7 @@ import android.content.Intent ...@@ -4,6 +4,7 @@ import android.content.Intent
import chat.rocket.android.authentication.ui.newServerIntent 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.ChangeServerActivity import chat.rocket.android.server.ui.ChangeServerActivity
import chat.rocket.android.server.ui.INTENT_CHAT_ROOM_ID
class ChangeServerNavigator (internal val activity: ChangeServerActivity) { class ChangeServerNavigator (internal val activity: ChangeServerActivity) {
fun toServerScreen() { fun toServerScreen() {
...@@ -11,8 +12,10 @@ class ChangeServerNavigator (internal val activity: ChangeServerActivity) { ...@@ -11,8 +12,10 @@ class ChangeServerNavigator (internal val activity: ChangeServerActivity) {
activity.finish() activity.finish()
} }
fun toChatRooms() { fun toChatRooms(chatRoomId: String? = null) {
activity.startActivity(Intent(activity, MainActivity::class.java)) activity.startActivity(Intent(activity, MainActivity::class.java).also {
it.putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
})
activity.finish() activity.finish()
} }
......
...@@ -21,7 +21,7 @@ class ChangeServerPresenter @Inject constructor( ...@@ -21,7 +21,7 @@ class ChangeServerPresenter @Inject constructor(
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val connectionManager: ConnectionManagerFactory private val connectionManager: ConnectionManagerFactory
) { ) {
fun loadServer(newUrl: String?) { fun loadServer(newUrl: String?, chatRoomId: String? = null) {
launchUI(strategy) { launchUI(strategy) {
view.showProgress() view.showProgress()
var url = newUrl var url = newUrl
...@@ -56,7 +56,7 @@ class ChangeServerPresenter @Inject constructor( ...@@ -56,7 +56,7 @@ class ChangeServerPresenter @Inject constructor(
saveCurrentServerInteractor.save(serverUrl) saveCurrentServerInteractor.save(serverUrl)
view.hideProgress() view.hideProgress()
navigator.toChatRooms() navigator.toChatRooms(chatRoomId)
}.ifNull { }.ifNull {
view.hideProgress() view.hideProgress()
navigator.toServerScreen() navigator.toServerScreen()
......
...@@ -21,7 +21,8 @@ class ChangeServerActivity : AppCompatActivity(), ChangeServerView { ...@@ -21,7 +21,8 @@ class ChangeServerActivity : AppCompatActivity(), ChangeServerView {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val serverUrl: String? = intent.getStringExtra(INTENT_SERVER_URL) val serverUrl: String? = intent.getStringExtra(INTENT_SERVER_URL)
presenter.loadServer(serverUrl) val chatRoomId: String? = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
presenter.loadServer(serverUrl, chatRoomId)
} }
override fun showInvalidCredentials() { override fun showInvalidCredentials() {
...@@ -40,11 +41,13 @@ class ChangeServerActivity : AppCompatActivity(), ChangeServerView { ...@@ -40,11 +41,13 @@ class ChangeServerActivity : AppCompatActivity(), ChangeServerView {
private const val INTENT_SERVER_URL = "INTENT_SERVER_URL" private const val INTENT_SERVER_URL = "INTENT_SERVER_URL"
private const val INTENT_CHAT_ROOM_NAME = "INTENT_CHAT_ROOM_NAME" private const val INTENT_CHAT_ROOM_NAME = "INTENT_CHAT_ROOM_NAME"
private const val INTENT_CHAT_ROOM_TYPE = "INTENT_CHAT_ROOM_TYPE" private const val INTENT_CHAT_ROOM_TYPE = "INTENT_CHAT_ROOM_TYPE"
const val INTENT_CHAT_ROOM_ID = "INTENT_CHAT_ROOM_ID"
fun Context.changeServerIntent(serverUrl: String? = null): Intent { fun Context.changeServerIntent(serverUrl: String? = null, chatRoomId: String? = ""): Intent {
return Intent(this, ChangeServerActivity::class.java).apply { return Intent(this, ChangeServerActivity::class.java).apply {
serverUrl?.let { url -> serverUrl?.let { url ->
putExtra(INTENT_SERVER_URL, url) putExtra(INTENT_SERVER_URL, url)
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
} }
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
} }
......
...@@ -2,6 +2,7 @@ package chat.rocket.android.util.extensions ...@@ -2,6 +2,7 @@ package chat.rocket.android.util.extensions
import android.graphics.Color import android.graphics.Color
import android.util.Patterns import android.util.Patterns
import chat.rocket.common.model.Token
import timber.log.Timber import timber.log.Timber
fun String.removeTrailingSlash(): String { fun String.removeTrailingSlash(): String {
...@@ -17,7 +18,11 @@ fun String.sanitize(): String { ...@@ -17,7 +18,11 @@ fun String.sanitize(): String {
return tmp.removeTrailingSlash() return tmp.removeTrailingSlash()
} }
fun String.avatarUrl(avatar: String, isGroupOrChannel: Boolean = false, format: String = "jpeg"): String { fun String.avatarUrl(
avatar: String,
isGroupOrChannel: Boolean = false,
format: String = "jpeg"
): String {
return if (isGroupOrChannel) { return if (isGroupOrChannel) {
"${removeTrailingSlash()}/avatar/%23${avatar.removeTrailingSlash()}?format=$format" "${removeTrailingSlash()}/avatar/%23${avatar.removeTrailingSlash()}?format=$format"
} else { } else {
...@@ -25,6 +30,14 @@ fun String.avatarUrl(avatar: String, isGroupOrChannel: Boolean = false, format: ...@@ -25,6 +30,14 @@ fun String.avatarUrl(avatar: String, isGroupOrChannel: Boolean = false, format:
} }
} }
fun String.fileUrl(path: String, token: Token): String {
return (this + path + "?rc_uid=${token.userId}" + "&rc_token=${token.authToken}").safeUrl()
}
fun String.safeUrl(): String {
return this.replace(" ", "%20").replace("\\", "")
}
fun String.serverLogoUrl(favicon: String) = "${removeTrailingSlash()}/$favicon" fun String.serverLogoUrl(favicon: String) = "${removeTrailingSlash()}/$favicon"
fun String.casUrl(serverUrl: String, token: String) = fun String.casUrl(serverUrl: String, token: String) =
......
...@@ -40,7 +40,6 @@ class OauthWebViewActivity : AppCompatActivity() { ...@@ -40,7 +40,6 @@ class OauthWebViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view) setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL) webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" } requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
...@@ -81,7 +80,8 @@ class OauthWebViewActivity : AppCompatActivity() { ...@@ -81,7 +80,8 @@ class OauthWebViewActivity : AppCompatActivity() {
domStorageEnabled = true domStorageEnabled = true
// TODO Remove this workaround that is required to make Google OAuth to work. We should use Custom Tabs instead. See https://github.com/RocketChat/Rocket.Chat.Android/issues/968 // TODO Remove this workaround that is required to make Google OAuth to work. We should use Custom Tabs instead. See https://github.com/RocketChat/Rocket.Chat.Android/issues/968
if (webPageUrl.contains("google")) { if (webPageUrl.contains("google")) {
userAgentString = "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/43.0.2357.65 Mobile Safari/535.19" userAgentString =
"Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/43.0.2357.65 Mobile Safari/535.19"
} }
} }
web_view.webViewClient = object : WebViewClient() { web_view.webViewClient = object : WebViewClient() {
...@@ -114,8 +114,18 @@ class OauthWebViewActivity : AppCompatActivity() { ...@@ -114,8 +114,18 @@ class OauthWebViewActivity : AppCompatActivity() {
private fun getCredentialSecret(json: JSONObject): String = private fun getCredentialSecret(json: JSONObject): String =
json.optString(JSON_CREDENTIAL_SECRET) json.optString(JSON_CREDENTIAL_SECRET)
private fun closeView(activityResult: Int = Activity.RESULT_CANCELED, credentialToken: String? = null, credentialSecret: String? = null) { private fun closeView(
setResult(activityResult, Intent().putExtra(INTENT_OAUTH_CREDENTIAL_TOKEN, credentialToken).putExtra(INTENT_OAUTH_CREDENTIAL_SECRET, credentialSecret)) activityResult: Int = Activity.RESULT_CANCELED,
credentialToken: String? = null,
credentialSecret: String? = null
) {
setResult(
activityResult,
Intent().putExtra(INTENT_OAUTH_CREDENTIAL_TOKEN, credentialToken).putExtra(
INTENT_OAUTH_CREDENTIAL_SECRET,
credentialSecret
)
)
finish() finish()
overridePendingTransition(R.anim.hold, R.anim.slide_down) overridePendingTransition(R.anim.hold, R.anim.slide_down)
} }
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:fillColor="#9EA2A8"
android:fillType="evenOdd"
android:pathData="M2.4,0h1.2v12h-1.2z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#9EA2A8"
android:fillType="evenOdd"
android:pathData="M0,2.4h12v1.2h-12z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#9EA2A8"
android:fillType="evenOdd"
android:pathData="M0,8.4h12v1.2h-12z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#9EA2A8"
android:fillType="evenOdd"
android:pathData="M8.4,0h1.2v12h-1.2z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:fillColor="#FF000000"
android:pathData="M2.4,0h1.2v12h-1.2z" />
<path
android:fillColor="#FF000000"
android:pathData="M0,2.4h12v1.2h-12z" />
<path
android:fillColor="#FF000000"
android:pathData="M0,8.4h12v1.2h-12z" />
<path
android:fillColor="#FF000000"
android:pathData="M8.4,0h1.2v12h-1.2z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp" android:width="24dp"
android:height="16dp" android:height="24dp"
android:viewportHeight="24.0" android:viewportHeight="24.0"
android:viewportWidth="24.0"> android:viewportWidth="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M18,10v-4c0,-3.313 -2.687,-6 -6,-6s-6,2.687 -6,6v4h-3v14h18v-14h-3zM8,6c0,-2.206 1.794,-4 4,-4s4,1.794 4,4v4h-8v-4zM19,22h-14v-10h14v10z" /> android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
</vector> </vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M1.5,5.5h9v6h-9z"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#9EA2A8"
android:fillType="evenOdd"/>
<path
android:pathData="M2.5,5.5L9.5,5.5L9.5,4C9.5,2.067 7.933,0.5 6,0.5C4.067,0.5 2.5,2.067 2.5,4L2.5,5.5Z"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#9EA2A8"
android:fillType="evenOdd"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:pathData="M1.5,5.5h9v6h-9z"
android:strokeColor="#FF000000"
android:strokeWidth="1" />
<path
android:pathData="M2.5,5.5L9.5,5.5L9.5,4C9.5,2.067 7.933,0.5 6,0.5C4.067,0.5 2.5,2.067 2.5,4L2.5,5.5Z"
android:strokeColor="#FF000000"
android:strokeWidth="1" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp" android:width="24dp"
android:height="16dp" android:height="24dp"
android:viewportHeight="24.0" android:viewportHeight="24.0"
android:viewportWidth="24.0"> android:viewportWidth="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="#FF000000"
android:pathData="M22.548,9l0.452,-2h-5.364l1.364,-6h-2l-1.364,6h-5l1.364,-6h-2l-1.364,6h-6.184l-0.452,2h6.182l-1.364,6h-5.36l-0.458,2h5.364l-1.364,6h2l1.364,-6h5l-1.364,6h2l1.364,-6h6.185l0.451,-2h-6.182l1.364,-6h5.366zM13.818,15h-5l1.364,-6h5l-1.364,6z" /> android:pathData="M8,5v14l11,-7z" />
</vector> </vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="20.0"
android:viewportWidth="20.0">
<path
android:fillColor="#FF000000"
android:pathData="M13.6,13.47c-0.91,0.953 -2.191,1.545 -3.61,1.545 -2.756,0 -4.99,-2.234 -4.99,-4.99 0,-0.009 0,-0.018 0,-0.026v0.001c0,-2.761 2.239,-5 5,-5 1.131,0 2.175,0.376 3.013,1.009l-0.013,-0.009v-1h2v6.5c0,0.828 0.672,1.5 1.5,1.5s1.5,-0.672 1.5,-1.5v0,-1.5c-0.003,-4.416 -3.584,-7.994 -8,-7.994 -4.418,0 -8,3.582 -8,8s3.582,8 8,8c1.305,0 2.537,-0.312 3.625,-0.867l-0.045,0.021 0.9,1.79c-1.305,0.668 -2.847,1.06 -4.48,1.06 -5.523,0 -10,-4.477 -10,-10s4.477,-10 10,-10c5.519,0 9.994,4.472 10,9.99v0.001h-0.01v1.5c0,0.003 0,0.007 0,0.01 0,1.933 -1.567,3.5 -3.5,3.5 -1.202,0 -2.262,-0.606 -2.892,-1.528l-0.008,-0.012zM10,13c1.657,0 3,-1.343 3,-3s-1.343,-3 -3,-3v0c-1.657,0 -3,1.343 -3,3s1.343,3 3,3v0z" />
</vector>
\ No newline at end of file
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="@dimen/nav_header_height" android:layout_marginTop="@dimen/nav_header_height"
android:alpha="0" android:alpha="0"
android:background="@color/white" android:background="@color/colorWhite"
android:elevation="20dp" android:elevation="20dp"
android:visibility="gone" /> android:visibility="gone" />
</FrameLayout> </FrameLayout>
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
app:indicatorColor="@color/black" app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" /> app:indicatorName="BallPulseIndicator" />
</RelativeLayout> </RelativeLayout>
\ No newline at end of file
...@@ -16,35 +16,17 @@ ...@@ -16,35 +16,17 @@
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.constraint.ConstraintLayout <TextView
android:id="@+id/toolbar_content_container" android:id="@+id/text_room_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:drawablePadding="@dimen/text_view_drawable_padding"
android:ellipsize="end"
<!-- TODO implement --> android:maxLines="1"
<com.facebook.drawee.view.SimpleDraweeView android:textColor="@color/colorWhite"
android:id="@+id/image_room_avatar" android:textSize="18sp"
android:layout_width="30dp" android:textStyle="bold"
android:layout_height="30dp" tools:text="general" />
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:roundAsCircle="true" />
<TextView
android:id="@+id/text_room_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:drawablePadding="@dimen/text_view_drawable_padding"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Developers" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.Toolbar> </android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout> </android.support.design.widget.AppBarLayout>
\ No newline at end of file
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="@color/white" android:textColor="@color/colorWhite"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
tools:text="@string/title_password" /> tools:text="@string/title_password" />
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
android:id="@+id/emoji_keyboard_container" android:id="@+id/emoji_keyboard_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:background="@color/white"> android:background="@color/colorWhite">
<View <View
android:id="@+id/divider" android:id="@+id/divider"
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
android:id="@+id/picker_container" android:id="@+id/picker_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/white" android:background="@color/colorWhite"
android:orientation="vertical"> android:orientation="vertical">
<android.support.design.widget.TabLayout <android.support.design.widget.TabLayout
...@@ -22,6 +22,6 @@ ...@@ -22,6 +22,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:background="@color/white" /> android:background="@color/colorWhite" />
</LinearLayout> </LinearLayout>
\ No newline at end of file
...@@ -32,6 +32,18 @@ ...@@ -32,6 +32,18 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_headline" /> app:layout_constraintTop_toBottomOf="@+id/text_headline" />
<ImageView
android:id="@+id/image_key"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:src="@drawable/ic_vpn_key_black_24dp"
android:tint="@color/colorDrawableTintGrey"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/text_username_or_email"
app:layout_constraintEnd_toEndOf="@+id/text_username_or_email"
app:layout_constraintTop_toTopOf="@+id/text_username_or_email" />
<EditText <EditText
android:id="@+id/text_password" android:id="@+id/text_password"
style="@style/Authentication.EditText" style="@style/Authentication.EditText"
...@@ -214,7 +226,7 @@ ...@@ -214,7 +226,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/ic_expand_more_black_24dp" android:src="@drawable/ic_expand_more_black_24dp"
android:theme="@style/Theme.AppCompat" android:theme="@style/Theme.AppCompat"
android:tint="@color/white" android:tint="@color/colorWhite"
android:visibility="gone" android:visibility="gone"
app:backgroundTint="@color/colorAccent" app:backgroundTint="@color/colorAccent"
app:elevation="@dimen/fab_elevation" app:elevation="@dimen/fab_elevation"
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" android:visibility="gone"
app:indicatorColor="@color/black" app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
android:elevation="4dp" android:elevation="4dp"
android:gravity="center" android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/white" android:textColor="@color/colorWhite"
android:visibility="gone" android:visibility="gone"
tools:alpha="1" tools:alpha="1"
tools:text="connected" tools:text="connected"
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:visibility="gone" android:visibility="gone"
app:indicatorColor="@color/black" app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" /> app:indicatorName="BallPulseIndicator" />
<TextView <TextView
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
android:elevation="4dp" android:elevation="4dp"
android:gravity="center" android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2" android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/white" android:textColor="@color/colorWhite"
android:visibility="gone" android:visibility="gone"
tools:alpha="1" tools:alpha="1"
tools:text="connected" tools:text="connected"
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:visibility="gone" android:visibility="gone"
app:indicatorColor="@color/black" app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".files.ui.FilesFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="@+id/recycler_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/image_file"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_insert_drive_file_black_24dp"
android:tint="@color/icon_grey"
app:layout_constraintBottom_toTopOf="@+id/text_no_file"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/text_no_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/msg_no_files"
android:textColor="@color/colorSecondaryText"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/text_all_files_appear_here"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image_file" />
<TextView
android:id="@+id/text_all_files_appear_here"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/msg_all_files_appear_here"
android:textColor="@color/colorSecondaryTextLight"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_no_file" />
<android.support.constraint.Group
android:id="@+id/group_no_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="image_file,text_no_file,text_all_files_appear_here"
tools:visibility="visible" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
style="@style/TextAppearance.AppCompat.Title" style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/white" android:textColor="@color/colorWhite"
tools:text="Ronald Perkins" /> tools:text="Ronald Perkins" />
<TextView <TextView
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:textColor="@color/white" android:textColor="@color/colorWhite"
tools:text="\@ronaldPerkins" /> tools:text="\@ronaldPerkins" />
</LinearLayout> </LinearLayout>
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:layout_gravity="center" android:layout_gravity="center"
app:indicatorColor="@color/black" app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" /> app:indicatorName="BallPulseIndicator" />
</android.support.design.widget.CoordinatorLayout> </android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:visibility="visible" tools:visibility="visible"
android:visibility="gone" android:visibility="gone"
app:indicatorColor="@color/black" app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" app:indicatorName="BallPulseIndicator"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:visibility="gone" android:visibility="gone"
app:indicatorColor="@color/black" app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="@+id/recycler_view_pinned" app:layout_constraintBottom_toBottomOf="@+id/recycler_view_pinned"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
......
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
app:indicatorColor="@color/black" app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" /> app:indicatorName="BallPulseIndicator" />
</RelativeLayout> </RelativeLayout>
\ No newline at end of file
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
app:layout_constraintBottom_toBottomOf="@+id/text_chat_name" app:layout_constraintBottom_toBottomOf="@+id/text_chat_name"
app:layout_constraintStart_toEndOf="@+id/image_avatar" app:layout_constraintStart_toEndOf="@+id/image_avatar"
app:layout_constraintTop_toTopOf="@+id/text_chat_name" app:layout_constraintTop_toTopOf="@+id/text_chat_name"
tools:src="@drawable/ic_hashtag_12dp" /> tools:src="@drawable/ic_hashtag_black_12dp" />
<TextView <TextView
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
style="@style/ChatRoom.Name.TextView" style="@style/ChatRoom.Name.TextView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="@dimen/text_view_drawable_padding"
android:ellipsize="end" android:ellipsize="end"
android:lines="1" android:lines="1"
android:maxLines="1" android:maxLines="1"
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:textDirection="locale"
tools:text="This is a very, very, very long filename, to test how the layout will work on very very very long filenames.pdf" /> tools:text="This is a very, very, very long filename, to test how the layout will work on very very very long filenames.pdf" />
<include <include
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingBottom="@dimen/member_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/member_item_top_and_bottom_padding">
<LinearLayout
android:id="@+id/image_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_file_thumbnail"
android:layout_width="80dp"
android:layout_height="70dp"
android:visibility="gone"
app:roundedCornerRadius="3dp" />
<ImageView
android:id="@+id/image_file_media_thumbnail"
android:layout_width="80dp"
android:layout_height="70dp"
android:visibility="gone" />
</LinearLayout>
<TextView
android:id="@+id/text_file_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/colorPrimaryText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_container"
app:layout_constraintTop_toTopOf="@+id/image_container"
tools:text="File.mp3" />
<TextView
android:id="@+id/text_uploader"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/colorSecondaryText"
app:layout_constraintBottom_toTopOf="@+id/text_upload_date"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_container"
tools:text="\@filipe.brito" />
<TextView
android:id="@+id/text_upload_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textColor="@color/colorSecondaryTextLight"
app:layout_constraintBottom_toBottomOf="@+id/image_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_container"
tools:text="Ma 22, 2018 6:42 PM" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -13,21 +13,13 @@ ...@@ -13,21 +13,13 @@
android:paddingStart="@dimen/screen_edge_left_and_right_padding" android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding"> android:paddingTop="@dimen/message_item_top_and_bottom_padding">
<include
android:id="@+id/layout_avatar"
layout="@layout/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif" />
<LinearLayout <LinearLayout
android:id="@+id/new_messages_notif" android:id="@+id/new_messages_notif"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone" android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"> tools:visibility="visible">
...@@ -38,13 +30,13 @@ ...@@ -38,13 +30,13 @@
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@color/red" /> android:background="@color/colorRed" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/msg_unread_messages" android:text="@string/msg_unread_messages"
android:textColor="@color/red" /> android:textColor="@color/colorRed" />
<View <View
android:layout_width="0dp" android:layout_width="0dp"
...@@ -52,16 +44,25 @@ ...@@ -52,16 +44,25 @@
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
android:layout_weight="1" android:layout_weight="1"
android:background="@color/red" /> android:background="@color/colorRed" />
</LinearLayout> </LinearLayout>
<include
android:id="@+id/layout_avatar"
layout="@layout/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="5dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif" />
<LinearLayout <LinearLayout
android:id="@+id/top_container" android:id="@+id/top_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:orientation="horizontal" android:orientation="horizontal"
app:layout_constraintLeft_toRightOf="@+id/layout_avatar" app:layout_constraintStart_toEndOf="@+id/layout_avatar"
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif"> app:layout_constraintTop_toBottomOf="@+id/new_messages_notif">
<TextView <TextView
...@@ -108,9 +109,10 @@ ...@@ -108,9 +109,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:layout_marginBottom="2dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="@+id/top_container" app:layout_constraintStart_toStartOf="@+id/top_container"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_container" app:layout_constraintTop_toBottomOf="@+id/top_container"
android:textDirection="locale"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" /> tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<include <include
......
...@@ -23,31 +23,27 @@ ...@@ -23,31 +23,27 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<LinearLayout <TextView
android:id="@+id/top_container" android:id="@+id/text_sender"
android:layout_width="0dp" style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:textColor="@color/colorPrimary"
android:orientation="horizontal" tools:text="Ronald Perkins"
app:layout_constraintLeft_toRightOf="@+id/quote_bar" app:layout_constraintStart_toEndOf="@+id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/new_messages_notif"> app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="8dp" />
<TextView
android:id="@+id/text_sender"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorPrimary"
tools:text="Ronald Perkins" />
<TextView <TextView
android:id="@+id/text_message_time" android:id="@+id/text_message_time"
style="@style/Timestamp.TextView" style="@style/Timestamp.TextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
tools:text="11:45 PM" /> tools:text="11:45 PM"
</LinearLayout> app:layout_constraintStart_toEndOf="@+id/text_sender"
app:layout_constraintTop_toTopOf="@+id/text_sender"
app:layout_constraintBottom_toBottomOf="@+id/text_sender"/>
<TextView <TextView
android:id="@+id/text_content" android:id="@+id/text_content"
...@@ -57,8 +53,8 @@ ...@@ -57,8 +53,8 @@
android:ellipsize="end" android:ellipsize="end"
android:singleLine="true" android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/top_container" app:layout_constraintStart_toStartOf="@+id/text_sender"
app:layout_constraintTop_toBottomOf="@+id/top_container" app:layout_constraintTop_toBottomOf="@+id/text_sender"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" /> tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<include <include
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="2" android:maxLines="2"
android:textColor="@color/white" android:textColor="@color/colorWhite"
android:textSize="14sp" android:textSize="14sp"
android:typeface="normal" android:typeface="normal"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
android:id="@+id/audio_video_attachment" android:id="@+id/audio_video_attachment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="150dp" android:layout_height="150dp"
android:background="@color/black" android:background="@color/colorBlack"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
......
...@@ -21,10 +21,10 @@ ...@@ -21,10 +21,10 @@
android:id="@+id/text_room_is_read_only" android:id="@+id/text_room_is_read_only"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="45dp" android:layout_height="45dp"
android:background="@color/white" android:background="@color/colorWhite"
android:gravity="center" android:gravity="center"
android:text="@string/msg_this_room_is_read_only" android:text="@string/msg_this_room_is_read_only"
android:textColor="@color/black" android:textColor="@color/colorBlack"
android:visibility="gone" android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/divider" /> app:layout_constraintTop_toBottomOf="@+id/divider" />
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
android:id="@+id/button_join_chat" android:id="@+id/button_join_chat"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="45dp" android:layout_height="45dp"
android:background="@color/white" android:background="@color/colorWhite"
android:text="@string/action_join_chat" android:text="@string/action_join_chat"
android:textColor="@color/colorAccent" android:textColor="@color/colorAccent"
android:visibility="gone" android:visibility="gone"
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
android:theme="@style/Theme.AppCompat" android:theme="@style/Theme.AppCompat"
android:tint="@color/gray_material" android:tint="@color/gray_material"
android:visibility="invisible" android:visibility="invisible"
app:backgroundTint="@color/white" app:backgroundTint="@color/colorWhite"
app:fabSize="mini" app:fabSize="mini"
app:layout_anchor="@id/recycler_view" app:layout_anchor="@id/recycler_view"
app:layout_anchorGravity="bottom|end" /> app:layout_anchorGravity="bottom|end" />
......
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
android:id="@+id/image_preview" android:id="@+id/image_preview"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="50dp" android:layout_height="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:actualImageScaleType="centerCrop" /> app:actualImageScaleType="centerCrop" />
<TextView <TextView
...@@ -23,6 +25,7 @@ ...@@ -23,6 +25,7 @@
android:textColor="@color/colorSecondaryText" android:textColor="@color/colorSecondaryText"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_preview" app:layout_constraintStart_toEndOf="@+id/image_preview"
android:textDirection="locale"
tools:text="www.uol.com.br" /> tools:text="www.uol.com.br" />
<TextView <TextView
...@@ -33,6 +36,7 @@ ...@@ -33,6 +36,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host" app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_host" app:layout_constraintTop_toBottomOf="@id/text_host"
android:textDirection="locale"
tools:text="Web page title" /> tools:text="Web page title" />
<TextView <TextView
...@@ -42,6 +46,7 @@ ...@@ -42,6 +46,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host" app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_title" app:layout_constraintTop_toBottomOf="@id/text_title"
android:textDirection="locale"
tools:text="description" /> tools:text="description" />
<include <include
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:textColor="@color/white" android:textColor="@color/colorWhite"
app:layout_constraintBottom_toBottomOf="@+id/image_user_status" app:layout_constraintBottom_toBottomOf="@+id/image_user_status"
app:layout_constraintEnd_toStartOf="@+id/image_account_expand" app:layout_constraintEnd_toStartOf="@+id/image_account_expand"
app:layout_constraintStart_toEndOf="@+id/image_user_status" app:layout_constraintStart_toEndOf="@+id/image_user_status"
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@color/white" android:textColor="@color/colorWhite"
app:layout_constraintEnd_toStartOf="@+id/image_account_expand" app:layout_constraintEnd_toStartOf="@+id/image_account_expand"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_user_name" app:layout_constraintTop_toBottomOf="@+id/text_user_name"
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/ic_expand_more_24dp" android:src="@drawable/ic_expand_more_24dp"
android:tint="@color/white" android:tint="@color/colorWhite"
app:layout_constraintBottom_toBottomOf="@+id/text_server_url" app:layout_constraintBottom_toBottomOf="@+id/text_server_url"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="@color/black" android:textColor="@color/colorBlack"
android:textSize="14sp" android:textSize="14sp"
tools:text="/leave" /> tools:text="/leave" />
......
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