Commit 16b3a106 authored by pcforgeek's avatar pcforgeek

Fix Merge Conflict

parents bab39f4b 676b84c0
...@@ -59,6 +59,7 @@ dependencies { ...@@ -59,6 +59,7 @@ dependencies {
implementation libraries.design implementation libraries.design
implementation libraries.constraintLayout implementation libraries.constraintLayout
implementation libraries.cardView implementation libraries.cardView
implementation libraries.flexbox
implementation libraries.androidKtx implementation libraries.androidKtx
......
...@@ -41,7 +41,12 @@ ...@@ -41,7 +41,12 @@
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity <activity
android:name=".webview.WebViewActivity" android:name=".webview.ui.WebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity
android:name=".webview.cas.ui.CasWebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
...@@ -55,6 +60,10 @@ ...@@ -55,6 +60,10 @@
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity
android:name=".settings.password.ui.PasswordActivity"
android:theme="@style/AppTheme" />
<receiver <receiver
android:name="com.google.android.gms.gcm.GcmReceiver" android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true" android:exported="true"
......
...@@ -4,19 +4,20 @@ import chat.rocket.android.authentication.domain.model.TokenModel ...@@ -4,19 +4,20 @@ import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.generateRandomString
import chat.rocket.android.util.extensions.isEmailValid import chat.rocket.android.util.extensions.isEmailValid
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatTwoFactorException import chat.rocket.common.RocketChatTwoFactorException
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.login import chat.rocket.core.internal.rest.*
import chat.rocket.core.internal.rest.loginWithEmail import kotlinx.coroutines.experimental.delay
import chat.rocket.core.internal.rest.me import java.util.concurrent.TimeUnit
import chat.rocket.core.internal.rest.registerPushToken
import javax.inject.Inject import javax.inject.Inject
class LoginPresenter @Inject constructor(private val view: LoginView, class LoginPresenter @Inject constructor(private val view: LoginView,
...@@ -30,51 +31,69 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -30,51 +31,69 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
// TODO - we should validate the current server when opening the app, and have a nonnull get() // TODO - we should validate the current server when opening the app, and have a nonnull get()
private val client: RocketChatClient = factory.create(serverInteractor.get()!!) private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
fun setup() { fun setupView() {
val server = serverInteractor.get() val server = serverInteractor.get()
if (server == null) { if (server == null) {
navigator.toServerScreen() navigator.toServerScreen()
return return
} }
val settings = settingsInteractor.get(server) val settings = settingsInteractor.get(server)
if (settings == null) {
navigator.toServerScreen() if (settings.isLoginFormEnabled()) {
return view.showFormView()
view.setupLoginButtonListener()
view.setupGlobalListener()
} else {
view.hideFormView()
} }
view.showSignUpView(settings.registrationEnabled()) if (settings.isRegistrationEnabledForNewUsers()) {
view.showSignUpView()
view.setupSignUpView()
}
var hasSocial = false if (settings.isCasAuthenticationEnabled()) {
if (settings.facebookEnabled()) { val token = generateRandomString(17)
view.setupCasButtonListener(UrlHelper.getCasUrl(settings.casLoginUrl(), server, token), token)
view.showCasButton()
}
var totalSocialAccountsEnabled = 0
if (settings.isFacebookAuthenticationEnabled()) {
view.enableLoginByFacebook() view.enableLoginByFacebook()
hasSocial = true totalSocialAccountsEnabled++
} }
if (settings.githubEnabled()) { if (settings.isGithubAuthenticationEnabled()) {
view.enableLoginByGithub() view.enableLoginByGithub()
hasSocial = true totalSocialAccountsEnabled++
} }
if (settings.googleEnabled()) { if (settings.isGoogleAuthenticationEnabled()) {
view.enableLoginByGoogle() view.enableLoginByGoogle()
hasSocial = true totalSocialAccountsEnabled++
} }
if (settings.linkedinEnabled()) { if (settings.isLinkedinAuthenticationEnabled()) {
view.enableLoginByLinkedin() view.enableLoginByLinkedin()
hasSocial = true totalSocialAccountsEnabled++
} }
if (settings.meteorEnabled()) { if (settings.isMeteorAuthenticationEnabled()) {
view.enableLoginByMeteor() view.enableLoginByMeteor()
hasSocial = true totalSocialAccountsEnabled++
} }
if (settings.twitterEnabled()) { if (settings.isTwitterAuthenticationEnabled()) {
view.enableLoginByTwitter() view.enableLoginByTwitter()
hasSocial = true totalSocialAccountsEnabled++
} }
if (settings.gitlabEnabled()) { if (settings.isGitlabAuthenticationEnabled()) {
view.enableLoginByGitlab() view.enableLoginByGitlab()
hasSocial = true totalSocialAccountsEnabled++
}
if (totalSocialAccountsEnabled > 0) {
view.showOauthView()
if (totalSocialAccountsEnabled > 3) {
view.setupFabListener()
}
} }
view.showOauthView(hasSocial)
} }
fun authenticate(usernameOrEmail: String, password: String) { fun authenticate(usernameOrEmail: String, password: String) {
...@@ -92,21 +111,21 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -92,21 +111,21 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
else -> { else -> {
launchUI(strategy) { launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) { if (NetworkHelper.hasInternetAccess()) {
view.disableUserInput()
view.showLoading() view.showLoading()
try { try {
val token = if (usernameOrEmail.isEmailValid()) { val token = if (usernameOrEmail.isEmailValid()) {
client.loginWithEmail(usernameOrEmail, password) client.loginWithEmail(usernameOrEmail, password)
} else {
val settings = settingsInteractor.get(server)
if (settings.isLdapAuthenticationEnabled()) {
client.loginWithLdap(usernameOrEmail, password)
} else { } else {
client.login(usernameOrEmail, password) client.login(usernameOrEmail, password)
} }
}
val me = client.me() saveToken(server, TokenModel(token.userId, token.authToken), client.me().username)
multiServerRepository.save(
server,
TokenModel(token.userId, token.authToken)
)
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
...@@ -124,6 +143,7 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -124,6 +143,7 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
} }
} finally { } finally {
view.hideLoading() view.hideLoading()
view.enableUserInput()
} }
} else { } else {
view.showNoInternetConnection() view.showNoInternetConnection()
...@@ -133,8 +153,46 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -133,8 +153,46 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
} }
} }
fun authenticateWithCas(casToken: String) {
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.disableUserInput()
view.showLoading()
try {
val server = serverInteractor.get()
if (server != null) {
delay(3, TimeUnit.SECONDS)
val token = client.loginWithCas(casToken)
saveToken(server, TokenModel(token.userId, token.authToken), client.me().username)
registerPushToken()
navigator.toChatList()
} else {
navigator.toServerScreen()
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
view.enableUserInput()
}
} else {
view.showNoInternetConnection()
}
}
}
fun signup() = navigator.toSignUp() fun signup() = navigator.toSignUp()
private suspend fun saveToken(server: String, tokenModel: TokenModel, username: String?) {
multiServerRepository.save(server, tokenModel)
localRepository.save(LocalRepository.USERNAME_KEY, username)
registerPushToken()
}
private suspend fun registerPushToken() { private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let { localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it) client.registerPushToken(it)
......
...@@ -7,62 +7,135 @@ import chat.rocket.android.core.behaviours.MessageView ...@@ -7,62 +7,135 @@ import chat.rocket.android.core.behaviours.MessageView
interface LoginView : LoadingView, MessageView, InternetView { interface LoginView : LoadingView, MessageView, InternetView {
/** /**
* Shows the oauth view if the server settings allow the login via social accounts. * Shows the form view (i.e the username/email and password fields) if it is enabled by the server settings.
* *
* REMARK: we must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle], * REMARK: We must set up the login button listener [setupLoginButtonListener].
* Remember to enable [enableUserInput] or disable [disableUserInput] the view interaction for the user when submitting the form.
*/
fun showFormView()
/**
* Hides the form view.
*/
fun hideFormView()
/**
* Setups the login button when tapped.
*/
fun setupLoginButtonListener()
/**
* Enables the view interactions for the user.
*/
fun enableUserInput()
/**
* Disables the view interactions for the user.
*/
fun disableUserInput()
/**
* Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server settings.
*
* REMARK: We must set up the CAS button listener [setupCasButtonListener].
*/
fun showCasButton()
/**
* Hides the CAS button.
*/
fun hideCasButton()
/**
* Setups the CAS button when tapped.
*
* @param casUrl The CAS URL to login/sign up with.
* @param casToken The requested Token sent to the CAS server.
*/
fun setupCasButtonListener(casUrl: String, casToken: String)
/**
* Shows the sign up view if the new users registration is enabled by the server settings.
*
* REMARK: We must set up the sign up view listener [setupSignUpView].
*/
fun showSignUpView()
/**
* Setups the sign up view when tapped.
*/
fun setupSignUpView()
/**
* Hides the sign up view.
*/
fun hideSignUpView()
/**
* Shows the oauth view if the login via social accounts is enabled by the server settings.
*
* REMARK: We must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle],
* [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter] or [enableLoginByGitlab]) for the oauth view. * [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter] or [enableLoginByGitlab]) for the oauth view.
* If the possibility of login via social accounts exceeds 3 different ways we should set up the FAB ([setupFabListener]) to show the remaining view(s). * If the possibility of login via social accounts exceeds 3 different ways we should set up the FAB ([setupFabListener]) to show the remaining view(s).
*
* @param value True to show the oauth view, false otherwise.
*/ */
fun showOauthView(value: Boolean) fun showOauthView()
/** /**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)). * Hides the oauth view.
*/ */
fun setupFabListener() fun hideOauthView()
/**
* Shows the login button.
*/
fun showLoginButton()
/**
* Hides the login button.
*/
fun hideLoginButton()
/** /**
* Shows the login by Facebook view. * Shows the "login by Facebook view if it is enabled by the server settings.
*/ */
fun enableLoginByFacebook() fun enableLoginByFacebook()
/** /**
* Shows the login by Github view. * Shows the "login by Github" view if it is enabled by the server settings.
*/ */
fun enableLoginByGithub() fun enableLoginByGithub()
/** /**
* Shows the login by Google view. * Shows the "login by Google" view if it is enabled by the server settings.
*/ */
fun enableLoginByGoogle() fun enableLoginByGoogle()
/** /**
* Shows the login by Linkedin view. * Shows the "login by Linkedin" view if it is enabled by the server settings.
*/ */
fun enableLoginByLinkedin() fun enableLoginByLinkedin()
/** /**
* Shows the login by Meteor view. * Shows the "login by Meteor" view if it is enabled by the server settings.
*/ */
fun enableLoginByMeteor() fun enableLoginByMeteor()
/** /**
* Shows the login by Twitter view. * Shows the "login by Twitter" view if it is enabled by the server settings.
*/ */
fun enableLoginByTwitter() fun enableLoginByTwitter()
/** /**
* Shows the login by Gitlab view. * Shows the "login by Gitlab" view if it is enabled by the server settings.
*/ */
fun enableLoginByGitlab() fun enableLoginByGitlab()
/** /**
* Shows the sign up view if the server settings allow the new users registration. * Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*
* @param value True to show the sign up view, false otherwise.
*/ */
fun showSignUpView(value: Boolean) fun setupFabListener()
fun setupGlobalListener()
/** /**
* Alerts the user about a wrong inputted username or email. * Alerts the user about a wrong inputted username or email.
......
...@@ -9,7 +9,7 @@ import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment ...@@ -9,7 +9,7 @@ import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.util.extensions.addFragmentBackStack import chat.rocket.android.util.extensions.addFragmentBackStack
import chat.rocket.android.webview.webViewIntent import chat.rocket.android.webview.ui.webViewIntent
class AuthenticationNavigator(internal val activity: AuthenticationActivity, internal val context: Context) { class AuthenticationNavigator(internal val activity: AuthenticationActivity, internal val context: Context) {
...@@ -37,10 +37,7 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int ...@@ -37,10 +37,7 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int
} }
fun toChatList() { fun toChatList() {
val chatList = Intent(activity, MainActivity::class.java).apply { activity.startActivity(Intent(activity, MainActivity::class.java))
//TODO any parameter to pass
}
activity.startActivity(chatList)
activity.finish() activity.finish()
} }
......
...@@ -17,16 +17,14 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -17,16 +17,14 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val refreshSettingsInteractor: RefreshSettingsInteractor) { private val refreshSettingsInteractor: RefreshSettingsInteractor) {
fun connect(server: String) { fun connect(server: String) {
if (!UrlHelper.isValidUrl(server)) { if (!UrlHelper.isValidUrl(server)) {
view.showInvalidServerUrl() view.showInvalidServerUrlMessage()
} else { } else {
launchUI(strategy) { launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) { if (NetworkHelper.hasInternetAccess()) {
view.showLoading() view.showLoading()
try { try {
refreshSettingsInteractor.refresh(server) refreshSettingsInteractor.refresh(server)
serverInteractor.save(server) serverInteractor.save(server)
navigator.toLogin() navigator.toLogin()
} catch (ex: Exception) { } catch (ex: Exception) {
ex.message?.let { ex.message?.let {
......
...@@ -7,7 +7,7 @@ import chat.rocket.android.core.behaviours.MessageView ...@@ -7,7 +7,7 @@ import chat.rocket.android.core.behaviours.MessageView
interface ServerView : LoadingView, MessageView, InternetView { interface ServerView : LoadingView, MessageView, InternetView {
/** /**
* Notifies the user about an invalid inputted server URL. * Shows an invalid server URL message.
*/ */
fun showInvalidServerUrl() fun showInvalidServerUrlMessage()
} }
\ No newline at end of file
...@@ -30,7 +30,8 @@ class ServerFragment : Fragment(), ServerView { ...@@ -30,7 +30,8 @@ class ServerFragment : Fragment(), ServerView {
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_server) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
container?.inflate(R.layout.fragment_authentication_server)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
...@@ -43,7 +44,7 @@ class ServerFragment : Fragment(), ServerView { ...@@ -43,7 +44,7 @@ class ServerFragment : Fragment(), ServerView {
relative_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener) relative_layout.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
} }
override fun showInvalidServerUrl() = showMessage(getString(R.string.msg_invalid_server_url)) override fun showInvalidServerUrlMessage() = showMessage(getString(R.string.msg_invalid_server_url))
override fun showLoading() { override fun showLoading() {
enableUserInput(false) enableUserInput(false)
......
package chat.rocket.android.authentication.signup.ui package chat.rocket.android.authentication.signup.ui
import DrawableHelper import DrawableHelper
import android.content.Context
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
...@@ -11,19 +10,15 @@ import android.widget.Toast ...@@ -11,19 +10,15 @@ import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
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.AnimationHelper
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.showToast
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
class SignupFragment : Fragment(), SignupView { class SignupFragment : Fragment(), SignupView {
@Inject lateinit var presenter: SignupPresenter @Inject lateinit var presenter: SignupPresenter
@Inject lateinit var appContext: Context
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(constraint_layout.rootView)) { if (KeyboardHelper.isSoftKeyboardShown(constraint_layout.rootView)) {
...@@ -68,26 +63,26 @@ class SignupFragment : Fragment(), SignupView { ...@@ -68,26 +63,26 @@ class SignupFragment : Fragment(), SignupView {
} }
override fun alertBlankName() { override fun alertBlankName() {
AnimationHelper.vibrateSmartPhone(appContext) vibrateSmartPhone()
AnimationHelper.shakeView(text_name) text_name.shake()
text_name.requestFocus() text_name.requestFocus()
} }
override fun alertBlankUsername() { override fun alertBlankUsername() {
AnimationHelper.vibrateSmartPhone(appContext) vibrateSmartPhone()
AnimationHelper.shakeView(text_username) text_username.shake()
text_username.requestFocus() text_username.requestFocus()
} }
override fun alertEmptyPassword() { override fun alertEmptyPassword() {
AnimationHelper.vibrateSmartPhone(appContext) vibrateSmartPhone()
AnimationHelper.shakeView(text_password) text_password.shake()
text_password.requestFocus() text_password.requestFocus()
} }
override fun alertBlankEmail() { override fun alertBlankEmail() {
AnimationHelper.vibrateSmartPhone(appContext) vibrateSmartPhone()
AnimationHelper.shakeView(text_email) text_email.shake()
text_email.requestFocus() text_email.requestFocus()
} }
...@@ -114,16 +109,18 @@ class SignupFragment : Fragment(), SignupView { ...@@ -114,16 +109,18 @@ class SignupFragment : Fragment(), SignupView {
} }
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, appContext) activity?.apply {
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, appContext) val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, appContext) val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, appContext) val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, this)
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable) val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.wrapDrawables(drawables) DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, appContext, R.color.colorDrawableTintGrey) DrawableHelper.tintDrawables(drawables, this, 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)
......
...@@ -12,10 +12,7 @@ import android.view.inputmethod.InputMethodManager ...@@ -12,10 +12,7 @@ import android.view.inputmethod.InputMethodManager
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter
import chat.rocket.android.authentication.twofactor.presentation.TwoFAView import chat.rocket.android.authentication.twofactor.presentation.TwoFAView
import chat.rocket.android.helper.AnimationHelper import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.showToast
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_two_fa.* import kotlinx.android.synthetic.main.fragment_authentication_two_fa.*
import javax.inject.Inject import javax.inject.Inject
...@@ -65,10 +62,8 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -65,10 +62,8 @@ class TwoFAFragment : Fragment(), TwoFAView {
} }
override fun alertBlankTwoFactorAuthenticationCode() { override fun alertBlankTwoFactorAuthenticationCode() {
activity?.let { vibrateSmartPhone()
AnimationHelper.vibrateSmartPhone(it) text_two_factor_auth.shake()
AnimationHelper.shakeView(text_two_factor_auth)
}
} }
override fun alertInvalidTwoFactorAuthenticationCode() = showMessage(getString(R.string.msg_invalid_2fa_code)) override fun alertInvalidTwoFactorAuthenticationCode() = showMessage(getString(R.string.msg_invalid_2fa_code))
......
...@@ -4,10 +4,13 @@ import android.view.View ...@@ -4,10 +4,13 @@ import android.view.View
import chat.rocket.android.chatroom.viewmodel.AudioAttachmentViewModel import chat.rocket.android.chatroom.viewmodel.AudioAttachmentViewModel
import chat.rocket.android.player.PlayerActivity import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_attachment.view.* import kotlinx.android.synthetic.main.message_attachment.view.*
class AudioAttachmentViewHolder(itemView: View, listener: ActionsListener) class AudioAttachmentViewHolder(itemView: View,
: BaseViewHolder<AudioAttachmentViewModel>(itemView, listener) { listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<AudioAttachmentViewModel>(itemView, listener, reactionListener) {
init { init {
with(itemView) { with(itemView) {
......
...@@ -7,14 +7,20 @@ import chat.rocket.android.R ...@@ -7,14 +7,20 @@ import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
import chat.rocket.android.chatroom.viewmodel.BaseViewModel import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage import chat.rocket.core.model.isSystemMessage
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexboxLayoutManager
import ru.whalemare.sheetmenu.extension.inflate import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList import ru.whalemare.sheetmenu.extension.toList
abstract class BaseViewHolder<T : BaseViewModel<*>>( abstract class BaseViewHolder<T : BaseViewModel<*>>(
itemView: View, itemView: View,
private val listener: ActionsListener private val listener: ActionsListener,
var reactionListener: EmojiReactionListener? = null
) : RecyclerView.ViewHolder(itemView), ) : RecyclerView.ViewHolder(itemView),
MenuItem.OnMenuItemClickListener { MenuItem.OnMenuItemClickListener {
var data: T? = null var data: T? = null
...@@ -26,6 +32,39 @@ abstract class BaseViewHolder<T : BaseViewModel<*>>( ...@@ -26,6 +32,39 @@ abstract class BaseViewHolder<T : BaseViewModel<*>>(
fun bind(data: T) { fun bind(data: T) {
this.data = data this.data = data
bindViews(data) bindViews(data)
bindReactions()
}
private fun bindReactions() {
data?.let {
val recyclerView = itemView.findViewById(R.id.recycler_view_reactions) as RecyclerView
val adapter: MessageReactionsAdapter
if (recyclerView.adapter == null) {
adapter = MessageReactionsAdapter()
} else {
adapter = recyclerView.adapter as MessageReactionsAdapter
adapter.clear()
}
if (it.nextDownStreamMessage == null) {
adapter.listener = object : EmojiReactionListener {
override fun onReactionTouched(messageId: String, emojiShortname: String) {
reactionListener?.onReactionTouched(messageId, emojiShortname)
}
override fun onReactionAdded(messageId: String, emoji: Emoji) {
if (!adapter.contains(emoji.shortname)) {
reactionListener?.onReactionAdded(messageId, emoji)
}
}
}
val context = itemView.context
val manager = FlexboxLayoutManager(context, FlexDirection.ROW)
recyclerView.layoutManager = manager
recyclerView.adapter = adapter
adapter.addReactions(it.reactions.filterNot { it.unicode.startsWith(":") })
}
}
} }
abstract fun bindViews(data: T) abstract fun bindViews(data: T)
......
...@@ -7,7 +7,9 @@ import chat.rocket.android.R ...@@ -7,7 +7,9 @@ import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.viewmodel.* import chat.rocket.android.chatroom.viewmodel.*
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException import java.security.InvalidParameterException
...@@ -15,7 +17,8 @@ class ChatRoomAdapter( ...@@ -15,7 +17,8 @@ class ChatRoomAdapter(
private val roomType: String, private val roomType: String,
private val roomName: String, private val roomName: String,
private val presenter: ChatRoomPresenter?, private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() { ) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseViewModel<*>>() private val dataSet = ArrayList<BaseViewModel<*>>()
...@@ -25,26 +28,26 @@ class ChatRoomAdapter( ...@@ -25,26 +28,26 @@ class ChatRoomAdapter(
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return when(viewType.toViewType()) { return when (viewType.toViewType()) {
BaseViewModel.ViewType.MESSAGE -> { BaseViewModel.ViewType.MESSAGE -> {
val view = parent.inflate(R.layout.item_message) val view = parent.inflate(R.layout.item_message)
MessageViewHolder(view, actionsListener) MessageViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.IMAGE_ATTACHMENT -> { BaseViewModel.ViewType.IMAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment) val view = parent.inflate(R.layout.message_attachment)
ImageAttachmentViewHolder(view, actionsListener) ImageAttachmentViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.AUDIO_ATTACHMENT -> { BaseViewModel.ViewType.AUDIO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment) val view = parent.inflate(R.layout.message_attachment)
AudioAttachmentViewHolder(view, actionsListener) AudioAttachmentViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.VIDEO_ATTACHMENT -> { BaseViewModel.ViewType.VIDEO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment) val view = parent.inflate(R.layout.message_attachment)
VideoAttachmentViewHolder(view, actionsListener) VideoAttachmentViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.URL_PREVIEW -> { BaseViewModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview) val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view, actionsListener) UrlPreviewViewHolder(view, actionsListener, reactionListener)
} }
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
...@@ -61,6 +64,23 @@ class ChatRoomAdapter( ...@@ -61,6 +64,23 @@ class ChatRoomAdapter(
} }
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) { override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
if (holder !is MessageViewHolder) {
if (position + 1 < itemCount) {
val messageAbove = dataSet[position + 1]
if (messageAbove.messageId == dataSet[position].messageId) {
messageAbove.nextDownStreamMessage = dataSet[position]
}
}
} else {
if (position == 0) {
dataSet[0].nextDownStreamMessage = null
} else if (position - 1 > 0) {
if (dataSet[position - 1].messageId != dataSet[position].messageId) {
dataSet[position].nextDownStreamMessage = null
}
}
}
when (holder) { when (holder) {
is MessageViewHolder -> holder.bind(dataSet[position] as MessageViewModel) is MessageViewHolder -> holder.bind(dataSet[position] as MessageViewModel)
is ImageAttachmentViewHolder -> holder.bind(dataSet[position] as ImageAttachmentViewModel) is ImageAttachmentViewHolder -> holder.bind(dataSet[position] as ImageAttachmentViewModel)
...@@ -86,16 +106,32 @@ class ChatRoomAdapter( ...@@ -86,16 +106,32 @@ class ChatRoomAdapter(
} }
fun prependData(dataSet: List<BaseViewModel<*>>) { fun prependData(dataSet: List<BaseViewModel<*>>) {
val item = dataSet.firstOrNull { newItem ->
this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1
}
if (item == null) {
this.dataSet.addAll(0, dataSet) this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size) notifyItemRangeInserted(0, dataSet.size)
} }
}
fun updateItem(message: BaseViewModel<*>) { fun updateItem(message: BaseViewModel<*>) {
val index = dataSet.indexOfLast { it.messageId == message.messageId } var index = dataSet.indexOfLast { it.messageId == message.messageId }
val indexOfFirst = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index") Timber.d("index: $index")
if (index > -1) { if (index > -1) {
message.nextDownStreamMessage = dataSet[index].nextDownStreamMessage
dataSet[index] = message dataSet[index] = message
notifyItemChanged(index) notifyItemChanged(index)
while (dataSet[index].nextDownStreamMessage != null) {
dataSet[index].nextDownStreamMessage!!.reactions = message.reactions
notifyItemChanged(--index)
}
// Delete message only if current is a system message update, i.e.: Message Removed
if (message.message.isSystemMessage() && indexOfFirst > -1 && indexOfFirst != index) {
dataSet.removeAt(indexOfFirst)
notifyItemRemoved(indexOfFirst)
}
} }
} }
...@@ -131,6 +167,7 @@ class ChatRoomAdapter( ...@@ -131,6 +167,7 @@ class ChatRoomAdapter(
} }
} }
} }
R.id.action_menu_msg_react -> presenter?.showReactions(id)
else -> TODO("Not implemented") else -> TODO("Not implemented")
} }
} }
......
...@@ -2,11 +2,14 @@ package chat.rocket.android.chatroom.adapter ...@@ -2,11 +2,14 @@ package chat.rocket.android.chatroom.adapter
import android.view.View import android.view.View
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import com.stfalcon.frescoimageviewer.ImageViewer import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.* import kotlinx.android.synthetic.main.message_attachment.view.*
class ImageAttachmentViewHolder(itemView: View, listener: ActionsListener) class ImageAttachmentViewHolder(itemView: View,
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener) { listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
init { init {
with(itemView) { with(itemView) {
......
package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.ReactionViewModel
import chat.rocket.android.dagger.DaggerLocalComponent
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiListenerAdapter
import chat.rocket.android.widget.emoji.EmojiPickerPopup
import chat.rocket.android.widget.emoji.EmojiReactionListener
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val REACTION_VIEW_TYPE = 0
private const val ADD_REACTION_VIEW_TYPE = 1
}
private val reactions = CopyOnWriteArrayList<ReactionViewModel>()
var listener: EmojiReactionListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view: View
return when (viewType) {
ADD_REACTION_VIEW_TYPE -> {
view = inflater.inflate(R.layout.item_add_reaction, parent, false)
AddReactionViewHolder(view, listener)
}
else -> {
view = inflater.inflate(R.layout.item_reaction, parent, false)
SingleReactionViewHolder(view, listener)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is SingleReactionViewHolder) {
holder.bind(reactions[position])
} else {
holder as AddReactionViewHolder
holder.bind(reactions[0].messageId)
}
}
override fun getItemCount() = if (reactions.isEmpty()) 0 else reactions.size + 1
override fun getItemViewType(position: Int): Int {
if (position == reactions.size) {
return ADD_REACTION_VIEW_TYPE
}
return REACTION_VIEW_TYPE
}
fun addReactions(reactions: List<ReactionViewModel>) {
this.reactions.clear()
this.reactions.addAllAbsent(reactions)
notifyItemRangeInserted(0, reactions.size)
}
fun clear() {
val oldSize = reactions.size
reactions.clear()
notifyItemRangeRemoved(0, oldSize)
}
fun contains(reactionShortname: String) =
reactions.firstOrNull { it.shortname == reactionShortname} != null
class SingleReactionViewHolder(view: View,
private val listener: EmojiReactionListener?)
: RecyclerView.ViewHolder(view), View.OnClickListener {
@Inject lateinit var localRepository: LocalRepository
@Volatile lateinit var reaction: ReactionViewModel
@Volatile
var clickHandled = false
init {
DaggerLocalComponent.builder()
.context(itemView.context)
.build()
.inject(this)
}
fun bind(reaction: ReactionViewModel) {
clickHandled = false
this.reaction = reaction
with(itemView) {
val emojiTextView = findViewById<TextView>(R.id.text_emoji)
val countTextView = findViewById<TextView>(R.id.text_count)
emojiTextView.text = reaction.unicode
countTextView.text = reaction.count.toString()
val myself = localRepository.get(LocalRepository.USERNAME_KEY)
if (reaction.usernames.contains(myself)) {
val context = itemView.context
val resources = context.resources
countTextView.setTextColor(resources.getColor(R.color.colorAccent))
}
emojiTextView.setOnClickListener(this@SingleReactionViewHolder)
countTextView.setOnClickListener(this@SingleReactionViewHolder)
}
}
override fun onClick(v: View?) {
synchronized(this) {
if (!clickHandled) {
clickHandled = true
listener?.onReactionTouched(reaction.messageId, reaction.shortname)
}
}
}
}
class AddReactionViewHolder(view: View,
private val listener: EmojiReactionListener?) : RecyclerView.ViewHolder(view) {
fun bind(messageId: String) {
itemView as ImageView
itemView.setOnClickListener {
val emojiPickerPopup = EmojiPickerPopup(itemView.context)
emojiPickerPopup.listener = object : EmojiListenerAdapter() {
override fun onEmojiAdded(emoji: Emoji) {
listener?.onReactionAdded(messageId, emoji)
}
}
emojiPickerPopup.show()
}
}
}
}
\ No newline at end of file
...@@ -3,13 +3,15 @@ package chat.rocket.android.chatroom.adapter ...@@ -3,13 +3,15 @@ package chat.rocket.android.chatroom.adapter
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.avatar.view.* import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.* import kotlinx.android.synthetic.main.item_message.view.*
class MessageViewHolder( class MessageViewHolder(
itemView: View, itemView: View,
listener: ActionsListener listener: ActionsListener,
) : BaseViewHolder<MessageViewModel>(itemView, listener) { reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageViewModel>(itemView, listener, reactionListener) {
init { init {
with(itemView) { with(itemView) {
...@@ -25,6 +27,9 @@ class MessageViewHolder( ...@@ -25,6 +27,9 @@ class MessageViewHolder(
day_marker_layout.visibility = View.VISIBLE day_marker_layout.visibility = View.VISIBLE
} else day_marker_layout.visibility = View.GONE } else day_marker_layout.visibility = View.GONE
if (data.isFirstUnread) new_messages_notif.visibility = View.VISIBLE
else new_messages_notif.visibility = View.GONE
text_message_time.text = data.time text_message_time.text = data.time
text_sender.text = data.senderName text_sender.text = data.senderName
text_content.text = data.content text_content.text = data.content
......
...@@ -6,10 +6,13 @@ import android.view.View ...@@ -6,10 +6,13 @@ import android.view.View
import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel
import chat.rocket.android.util.extensions.content import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_url_preview.view.* import kotlinx.android.synthetic.main.message_url_preview.view.*
class UrlPreviewViewHolder(itemView: View, listener: ActionsListener) class UrlPreviewViewHolder(itemView: View,
: BaseViewHolder<UrlPreviewViewModel>(itemView, listener) { listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<UrlPreviewViewModel>(itemView, listener, reactionListener) {
init { init {
with(itemView) { with(itemView) {
......
...@@ -4,10 +4,13 @@ import android.view.View ...@@ -4,10 +4,13 @@ import android.view.View
import chat.rocket.android.chatroom.viewmodel.VideoAttachmentViewModel import chat.rocket.android.chatroom.viewmodel.VideoAttachmentViewModel
import chat.rocket.android.player.PlayerActivity import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_attachment.view.* import kotlinx.android.synthetic.main.message_attachment.view.*
class VideoAttachmentViewHolder(itemView: View, listener: ActionsListener) class VideoAttachmentViewHolder(itemView: View,
: BaseViewHolder<VideoAttachmentViewModel>(itemView, listener) { listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<VideoAttachmentViewModel>(itemView, listener, reactionListener) {
init { init {
with(itemView) { with(itemView) {
......
...@@ -342,6 +342,23 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -342,6 +342,23 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
fun toMembersList(chatRoomId: String, chatRoomType: String) = navigator.toMembersList(chatRoomId, chatRoomType) fun toMembersList(chatRoomId: String, chatRoomType: String) = navigator.toMembersList(chatRoomId, chatRoomType)
/**
* Send an emoji reaction to a message.
*/
fun react(messageId: String, emoji: String) {
launchUI(strategy) {
try {
client.toggleReaction(messageId, emoji.removeSurrounding(":"))
} catch (ex: RocketChatException) {
Timber.e(ex)
}
}
}
fun showReactions(messageId: String) {
view.showReactionsPopup(messageId)
}
private fun updateMessage(streamedMessage: Message) { private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) { launchUI(strategy) {
val viewModelStreamedMessage = mapper.map(streamedMessage) val viewModelStreamedMessage = mapper.map(streamedMessage)
......
...@@ -100,4 +100,5 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -100,4 +100,5 @@ interface ChatRoomView : LoadingView, MessageView {
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int) fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
fun showConnectionState(state: State) fun showConnectionState(state: State)
fun showReactionsPopup(messageId: String)
} }
\ No newline at end of file
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.graphics.drawable.Drawable
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
...@@ -10,20 +12,24 @@ import chat.rocket.android.server.domain.GetCurrentServerInteractor ...@@ -10,20 +12,24 @@ import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.addFragment import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.* import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject import javax.inject.Inject
import timber.log.Timber
fun Context.chatRoomIntent(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Intent { fun Context.chatRoomIntent(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean, chatRoomLastSeen: Long): Intent {
return Intent(this, ChatRoomActivity::class.java).apply { return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId) putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName) putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType) putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
putExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly) putExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
} }
} }
...@@ -31,6 +37,7 @@ private const val INTENT_CHAT_ROOM_ID = "chat_room_id" ...@@ -31,6 +37,7 @@ private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name" private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type" private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only" private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
...@@ -43,6 +50,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -43,6 +50,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false private var isChatRoomReadOnly: Boolean = false
private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this) AndroidInjection.inject(this)
...@@ -66,8 +74,10 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -66,8 +74,10 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
setupToolbar() setupToolbar()
chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
addFragment("ChatRoomFragment", R.id.fragment_container) { addFragment("ChatRoomFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly) newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen)
} }
} }
...@@ -82,6 +92,29 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -82,6 +92,29 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
private fun setupToolbar() { private fun setupToolbar() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName
val roomType = roomTypeOf(chatRoomType)
val drawable = when (roomType) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, this)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, this)
}
is RoomType.DirectMessage -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, this)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.white)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
}
toolbar.setNavigationOnClickListener { toolbar.setNavigationOnClickListener {
finishActivity() finishActivity()
} }
......
...@@ -24,27 +24,27 @@ import chat.rocket.android.helper.EndlessRecyclerViewScrollListener ...@@ -24,27 +24,27 @@ 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.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.emoji.ComposerEditText import chat.rocket.android.widget.emoji.*
import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiKeyboardPopup
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.State
import chat.rocket.core.model.Message
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_chat_room.* import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.* import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.item_chat.*
import kotlinx.android.synthetic.main.message_composer.* import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.* import kotlinx.android.synthetic.main.message_list.*
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Fragment { fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean, chatRoomLastSeen: Long): Fragment {
return ChatRoomFragment().apply { return ChatRoomFragment().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_NAME, chatRoomName) putString(BUNDLE_CHAT_ROOM_NAME, chatRoomName)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType) putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
putBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly) putBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putLong(BUNDLE_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
} }
} }
} }
...@@ -54,8 +54,9 @@ private const val BUNDLE_CHAT_ROOM_NAME = "chat_room_name" ...@@ -54,8 +54,9 @@ private const val BUNDLE_CHAT_ROOM_NAME = "chat_room_name"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type" private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only" private const val BUNDLE_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val REQUEST_CODE_FOR_PERFORM_SAF = 42 private const val REQUEST_CODE_FOR_PERFORM_SAF = 42
private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
@Inject lateinit var presenter: ChatRoomPresenter @Inject lateinit var presenter: ChatRoomPresenter
@Inject lateinit var parser: MessageParser @Inject lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter private lateinit var adapter: ChatRoomAdapter
...@@ -65,7 +66,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -65,7 +66,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var isChatRoomReadOnly: Boolean = false private var isChatRoomReadOnly: Boolean = false
private var chatRoomLastSeen: Long = -1
private lateinit var actionSnackbar: ActionSnackbar private lateinit var actionSnackbar: ActionSnackbar
private var citation: String? = null private var citation: String? = null
private var editingMessageId: String? = null private var editingMessageId: String? = null
...@@ -90,6 +91,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -90,6 +91,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
chatRoomName = bundle.getString(BUNDLE_CHAT_ROOM_NAME) chatRoomName = bundle.getString(BUNDLE_CHAT_ROOM_NAME)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE) chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
isChatRoomReadOnly = bundle.getBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY) isChatRoomReadOnly = bundle.getBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY)
chatRoomLastSeen = bundle.getLong(BUNDLE_CHAT_ROOM_LAST_SEEN)
} else { } else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" } requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
} }
...@@ -153,10 +155,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -153,10 +155,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
} }
override fun showMessages(dataSet: List<BaseViewModel<*>>) { override fun showMessages(dataSet: List<BaseViewModel<*>>) {
// track the message sent immediately after the current message
var prevMessageViewModel : MessageViewModel? = null
var prevMsgModel = dataSet[0] var prevMsgModel = dataSet[0]
//checking for all messages to assign true to the required showDayMaker //checking for all messages to assign true to the required showDayMaker
//Loop over received messages to determine first unread
for (i in dataSet.indices) { for (i in dataSet.indices) {
val msgModel = dataSet[i] val msgModel = dataSet[i]
if(i>0){ if(i>0){
...@@ -170,11 +175,25 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -170,11 +175,25 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
prevMsgModel.showDayMarker = true prevMsgModel.showDayMarker = true
} }
} }
if (msgModel is MessageViewModel){
val msg = msgModel.rawData
if (msg.timestamp < chatRoomLastSeen) {
// This message was sent before the last seen of the room. Hence, it was seen.
// if there is a message after (below) this, mark it firstUnread.
if (prevMessageViewModel != null) {
prevMessageViewModel.isFirstUnread = true
}
break
}
prevMessageViewModel = msgModel
}
} }
activity?.apply { activity?.apply {
if (recycler_view.adapter == null) { if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter) adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter,
reactionListener = this@ChatRoomFragment)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
linearLayoutManager.stackFromEnd = true linearLayoutManager.stackFromEnd = true
...@@ -191,6 +210,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -191,6 +210,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
val oldMessagesCount = adapter.itemCount val oldMessagesCount = adapter.itemCount
adapter.appendData(dataSet) adapter.appendData(dataSet)
recycler_view.scrollToPosition(92)
if (oldMessagesCount == 0 && dataSet.isNotEmpty()) { if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
} }
...@@ -234,6 +254,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -234,6 +254,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
override fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>) { override fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>) {
adapter.updateItem(message.last()) adapter.updateItem(message.last())
if (message.size > 1) {
adapter.prependData(listOf(message.first()))
}
} }
override fun dispatchDeleteMessage(msgId: String) { override fun dispatchDeleteMessage(msgId: String) {
...@@ -300,6 +323,26 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -300,6 +323,26 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
} }
} }
override fun onReactionTouched(messageId: String, emojiShortname: String) {
presenter.react(messageId, emojiShortname)
}
override fun onReactionAdded(messageId: String, emoji: Emoji) {
presenter.react(messageId, emoji.shortname)
}
override fun showReactionsPopup(messageId: String) {
context?.let {
val emojiPickerPopup = EmojiPickerPopup(it)
emojiPickerPopup.listener = object : EmojiListenerAdapter() {
override fun onEmojiAdded(emoji: Emoji) {
onReactionAdded(messageId, emoji)
}
}
emojiPickerPopup.show()
}
}
private fun setReactionButtonIcon(@DrawableRes drawableId: Int) { private fun setReactionButtonIcon(@DrawableRes drawableId: Int) {
button_add_reaction.setImageResource(drawableId) button_add_reaction.setImageResource(drawableId)
button_add_reaction.setTag(drawableId) button_add_reaction.setTag(drawableId)
......
...@@ -10,8 +10,10 @@ data class AudioAttachmentViewModel( ...@@ -10,8 +10,10 @@ data class AudioAttachmentViewModel(
override val messageId: String, override val messageId: String,
override val attachmentUrl: String, override val attachmentUrl: String,
override val attachmentTitle: CharSequence, override val attachmentTitle: CharSequence,
override val id: Long override val id: Long,
) : BaseFileAttachmentViewModel<AudioAttachment> { override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
override val layoutId: Int override val layoutId: Int
......
...@@ -9,6 +9,8 @@ interface BaseViewModel<out T> { ...@@ -9,6 +9,8 @@ interface BaseViewModel<out T> {
val messageId: String val messageId: String
val viewType: Int val viewType: Int
val layoutId: Int val layoutId: Int
var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>?
enum class ViewType(val viewType: Int) { enum class ViewType(val viewType: Int) {
MESSAGE(0), MESSAGE(0),
......
...@@ -10,7 +10,9 @@ data class ImageAttachmentViewModel( ...@@ -10,7 +10,9 @@ data class ImageAttachmentViewModel(
override val messageId: String, override val messageId: String,
override val attachmentUrl: String, override val attachmentUrl: String,
override val attachmentTitle: CharSequence, override val attachmentTitle: CharSequence,
override val id: Long override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseFileAttachmentViewModel<ImageAttachment> { ) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
...@@ -13,7 +13,10 @@ data class MessageViewModel( ...@@ -13,7 +13,10 @@ data class MessageViewModel(
override val content: CharSequence, override val content: CharSequence,
override val isPinned: Boolean, override val isPinned: Boolean,
var currentDayMarkerText: String, var currentDayMarkerText: String,
var showDayMarker: Boolean var showDayMarker: Boolean,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var isFirstUnread: Boolean
) : 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
data class ReactionViewModel(
val messageId: String,
val shortname: String,
val unicode: CharSequence,
val count: Int,
val usernames: List<String> = emptyList()
)
\ No newline at end of file
...@@ -11,7 +11,9 @@ data class UrlPreviewViewModel( ...@@ -11,7 +11,9 @@ data class UrlPreviewViewModel(
val title: CharSequence?, val title: CharSequence?,
val hostname: String, val hostname: String,
val description: CharSequence?, val description: CharSequence?,
val thumbUrl: String? val thumbUrl: String?,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseViewModel<Url> { ) : BaseViewModel<Url> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
...@@ -10,7 +10,9 @@ data class VideoAttachmentViewModel( ...@@ -10,7 +10,9 @@ data class VideoAttachmentViewModel(
override val messageId: String, override val messageId: String,
override val attachmentUrl: String, override val attachmentUrl: String,
override val attachmentTitle: CharSequence, override val attachmentTitle: CharSequence,
override val id: Long override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseFileAttachmentViewModel<VideoAttachment> { ) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
...@@ -14,6 +14,7 @@ import chat.rocket.android.helper.MessageParser ...@@ -14,6 +14,7 @@ import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.TokenRepository import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType import chat.rocket.core.model.MessageType
...@@ -83,7 +84,8 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -83,7 +84,8 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val title = url.meta?.title val title = url.meta?.title
val description = url.meta?.description val description = url.meta?.description
return UrlPreviewViewModel(message, url, message.id, title, hostname, description, thumb) return UrlPreviewViewModel(message, url, message.id, title, hostname, description, thumb,
getReactions(message))
} }
private fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? { private fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? {
...@@ -99,11 +101,11 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -99,11 +101,11 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val id = "${message.id}_${attachment.titleLink}".hashCode().toLong() val id = "${message.id}_${attachment.titleLink}".hashCode().toLong()
return when (attachment) { return when (attachment) {
is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id, is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id) attachmentUrl, attachmentTitle ?: "", id, getReactions(message))
is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id, is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id) attachmentUrl, attachmentTitle ?: "", id, getReactions(message))
is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id, is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id) attachmentUrl, attachmentTitle ?: "", id, getReactions(message))
else -> null else -> null
} }
} }
...@@ -138,7 +140,9 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -138,7 +140,9 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val quoteUrl = HttpUrl.parse(url.url) val quoteUrl = HttpUrl.parse(url.url)
val serverUrl = HttpUrl.parse(baseUrl) val serverUrl = HttpUrl.parse(baseUrl)
if (quoteUrl != null && serverUrl != null) { if (quoteUrl != null && serverUrl != null) {
quote = makeQuote(quoteUrl, serverUrl) quote = makeQuote(quoteUrl, serverUrl)?.let {
getMessageWithoutQuoteMarkdown(it)
}
} }
} }
} }
...@@ -154,7 +158,34 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -154,7 +158,34 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val content = getContent(context, message, quote) val content = getContent(context, message, quote)
MessageViewModel(message = message, rawData = message, messageId = message.id, MessageViewModel(message = message, rawData = message, messageId = message.id,
avatar = avatar!!, time = time, senderName = sender, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned,currentDayMarkerText = dayMarkerText,showDayMarker = showDayMarker) content = content, isPinned = message.pinned,currentDayMarkerText = dayMarkerText,showDayMarker = showDayMarker,
reactions = getReactions(message), isFirstUnread = false)
}
private fun getReactions(message: Message): List<ReactionViewModel> {
val reactions = message.reactions?.let {
val list = mutableListOf<ReactionViewModel>()
it.getShortNames().forEach { shortname ->
val usernames = it.getUsernames(shortname) ?: emptyList()
val count = usernames.size
list.add(
ReactionViewModel(messageId = message.id,
shortname = shortname,
unicode = EmojiParser.parse(shortname),
count = count,
usernames = usernames)
)
}
list
}
return reactions ?: emptyList()
}
private fun getMessageWithoutQuoteMarkdown(message: Message): Message {
val baseUrl = settings.baseUrl()
return message.copy(
message = message.message.replace("\\[\\s\\]\\($baseUrl.*\\)".toRegex(), "").trim()
)
} }
private fun getSenderName(message: Message): CharSequence { private fun getSenderName(message: Message): CharSequence {
...@@ -201,7 +232,7 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -201,7 +232,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
var quoteViewModel: MessageViewModel? = null var quoteViewModel: MessageViewModel? = null
if (quote != null) { if (quote != null) {
val quoteMessage: Message = quote val quoteMessage: Message = quote
quoteViewModel = map(quoteMessage).first { it is MessageViewModel } as MessageViewModel quoteViewModel = mapMessage(quoteMessage)
} }
return parser.renderMarkdown(message.message, quoteViewModel, currentUsername) return parser.renderMarkdown(message.message, quoteViewModel, currentUsername)
} }
......
...@@ -3,11 +3,9 @@ package chat.rocket.android.chatrooms.di ...@@ -3,11 +3,9 @@ package chat.rocket.android.chatrooms.di
import android.arch.lifecycle.LifecycleOwner import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatrooms.presentation.ChatRoomsView import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
@PerFragment @PerFragment
...@@ -22,9 +20,4 @@ class ChatRoomsFragmentModule { ...@@ -22,9 +20,4 @@ class ChatRoomsFragmentModule {
fun provideLifecycleOwner(frag: ChatRoomsFragment): LifecycleOwner { fun provideLifecycleOwner(frag: ChatRoomsFragment): LifecycleOwner {
return frag return frag
} }
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
package chat.rocket.android.chatrooms.di
import android.content.Context
import chat.rocket.android.chatrooms.presentation.ChatRoomsNavigator
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module
import dagger.Provides
@Module
class ChatRoomsModule {
@Provides
@PerActivity
fun provideChatRoomsNavigator(activity: MainActivity, context: Context) = ChatRoomsNavigator(activity, context)
}
\ No newline at end of file
package chat.rocket.android.chatrooms.presentation
import android.content.Context
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.main.ui.MainActivity
class ChatRoomsNavigator(private val activity: MainActivity, private val context: Context) {
fun toChatRoom(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean) {
activity.startActivity(context.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.presentation package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManager import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
...@@ -24,7 +25,7 @@ import javax.inject.Inject ...@@ -24,7 +25,7 @@ import javax.inject.Inject
class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: ChatRoomsNavigator, private val navigator: MainNavigator,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor, private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor, private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
...@@ -68,7 +69,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -68,7 +69,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
} }
navigator.toChatRoom(chatRoom.id, roomName, navigator.toChatRoom(chatRoom.id, roomName,
chatRoom.type.toString(), chatRoom.readonly ?: false) chatRoom.type.toString(), chatRoom.readonly ?: false, chatRoom.lastSeen ?: -1)
} }
/** /**
......
package chat.rocket.android.chatrooms.ui package chat.rocket.android.chatrooms.ui
import DateTimeHelper import DateTimeHelper
import DrawableHelper
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
...@@ -72,10 +74,30 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -72,10 +74,30 @@ class ChatRoomsAdapter(private val context: Context,
} }
private fun bindName(chatRoom: ChatRoom, textView: TextView) { private fun bindName(chatRoom: ChatRoom, textView: TextView) {
if (chatRoom.type is RoomType.DirectMessage && settings.useRealName()) { textView.textContent = chatRoom.name
textView.content = chatRoom.fullName ?: chatRoom.name
} else { var drawable = when (chatRoom.type) {
textView.content = chatRoom.name is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, context)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, context)
}
is RoomType.DirectMessage -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, context)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
val color = when (chatRoom.alert || chatRoom.unread > 0) {
true -> R.color.colorPrimaryText
false -> R.color.colorSecondaryText
}
DrawableHelper.tintDrawable(mutableDrawable, context, color)
DrawableHelper.compoundDrawable(textView, mutableDrawable)
} }
} }
......
package chat.rocket.android.dagger
import android.content.Context
import chat.rocket.android.chatroom.adapter.MessageReactionsAdapter
import chat.rocket.android.dagger.module.LocalModule
import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton
@Singleton
@Component(modules = [LocalModule::class])
interface LocalComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun context(applicationContext: Context): Builder
fun build(): LocalComponent
}
fun inject(adapter: MessageReactionsAdapter.SingleReactionViewHolder)
fun inject(adapter: MessageReactionsAdapter.AddReactionViewHolder)
/*@Component.Builder
abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/
}
...@@ -11,13 +11,13 @@ import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider ...@@ -11,13 +11,13 @@ 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.chatroom.ui.PinnedMessagesActivity import chat.rocket.android.chatroom.ui.PinnedMessagesActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
import chat.rocket.android.chatrooms.di.ChatRoomsModule
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.main.di.MainActivityProvider
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.profile.di.ProfileFragmentProvider import chat.rocket.android.profile.di.ProfileFragmentProvider
import chat.rocket.android.settings.password.di.PasswordFragmentProvider
import chat.rocket.android.settings.password.ui.PasswordActivity
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
...@@ -35,8 +35,6 @@ abstract class ActivityBuilder { ...@@ -35,8 +35,6 @@ abstract class ActivityBuilder {
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [MainModule::class, @ContributesAndroidInjector(modules = [MainModule::class,
MainActivityProvider::class,
ChatRoomsModule::class,
ChatRoomsFragmentProvider::class, ChatRoomsFragmentProvider::class,
ProfileFragmentProvider::class ProfileFragmentProvider::class
]) ])
...@@ -49,4 +47,8 @@ abstract class ActivityBuilder { ...@@ -49,4 +47,8 @@ abstract class ActivityBuilder {
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [PinnedMessagesFragmentProvider::class]) @ContributesAndroidInjector(modules = [PinnedMessagesFragmentProvider::class])
abstract fun bindPinnedMessagesActivity(): PinnedMessagesActivity abstract fun bindPinnedMessagesActivity(): PinnedMessagesActivity
@PerActivity
@ContributesAndroidInjector(modules = [PasswordFragmentProvider::class])
abstract fun bindPasswordActivity(): PasswordActivity
} }
\ No newline at end of file
package chat.rocket.android.dagger.module
import android.content.Context
import android.content.SharedPreferences
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class LocalModule {
@Provides
fun provideSharedPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
}
@Provides
@Singleton
fun provideLocalRepository(prefs: SharedPreferences): LocalRepository {
return SharedPrefsLocalRepository(prefs)
}
}
\ No newline at end of file
package chat.rocket.android.helper
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.View
object AnimationHelper {
/**
* Shakes a view.
*/
fun shakeView(viewToShake: View, x: Float = 2F, num: Int = 0) {
if (num == 6) {
viewToShake.translationX = 0.toFloat()
return
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(ObjectAnimator.ofFloat(viewToShake, "translationX", dp(viewToShake.context, x)))
animatorSet.duration = 50
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
shakeView(viewToShake, if (num == 5) 0.toFloat() else -x, num + 1)
}
})
animatorSet.start()
}
/**
* Vibrates the smart phone.
*/
fun vibrateSmartPhone(context: Context) {
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
if (Build.VERSION.SDK_INT >= 26) {
vibrator.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
vibrator.vibrate(200)
}
}
private fun dp(context: Context, value: Float): Float {
val density = context.resources.displayMetrics.density
val result = Math.ceil(density.times(value.toDouble()))
return result.toFloat()
}
}
\ No newline at end of file
...@@ -59,6 +59,7 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -59,6 +59,7 @@ class MessageParser @Inject constructor(val context: Application, private val co
parentNode.appendChild(quoteNode) parentNode.appendChild(quoteNode)
quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length)) quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length))
quoteNode = parser.parse("> ${toLenientMarkdown(quote.rawData.message)}") quoteNode = parser.parse("> ${toLenientMarkdown(quote.rawData.message)}")
quoteNode.accept(EmojiVisitor(builder))
quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder)) quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
} }
parentNode.accept(LinkVisitor(builder)) parentNode.accept(LinkVisitor(builder))
......
...@@ -13,6 +13,17 @@ object UrlHelper { ...@@ -13,6 +13,17 @@ object UrlHelper {
*/ */
fun getAvatarUrl(serverUrl: String, avatarName: String): String = removeTrailingSlash(serverUrl) + "/avatar/" + avatarName fun getAvatarUrl(serverUrl: String, avatarName: String): String = removeTrailingSlash(serverUrl) + "/avatar/" + avatarName
/**
* Returns the CAS URL.
*
* @param casLoginUrl The CAS login URL from the server settings.
* @param serverUrl The server URL.
* @param token The token to be send to the CAS server.
* @return The avatar URL.
*/
fun getCasUrl(casLoginUrl: String, serverUrl: String, token: String): String =
removeTrailingSlash(casLoginUrl) + "?service=" + removeTrailingSlash(serverUrl) + "/_cas/" + token
/** /**
* Returns the server's Terms of Service URL. * Returns the server's Terms of Service URL.
* *
......
package chat.rocket.android.main.di
import chat.rocket.android.main.ui.MainActivity
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class MainActivityProvider {
@ContributesAndroidInjector(modules = [MainActivityModule::class])
abstract fun provideMainActivity(): MainActivity
}
\ No newline at end of file
package chat.rocket.android.main.di package chat.rocket.android.main.di
import android.arch.lifecycle.LifecycleOwner
import android.content.Context import android.content.Context
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.main.presentation.MainNavigator import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class MainModule { class MainModule {
...@@ -13,4 +17,13 @@ class MainModule { ...@@ -13,4 +17,13 @@ class MainModule {
@Provides @Provides
@PerActivity @PerActivity
fun provideMainNavigator(activity: MainActivity, context: Context) = MainNavigator(activity, context) fun provideMainNavigator(activity: MainActivity, context: Context) = MainNavigator(activity, context)
@Provides
fun provideMainView(activity: MainActivity): MainView = activity
@Provides
fun provideLifecycleOwner(activity: MainActivity): LifecycleOwner = activity
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy = CancelStrategy(owner, jobs)
} }
\ No newline at end of file
...@@ -2,9 +2,11 @@ package chat.rocket.android.main.presentation ...@@ -2,9 +2,11 @@ package chat.rocket.android.main.presentation
import android.content.Context import android.content.Context
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.ui.ProfileFragment import chat.rocket.android.profile.ui.ProfileFragment
import chat.rocket.android.settings.ui.SettingsFragment
import chat.rocket.android.util.extensions.addFragment import chat.rocket.android.util.extensions.addFragment
class MainNavigator(internal val activity: MainActivity, internal val context: Context) { class MainNavigator(internal val activity: MainActivity, internal val context: Context) {
...@@ -20,4 +22,15 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C ...@@ -20,4 +22,15 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C
ProfileFragment.newInstance() ProfileFragment.newInstance()
} }
} }
fun toSettings() {
activity.addFragment("SettingsFragment", R.id.fragment_container) {
SettingsFragment.newInstance()
}
}
fun toChatRoom(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean, chatRoomLastSeen: Long) {
activity.startActivity(context.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
} }
\ No newline at end of file
package chat.rocket.android.main.presentation package chat.rocket.android.main.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
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.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.disconnect import chat.rocket.core.internal.realtime.disconnect
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.unregisterPushToken import chat.rocket.core.internal.rest.unregisterPushToken
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MainPresenter @Inject constructor(private val navigator: MainNavigator, class MainPresenter @Inject constructor(private val view: MainView,
private val strategy: CancelStrategy,
private val navigator: MainNavigator,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
managerFactory: ConnectionManagerFactory, managerFactory: ConnectionManagerFactory,
...@@ -24,23 +29,27 @@ class MainPresenter @Inject constructor(private val navigator: MainNavigator, ...@@ -24,23 +29,27 @@ class MainPresenter @Inject constructor(private val navigator: MainNavigator,
fun toUserProfile() = navigator.toUserProfile() fun toUserProfile() = navigator.toUserProfile()
fun toSettings() = navigator.toSettings()
/** /**
* Logout from current server. * Logout from current server.
*/ */
fun logout() { fun logout() {
// TODO: inject CancelStrategy, and MainView. launchUI(strategy) {
// launchUI(strategy) {
try { try {
// clearTokens() clearTokens()
// client.logout() client.logout()
//TODO: Add the code to unsubscribe to all subscriptions. //TODO: Add the code to unsubscribe to all subscriptions.
client.disconnect() client.disconnect()
// view.onLogout() view.onLogout()
} catch (e: RocketChatException) { } catch (exception: RocketChatException) {
Timber.e(e) exception.message?.let {
// view.showMessage(e.message!!) view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
} }
// }
} }
private suspend fun clearTokens() { private suspend fun clearTokens() {
......
package chat.rocket.android.main.ui package chat.rocket.android.main.ui
import android.app.Activity
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
...@@ -14,12 +15,14 @@ import chat.rocket.android.util.extensions.showToast ...@@ -14,12 +15,14 @@ import chat.rocket.android.util.extensions.showToast
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.support.HasSupportFragmentInjector import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar.* import kotlinx.android.synthetic.main.app_bar.*
import javax.inject.Inject import javax.inject.Inject
class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector { class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupportFragmentInjector {
@Inject lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject lateinit var presenter: MainPresenter @Inject lateinit var presenter: MainPresenter
private var isFragmentAdded: Boolean = false private var isFragmentAdded: Boolean = false
...@@ -62,6 +65,8 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector { ...@@ -62,6 +65,8 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun activityInjector(): AndroidInjector<Activity> = activityDispatchingAndroidInjector
override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector
private fun setupToolbar() { private fun setupToolbar() {
...@@ -89,6 +94,9 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector { ...@@ -89,6 +94,9 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector {
R.id.action_profile -> { R.id.action_profile -> {
presenter.toUserProfile() presenter.toUserProfile()
} }
R.id.action_settings -> {
presenter.toSettings()
}
R.id.action_logout -> { R.id.action_logout -> {
presenter.logout() presenter.logout()
} }
......
package chat.rocket.android.member.ui
import android.os.Bundle
import android.support.design.widget.BottomSheetDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import kotlinx.android.synthetic.main.fragment_member_bottom_sheet.*
fun newInstance(avatarUri: String, realName: String, username: String, email: String, utcOffset: String): BottomSheetDialogFragment {
return MemberBottomSheetFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_AVATAR_URI, avatarUri)
putString(BUNDLE_REAL_NAME, realName)
putString(BUNDLE_USERNAME, username)
putString(BUNDLE_EMAIL, email)
putString(BUNDLE_UTC_OFFSET, utcOffset)
}
}
}
private const val BUNDLE_AVATAR_URI = "avatar_uri"
private const val BUNDLE_REAL_NAME = "real_name"
private const val BUNDLE_USERNAME = "username"
private const val BUNDLE_EMAIL = "email"
private const val BUNDLE_UTC_OFFSET = "utc_offset"
class MemberBottomSheetFragment: BottomSheetDialogFragment() {
private lateinit var avatarUri: String
private lateinit var realName: String
private lateinit var username: String
private lateinit var email: String
private lateinit var utcOffset: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val bundle = arguments
if (bundle != null) {
avatarUri = bundle.getString(BUNDLE_AVATAR_URI)
realName = bundle.getString(BUNDLE_REAL_NAME)
username = bundle.getString(BUNDLE_USERNAME)
email = bundle.getString(BUNDLE_EMAIL)
utcOffset = bundle.getString(BUNDLE_UTC_OFFSET)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_member_bottom_sheet, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showMemberDetails()
}
private fun showMemberDetails() {
image_bottom_sheet_avatar.setImageURI(avatarUri)
text_bottom_sheet_member_name.content = realName
text_bottom_sheet_member_username.content = username
if (email.isNotEmpty()) {
text_member_email_address.textContent = email
} else {
text_email_address.setVisible(false)
text_member_email_address.setVisible(false)
}
if (utcOffset.isNotEmpty()){
text_member_utc.content = utcOffset
} else {
text_utc.setVisible(false)
text_member_utc.setVisible(false)
}
}
}
\ No newline at end of file
package chat.rocket.android.members.di package chat.rocket.android.members.di
import android.arch.lifecycle.LifecycleOwner import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.members.presentation.MembersNavigator
import chat.rocket.android.members.presentation.MembersView import chat.rocket.android.members.presentation.MembersView
import chat.rocket.android.members.ui.MembersFragment import chat.rocket.android.members.ui.MembersFragment
import dagger.Module import dagger.Module
...@@ -13,6 +15,9 @@ import kotlinx.coroutines.experimental.Job ...@@ -13,6 +15,9 @@ import kotlinx.coroutines.experimental.Job
@PerFragment @PerFragment
class MembersFragmentModule { class MembersFragmentModule {
@Provides
fun provideChatRoomNavigator(activity: ChatRoomActivity) = MembersNavigator(activity)
@Provides @Provides
fun membersView(frag: MembersFragment): MembersView { fun membersView(frag: MembersFragment): MembersView {
return frag return frag
......
package chat.rocket.android.members.presentation
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.member.ui.newInstance
class MembersNavigator(internal val activity: ChatRoomActivity) {
fun toMemberDetails(avatarUri: String, realName: String, username: String, email: String, utcOffset: String) {
activity.apply {
newInstance(avatarUri, realName, username, email, utcOffset)
.show(supportFragmentManager, "MemberBottomSheetFragment")
}
}
}
package chat.rocket.android.members.presentation 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.MemberViewModelMapper import chat.rocket.android.members.viewmodel.MemberViewModelMapper
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
...@@ -13,6 +14,7 @@ import chat.rocket.core.internal.rest.getMembers ...@@ -13,6 +14,7 @@ import chat.rocket.core.internal.rest.getMembers
import javax.inject.Inject import javax.inject.Inject
class MembersPresenter @Inject constructor(private val view: MembersView, class MembersPresenter @Inject constructor(private val view: MembersView,
private val navigator: MembersNavigator,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory, factory: RocketChatClientFactory,
...@@ -38,4 +40,14 @@ class MembersPresenter @Inject constructor(private val view: MembersView, ...@@ -38,4 +40,14 @@ class MembersPresenter @Inject constructor(private val view: MembersView,
} }
} }
} }
fun toMemberDetails(memberViewModel: MemberViewModel) {
val avatarUri = memberViewModel.avatarUri.toString()
val realName = memberViewModel.realName.toString()
val username = "@${memberViewModel.username}"
val email = memberViewModel.email ?: ""
val utcOffset = memberViewModel.utcOffset.toString()
navigator.toMemberDetails(avatarUri, realName, username, email, utcOffset)
}
} }
\ No newline at end of file
package chat.rocket.android.members.ui package chat.rocket.android.members.ui
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.BottomSheetBehavior
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.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
...@@ -16,11 +15,12 @@ import chat.rocket.android.members.adapter.MembersAdapter ...@@ -16,11 +15,12 @@ import chat.rocket.android.members.adapter.MembersAdapter
import chat.rocket.android.members.presentation.MembersPresenter import chat.rocket.android.members.presentation.MembersPresenter
import chat.rocket.android.members.presentation.MembersView import chat.rocket.android.members.presentation.MembersView
import chat.rocket.android.members.viewmodel.MemberViewModel import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.util.extensions.* 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.widget.DividerItemDecoration import chat.rocket.android.widget.DividerItemDecoration
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_members.* import kotlinx.android.synthetic.main.fragment_members.*
import kotlinx.android.synthetic.main.member_bottom_sheet.*
import javax.inject.Inject import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment { fun newInstance(chatRoomId: String, chatRoomType: String): Fragment {
...@@ -37,9 +37,8 @@ private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type" ...@@ -37,9 +37,8 @@ private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
class MembersFragment : Fragment(), MembersView { class MembersFragment : Fragment(), MembersView {
@Inject lateinit var presenter: MembersPresenter @Inject lateinit var presenter: MembersPresenter
private val adapter: MembersAdapter = MembersAdapter { memberViewModel -> showMemberDetails(memberViewModel) } private val adapter: MembersAdapter = MembersAdapter { memberViewModel -> presenter.toMemberDetails(memberViewModel) }
private val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) private val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
private val bottomSheetBehavior by lazy { BottomSheetBehavior.from(member_bottom_sheet) }
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
...@@ -72,7 +71,7 @@ class MembersFragment : Fragment(), MembersView { ...@@ -72,7 +71,7 @@ class MembersFragment : Fragment(), MembersView {
setupToolbar(total) setupToolbar(total)
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
adapter.prependData(dataSet) adapter.prependData(dataSet)
if (dataSet.size >= 60) { if (dataSet.size >= 59) { // TODO Check why the API retorns the specified count -1
recycler_view.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager) { recycler_view.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) { override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType, page * 60L) presenter.loadChatRoomsMembers(chatRoomId, chatRoomType, page * 60L)
...@@ -103,24 +102,6 @@ class MembersFragment : Fragment(), MembersView { ...@@ -103,24 +102,6 @@ class MembersFragment : Fragment(), MembersView {
} }
} }
private fun showMemberDetails(memberViewModel: MemberViewModel) {
image_bottom_sheet_avatar.setImageURI(memberViewModel.avatarUri)
text_bottom_sheet_member_name.content = memberViewModel.realName
text_bottom_sheet_member_username.content = "@${memberViewModel.username}"
val memberEmail = memberViewModel.email
if (memberEmail != null) {
text_member_email_address.textContent = memberEmail
} else {
text_email_address.setVisible(false)
text_member_email_address.setVisible(false)
}
text_member_utc.content = memberViewModel.utcOffset.toString()
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
private fun setupToolbar(totalMembers: Long) { private fun setupToolbar(totalMembers: Long) {
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_members, totalMembers)) (activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_members, totalMembers))
} }
......
...@@ -8,7 +8,7 @@ import chat.rocket.core.model.Value ...@@ -8,7 +8,7 @@ import chat.rocket.core.model.Value
import javax.inject.Inject import javax.inject.Inject
class MemberViewModelMapper @Inject constructor(serverInteractor: GetCurrentServerInteractor, getSettingsInteractor: GetSettingsInteractor) { class MemberViewModelMapper @Inject constructor(serverInteractor: GetCurrentServerInteractor, getSettingsInteractor: GetSettingsInteractor) {
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private val baseUrl = settings.baseUrl() private val baseUrl = settings.baseUrl()
fun mapToViewModelList(memberList: List<User>): List<MemberViewModel> { fun mapToViewModelList(memberList: List<User>): List<MemberViewModel> {
......
package chat.rocket.android.profile.di package chat.rocket.android.profile.di
import android.arch.lifecycle.LifecycleOwner import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.profile.presentation.ProfileView import chat.rocket.android.profile.presentation.ProfileView
import chat.rocket.android.profile.ui.ProfileFragment import chat.rocket.android.profile.ui.ProfileFragment
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
@PerFragment @PerFragment
...@@ -22,9 +20,4 @@ class ProfileFragmentModule { ...@@ -22,9 +20,4 @@ class ProfileFragmentModule {
fun provideLifecycleOwner(frag: ProfileFragment): LifecycleOwner { fun provideLifecycleOwner(frag: ProfileFragment): LifecycleOwner {
return frag return frag
} }
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
...@@ -9,6 +9,7 @@ import chat.rocket.common.RocketChatException ...@@ -9,6 +9,7 @@ import chat.rocket.common.RocketChatException
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.me import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.setAvatar
import chat.rocket.core.internal.rest.updateProfile import chat.rocket.core.internal.rest.updateProfile
import javax.inject.Inject import javax.inject.Inject
...@@ -45,10 +46,13 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView, ...@@ -45,10 +46,13 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
} }
} }
fun updateUserProfile(email: String, name: String, username: String) { fun updateUserProfile(email: String, name: String, username: String, avatarUrl: String = "") {
launchUI(strategy) { launchUI(strategy) {
view.showLoading() view.showLoading()
try { try {
if(avatarUrl!="") {
client.setAvatar(avatarUrl)
}
val user = client.updateProfile(myselfId, email, name, username) val user = client.updateProfile(myselfId, email, name, username)
view.showProfileUpdateSuccessfullyMessage() view.showProfileUpdateSuccessfullyMessage()
loadUserProfile() loadUserProfile()
......
...@@ -6,7 +6,6 @@ import android.os.Bundle ...@@ -6,7 +6,6 @@ import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
import android.view.* import android.view.*
import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.presentation.ProfilePresenter import chat.rocket.android.profile.presentation.ProfilePresenter
...@@ -24,6 +23,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -24,6 +23,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
private lateinit var currentName: String private lateinit var currentName: String
private lateinit var currentUsername: String private lateinit var currentUsername: String
private lateinit var currentEmail: String private lateinit var currentEmail: String
private lateinit var currentAvatar: String
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
companion object { companion object {
...@@ -54,10 +54,12 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -54,10 +54,12 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_name.textContent = name text_name.textContent = name
text_username.textContent = username text_username.textContent = username
text_email.textContent = email text_email.textContent = email
text_avatar_url.textContent = ""
currentName = name currentName = name
currentUsername = username currentUsername = username
currentEmail = email currentEmail = email
currentAvatar = avatarUrl
profile_container.setVisible(true) profile_container.setVisible(true)
...@@ -93,7 +95,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -93,7 +95,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean { override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
return when (menuItem.itemId) { return when (menuItem.itemId) {
R.id.action_profile -> { R.id.action_profile -> {
presenter.updateUserProfile(text_email.textContent, text_name.textContent, text_username.textContent) presenter.updateUserProfile(text_email.textContent, text_name.textContent, text_username.textContent, text_avatar_url.textContent)
mode.finish() mode.finish()
true true
} }
...@@ -116,17 +118,26 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -116,17 +118,26 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this) val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this) val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, this) val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, this)
val linkDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_link_black_24dp, this)
val drawables = arrayOf(personDrawable, atDrawable, emailDrawable) val drawables = arrayOf(personDrawable, atDrawable, emailDrawable, linkDrawable)
DrawableHelper.wrapDrawables(drawables) DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey) DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_email), drawables) DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_email, text_avatar_url), drawables)
} }
} }
private fun listenToChanges() { private fun listenToChanges() {
Observables.combineLatest(text_name.asObservable(), text_username.asObservable(), text_email.asObservable()).subscribe({ t -> Observables.combineLatest(text_name.asObservable(),
if (t.first.toString() != currentName || t.second.toString() != currentUsername || t.third.toString() != currentEmail) { text_username.asObservable(),
text_email.asObservable(),
text_avatar_url.asObservable()) { text_name, text_username, text_email, text_avatar_url ->
return@combineLatest (text_name.toString() != currentName ||
text_username.toString() !=currentUsername ||
text_email.toString() != currentEmail ||
(text_avatar_url.toString() != "" && text_avatar_url.toString()!= currentAvatar))
}.subscribe({ isValid->
if (isValid) {
startActionMode() startActionMode()
} else { } else {
finishActionMode() finishActionMode()
...@@ -146,5 +157,6 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -146,5 +157,6 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_name.isEnabled = value text_name.isEnabled = value
text_username.isEnabled = value text_username.isEnabled = value
text_email.isEnabled = value text_email.isEnabled = value
text_avatar_url.isEnabled = value
} }
} }
...@@ -11,13 +11,17 @@ class RefreshSettingsInteractor @Inject constructor(private val factory: RocketC ...@@ -11,13 +11,17 @@ class RefreshSettingsInteractor @Inject constructor(private val factory: RocketC
private val repository: SettingsRepository) { private val repository: SettingsRepository) {
private var settingsFilter = arrayOf( private var settingsFilter = arrayOf(
LDAP_ENABLE, CAS_ENABLE, CAS_LOGIN_URL,
ACCOUNT_REGISTRATION, ACCOUNT_LOGIN_FORM, ACCOUNT_PASSWORD_RESET, ACCOUNT_CUSTOM_FIELDS,
ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR,
ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, ACCOUNT_GITLAB,
SITE_URL, SITE_NAME, FAVICON_512, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS, SITE_URL, SITE_NAME, FAVICON_512, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS,
FAVORITE_ROOMS, ACCOUNT_LOGIN_FORM, ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB, FAVORITE_ROOMS, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE, UPLOAD_WHITELIST_MIMETYPES,
ACCOUNT_GITLAB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR, ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, HIDE_USER_JOIN, HIDE_USER_LEAVE,
LDAP_ENABLE, ACCOUNT_REGISTRATION, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE, HIDE_TYPE_AU, HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ALLOW_MESSAGE_DELETING,
UPLOAD_WHITELIST_MIMETYPES, HIDE_USER_JOIN, HIDE_USER_LEAVE, HIDE_TYPE_AU, HIDE_MUTE_UNMUTE, ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
HIDE_TYPE_RU, ACCOUNT_CUSTOM_FIELDS, ALLOW_MESSAGE_DELETING, ALLOW_MESSAGE_EDITING,
ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
suspend fun refresh(server: String) { suspend fun refresh(server: String) {
withContext(CommonPool) { withContext(CommonPool) {
......
...@@ -7,20 +7,25 @@ typealias PublicSettings = Map<String, Value<Any>> ...@@ -7,20 +7,25 @@ typealias PublicSettings = Map<String, Value<Any>>
interface SettingsRepository { interface SettingsRepository {
fun save(url: String, settings: PublicSettings) fun save(url: String, settings: PublicSettings)
fun get(url: String): PublicSettings? fun get(url: String): PublicSettings
} }
// Authentication methods.
const val LDAP_ENABLE = "LDAP_Enable"
const val CAS_ENABLE = "CAS_enabled"
const val CAS_LOGIN_URL = "CAS_login_url"
const val ACCOUNT_REGISTRATION = "Accounts_RegistrationForm"
const val ACCOUNT_LOGIN_FORM = "Accounts_ShowFormLogin"
const val ACCOUNT_PASSWORD_RESET = "Accounts_PasswordReset"
const val ACCOUNT_CUSTOM_FIELDS = "Accounts_CustomFields"
const val ACCOUNT_GOOGLE = "Accounts_OAuth_Google"
const val ACCOUNT_FACEBOOK = "Accounts_OAuth_Facebook" const val ACCOUNT_FACEBOOK = "Accounts_OAuth_Facebook"
const val ACCOUNT_GITHUB = "Accounts_OAuth_Github" const val ACCOUNT_GITHUB = "Accounts_OAuth_Github"
const val ACCOUNT_GITLAB = "Accounts_OAuth_Gitlab"
const val ACCOUNT_GOOGLE = "Accounts_OAuth_Google"
const val ACCOUNT_LINKEDIN = "Accounts_OAuth_Linkedin" const val ACCOUNT_LINKEDIN = "Accounts_OAuth_Linkedin"
const val ACCOUNT_METEOR = "Accounts_OAuth_Meteor" const val ACCOUNT_METEOR = "Accounts_OAuth_Meteor"
const val ACCOUNT_TWITTER = "Accounts_OAuth_Twitter" const val ACCOUNT_TWITTER = "Accounts_OAuth_Twitter"
const val ACCOUNT_WORDPRESS = "Accounts_OAuth_Wordpress" const val ACCOUNT_WORDPRESS = "Accounts_OAuth_Wordpress"
const val ACCOUNT_REGISTRATION = "Accounts_RegistrationForm" const val ACCOUNT_GITLAB = "Accounts_OAuth_Gitlab"
const val ACCOUNT_LOGIN_FORM = "Accounts_ShowFormLogin"
const val ACCOUNT_CUSTOM_FIELDS = "Accounts_CustomFields"
const val SITE_URL = "Site_Url" const val SITE_URL = "Site_Url"
const val SITE_NAME = "Site_Name" const val SITE_NAME = "Site_Name"
...@@ -28,7 +33,6 @@ const val FAVICON_512 = "Assets_favicon_512" ...@@ -28,7 +33,6 @@ const val FAVICON_512 = "Assets_favicon_512"
const val USE_REALNAME = "UI_Use_Real_Name" const val USE_REALNAME = "UI_Use_Real_Name"
const val ALLOW_ROOM_NAME_SPECIAL_CHARS = "UI_Allow_room_names_with_special_chars" const val ALLOW_ROOM_NAME_SPECIAL_CHARS = "UI_Allow_room_names_with_special_chars"
const val FAVORITE_ROOMS = "Favorite_Rooms" const val FAVORITE_ROOMS = "Favorite_Rooms"
const val LDAP_ENABLE = "LDAP_Enable"
const val UPLOAD_STORAGE_TYPE = "FileUpload_Storage_Type" const val UPLOAD_STORAGE_TYPE = "FileUpload_Storage_Type"
const val UPLOAD_MAX_FILE_SIZE = "FileUpload_MaxFileSize" const val UPLOAD_MAX_FILE_SIZE = "FileUpload_MaxFileSize"
const val UPLOAD_WHITELIST_MIMETYPES = "FileUpload_MediaTypeWhiteList" const val UPLOAD_WHITELIST_MIMETYPES = "FileUpload_MediaTypeWhiteList"
...@@ -42,20 +46,27 @@ const val ALLOW_MESSAGE_EDITING = "Message_AllowEditing" ...@@ -42,20 +46,27 @@ const val ALLOW_MESSAGE_EDITING = "Message_AllowEditing"
const val SHOW_DELETED_STATUS = "Message_ShowDeletedStatus" const val SHOW_DELETED_STATUS = "Message_ShowDeletedStatus"
const val SHOW_EDITED_STATUS = "Message_ShowEditedStatus" const val SHOW_EDITED_STATUS = "Message_ShowEditedStatus"
const val ALLOW_MESSAGE_PINNING = "Message_AllowPinning" const val ALLOW_MESSAGE_PINNING = "Message_AllowPinning"
/* /*
* Extension functions for Public Settings. * Extension functions for Public Settings.
* *
* If you need to access a Setting, add a const val key above, add it to the filter on * If you need to access a Setting, add a const val key above, add it to the filter on
* ServerPresenter.kt and a extension function to access it * ServerPresenter.kt and a extension function to access it
*/ */
fun PublicSettings.googleEnabled(): Boolean = this[ACCOUNT_GOOGLE]?.value == true fun PublicSettings.isLdapAuthenticationEnabled(): Boolean = this[LDAP_ENABLE]?.value == true
fun PublicSettings.facebookEnabled(): Boolean = this[ACCOUNT_FACEBOOK]?.value == true fun PublicSettings.isCasAuthenticationEnabled(): Boolean = this[CAS_ENABLE]?.value == true
fun PublicSettings.githubEnabled(): Boolean = this[ACCOUNT_GITHUB]?.value == true fun PublicSettings.casLoginUrl(): String = this[CAS_LOGIN_URL]?.value.toString()
fun PublicSettings.linkedinEnabled(): Boolean = this[ACCOUNT_LINKEDIN]?.value == true fun PublicSettings.isRegistrationEnabledForNewUsers(): Boolean = this[ACCOUNT_REGISTRATION]?.value == "Public"
fun PublicSettings.meteorEnabled(): Boolean = this[ACCOUNT_METEOR]?.value == true fun PublicSettings.isLoginFormEnabled(): Boolean = this[ACCOUNT_LOGIN_FORM]?.value == true
fun PublicSettings.twitterEnabled(): Boolean = this[ACCOUNT_TWITTER]?.value == true fun PublicSettings.isPasswordResetEnabled(): Boolean = this[ACCOUNT_PASSWORD_RESET]?.value == true
fun PublicSettings.gitlabEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true fun PublicSettings.isGoogleAuthenticationEnabled(): Boolean = this[ACCOUNT_GOOGLE]?.value == true
fun PublicSettings.wordpressEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true fun PublicSettings.isFacebookAuthenticationEnabled(): Boolean = this[ACCOUNT_FACEBOOK]?.value == true
fun PublicSettings.isGithubAuthenticationEnabled(): Boolean = this[ACCOUNT_GITHUB]?.value == true
fun PublicSettings.isLinkedinAuthenticationEnabled(): Boolean = this[ACCOUNT_LINKEDIN]?.value == true
fun PublicSettings.isMeteorAuthenticationEnabled(): Boolean = this[ACCOUNT_METEOR]?.value == true
fun PublicSettings.isTwitterAuthenticationEnabled(): Boolean = this[ACCOUNT_TWITTER]?.value == true
fun PublicSettings.isGitlabAuthenticationEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true
fun PublicSettings.isWordpressAuthenticationEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true
...@@ -66,11 +77,6 @@ fun PublicSettings.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING ...@@ -66,11 +77,6 @@ fun PublicSettings.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING
fun PublicSettings.allowedMessageEditing(): Boolean = this[ALLOW_MESSAGE_EDITING]?.value == true fun PublicSettings.allowedMessageEditing(): Boolean = this[ALLOW_MESSAGE_EDITING]?.value == true
fun PublicSettings.allowedMessageDeleting(): Boolean = this[ALLOW_MESSAGE_DELETING]?.value == true fun PublicSettings.allowedMessageDeleting(): Boolean = this[ALLOW_MESSAGE_DELETING]?.value == true
fun PublicSettings.registrationEnabled(): Boolean {
val value = this[ACCOUNT_REGISTRATION]
return value?.value == "Public"
}
fun PublicSettings.uploadMimeTypeFilter(): Array<String> { fun PublicSettings.uploadMimeTypeFilter(): Array<String> {
val values = this[UPLOAD_WHITELIST_MIMETYPES]?.value val values = this[UPLOAD_WHITELIST_MIMETYPES]?.value
values?.let { it as String }?.split(",")?.let { values?.let { it as String }?.split(",")?.let {
......
...@@ -14,12 +14,10 @@ class SharedPreferencesSettingsRepository(private val localRepository: LocalRepo ...@@ -14,12 +14,10 @@ class SharedPreferencesSettingsRepository(private val localRepository: LocalRepo
localRepository.save("$SETTINGS_KEY$url", adapter.toJson(settings)) localRepository.save("$SETTINGS_KEY$url", adapter.toJson(settings))
} }
override fun get(url: String): PublicSettings? { override fun get(url: String): PublicSettings {
val settings = localRepository.get("$SETTINGS_KEY$url") val settings = localRepository.get("$SETTINGS_KEY$url")!!
settings?.let { settings.let {
return adapter.fromJson(it) return adapter.fromJson(it)!!
} }
return null
} }
} }
\ No newline at end of file
package chat.rocket.android.main.di package chat.rocket.android.settings.di
import android.arch.lifecycle.LifecycleOwner import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.main.presentation.MainView import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.settings.presentation.SettingsView
import chat.rocket.android.settings.ui.SettingsFragment
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.Job
@Module @Module
class MainActivityModule { @PerFragment
class SettingsFragmentModule {
// @Provides
// fun provideMainView(activity: MainActivity): MainView = activity
@Provides @Provides
fun provideLifecycleOwner(activity: MainActivity): LifecycleOwner = activity fun settingsView(frag: SettingsFragment): SettingsView {
return frag
}
@Provides @Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy = CancelStrategy(owner, jobs) fun settingsLifecycleOwner(frag: SettingsFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
package chat.rocket.android.settings.di
import chat.rocket.android.settings.ui.SettingsFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class SettingsFragmentProvider {
@ContributesAndroidInjector(modules = [SettingsFragmentModule::class])
abstract fun provideSettingsFragment(): SettingsFragment
}
\ No newline at end of file
package chat.rocket.android.settings.password.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.settings.password.presentation.PasswordView
import chat.rocket.android.settings.password.ui.PasswordFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class PasswordFragmentModule {
@Provides
fun passwordView(frag: PasswordFragment): PasswordView {
return frag
}
@Provides
fun settingsLifecycleOwner(frag: PasswordFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
package chat.rocket.android.settings.password.di
import chat.rocket.android.settings.password.ui.PasswordFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class PasswordFragmentProvider {
@ContributesAndroidInjector(modules = [PasswordFragmentModule::class])
abstract fun providePasswordFragment(): PasswordFragment
}
\ No newline at end of file
package chat.rocket.android.settings.password.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.updateProfile
import javax.inject.Inject
class PasswordPresenter @Inject constructor (private val view: PasswordView,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory){
private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl)
fun updatePassword(password: String) {
launchUI(strategy) {
try {
view.showLoading()
client.updateProfile(client.me().id, null, null, password, null)
view.showPasswordSuccessfullyUpdatedMessage()
view.hideLoading()
} catch (exception: RocketChatException) {
view.showPasswordFailsUpdateMessage(exception.message)
view.hideLoading()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.settings.password.presentation
import chat.rocket.android.core.behaviours.LoadingView
interface PasswordView: LoadingView {
/**
* Shows a message when a user's password is successfully updated
*/
fun showPasswordSuccessfullyUpdatedMessage()
/**
* Shows a message when the user's password fails to update
* @param error is a String containing the failure message
*/
fun showPasswordFailsUpdateMessage(error : String?)
}
package chat.rocket.android.settings.password.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_password.*
import javax.inject.Inject
class PasswordActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_password)
setupToolbar()
addFragment("PasswordFragment")
}
override fun onBackPressed() {
super.onBackPressed()
finish()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return super.onNavigateUp()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector
private fun addFragment(tag: String) {
addFragment(tag, R.id.fragment_container) {
PasswordFragment.newInstance()
}
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
text_change_password.textContent = resources.getString(R.string.title_password)
}
}
package chat.rocket.android.settings.password.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.*
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.settings.password.presentation.PasswordPresenter
import chat.rocket.android.settings.password.presentation.PasswordView
import chat.rocket.android.util.extensions.asObservable
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.textContent
import android.support.v7.view.ActionMode
import dagger.android.support.AndroidSupportInjection
import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.fragment_password.*
import javax.inject.Inject
class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.ActionMode.Callback {
@Inject lateinit var presenter: PasswordPresenter
private var actionMode: ActionMode? = null
companion object {
fun newInstance() = PasswordFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
AndroidSupportInjection.inject(this)
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_password)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listenToChanges()
}
override fun hideLoading() {
layout_new_password.visibility = View.VISIBLE
layout_confirm_password.visibility = View.VISIBLE
view_loading.visibility = View.GONE
}
override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_password -> {
presenter.updatePassword(text_new_password.textContent)
mode.finish()
return true
}
else -> {
false
}
}
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu?): Boolean {
mode.menuInflater.inflate(R.menu.password, menu)
mode.title = resources.getString(R.string.action_confirm_password)
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean = false
override fun onDestroyActionMode(mode: ActionMode?) {
actionMode = null
}
override fun showLoading() {
layout_new_password.visibility = View.GONE
layout_confirm_password.visibility = View.GONE
view_loading.visibility = View.VISIBLE
}
override fun showPasswordFailsUpdateMessage(error: String?) {
showToast("Password fails to update: " + error)
}
override fun showPasswordSuccessfullyUpdatedMessage() {
showToast("Password was successfully updated!")
}
private fun finishActionMode() = actionMode?.finish()
private fun listenToChanges() {
Observables.combineLatest(text_new_password.asObservable(), text_confirm_password.asObservable()).subscribe {
val textPassword = text_new_password.textContent
val textConfirmPassword = text_confirm_password.textContent
if (textPassword.length > 5 && textConfirmPassword.length > 5 && textPassword.equals(textConfirmPassword))
startActionMode()
else
finishActionMode()
}
}
private fun showToast(msg: String?) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show()
}
private fun startActionMode() {
if (actionMode == null) {
actionMode = (activity as PasswordActivity).startSupportActionMode(this)
}
}
}
\ No newline at end of file
package chat.rocket.android.settings.presentation
interface SettingsView
package chat.rocket.android.settings.ui
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.settings.password.ui.PasswordActivity
import chat.rocket.android.settings.presentation.SettingsView
import chat.rocket.android.util.extensions.inflate
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.fragment_settings.*
import kotlin.reflect.KClass
class SettingsFragment: Fragment(), SettingsView, AdapterView.OnItemClickListener {
companion object {
fun newInstance() = SettingsFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_settings)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
setupListView()
}
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when (parent?.getItemAtPosition(position).toString()) {
"Change Password" -> {
startNewActivity(PasswordActivity::class)
}
}
}
private fun setupListView() {
settings_list.onItemClickListener = this
}
private fun setupToolbar() {
(activity as MainActivity).toolbar.title = getString(R.string.title_settings)
}
private fun startNewActivity(classType: KClass<out AppCompatActivity>) {
startActivity(Intent(activity, classType.java))
activity?.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
}
package chat.rocket.android.util.extensions package chat.rocket.android.util.extensions
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.support.v4.app.Fragment
import android.view.View import android.view.View
import android.view.ViewAnimationUtils import android.view.ViewAnimationUtils
import android.view.animation.AccelerateInterpolator import android.view.animation.AccelerateInterpolator
...@@ -64,3 +73,36 @@ fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float ...@@ -64,3 +73,36 @@ fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float
anim.start() anim.start()
} }
fun View.shake(x: Float = 2F, num: Int = 0){
if (num == 6) {
this.translationX = 0.toFloat()
return
}
val animatorSet = AnimatorSet()
animatorSet.playTogether(ObjectAnimator.ofFloat(this, "translationX", this.context.dp(x)))
animatorSet.duration = 50
animatorSet.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
shake(if (num == 5) 0.toFloat() else -x, num + 1)
}
})
animatorSet.start()
}
fun Context.dp(value: Float): Float {
val density = this.resources.displayMetrics.density
val result = Math.ceil(density.times(value.toDouble()))
return result.toFloat()
}
fun Fragment.vibrateSmartPhone() {
val vibrator = context?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
if (Build.VERSION.SDK_INT >= 26) {
vibrator.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
vibrator.vibrate(200)
}
}
\ No newline at end of file
...@@ -9,6 +9,7 @@ import android.widget.TextView ...@@ -9,6 +9,7 @@ import android.widget.TextView
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import java.security.SecureRandom
fun String.ifEmpty(value: String): String { fun String.ifEmpty(value: String): String {
if (isEmpty()) { if (isEmpty()) {
...@@ -34,6 +35,17 @@ fun EditText.erase() { ...@@ -34,6 +35,17 @@ fun EditText.erase() {
fun String.isEmailValid(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches() fun String.isEmailValid(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun generateRandomString(stringLength: Int): String {
val base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
val secureRandom = SecureRandom()
val stringBuilder = StringBuilder(stringLength)
for (i in 0 until stringLength) {
stringBuilder.append(base[secureRandom.nextInt(base.length)])
}
return stringBuilder.toString()
}
var TextView.textContent: String var TextView.textContent: String
get() = text.toString() get() = text.toString()
set(value) { set(value) {
......
package chat.rocket.android.webview.cas.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.webkit.WebView
import android.webkit.WebViewClient
import chat.rocket.android.R
import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.*
fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent {
return Intent(this, CasWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_CAS_TOKEN, casToken)
}
}
private const val INTENT_WEB_PAGE_URL = "web_page_url"
private const val INTENT_CAS_TOKEN = "cas_token"
class CasWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
private lateinit var casToken: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
casToken = intent.getStringExtra(INTENT_CAS_TOKEN)
requireNotNull(casToken) { "no cas_token provided in Intent extras" }
setupToolbar()
}
override fun onResume() {
super.onResume()
setupWebView()
}
override fun onBackPressed() {
if (web_view.canGoBack()) {
web_view.goBack()
} else {
finishActivity(false)
}
}
private fun setupToolbar() {
toolbar.title = getString(R.string.title_authentication)
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
toolbar.setNavigationOnClickListener { finishActivity(false) }
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
web_view.settings.javaScriptEnabled = true
web_view.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
// The user can be already logged in the CAS, so check if the URL contains the "ticket" word
// (that means he/she is successful authenticated and we don't need to wait until the page is finished.
if (url.contains("ticket")) {
finishActivity(true)
}
}
override fun onPageFinished(view: WebView, url: String) {
if (url.contains("ticket")) {
finishActivity(true)
} else {
view_loading.hide()
}
}
}
web_view.loadUrl(webPageUrl)
}
private fun finishActivity(setResultOk: Boolean) {
if (setResultOk) {
setResult(Activity.RESULT_OK, Intent().putExtra(INTENT_CAS_TOKEN, casToken))
finish()
} else {
super.onBackPressed()
}
overridePendingTransition(R.anim.hold, R.anim.slide_down)
}
}
\ No newline at end of file
package chat.rocket.android.webview package chat.rocket.android.webview.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
...@@ -19,6 +19,7 @@ fun Context.webViewIntent(webPageUrl: String): Intent { ...@@ -19,6 +19,7 @@ fun Context.webViewIntent(webPageUrl: String): Intent {
private const val INTENT_WEB_PAGE_URL = "web_page_url" private const val INTENT_WEB_PAGE_URL = "web_page_url"
// Simple WebView to load URL.
class WebViewActivity : AppCompatActivity() { class WebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String private lateinit var webPageUrl: String
......
...@@ -9,10 +9,10 @@ import android.view.View ...@@ -9,10 +9,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.widget.emoji.EmojiKeyboardPopup.Listener
import java.util.* import java.util.*
class CategoryPagerAdapter(val listener: Listener) : PagerAdapter() { class CategoryPagerAdapter(val listener: EmojiKeyboardListener) : PagerAdapter() {
override fun isViewFromObject(view: View, obj: Any): Boolean { override fun isViewFromObject(view: View, obj: Any): Boolean {
return view == obj return view == obj
} }
...@@ -46,7 +46,7 @@ class CategoryPagerAdapter(val listener: Listener) : PagerAdapter() { ...@@ -46,7 +46,7 @@ class CategoryPagerAdapter(val listener: Listener) : PagerAdapter() {
override fun getPageTitle(position: Int) = EmojiCategory.values()[position].textIcon() override fun getPageTitle(position: Int) = EmojiCategory.values()[position].textIcon()
class EmojiAdapter(val spanCount: Int, val listener: Listener) : RecyclerView.Adapter<EmojiRowViewHolder>() { class EmojiAdapter(val spanCount: Int, val listener: EmojiKeyboardListener) : RecyclerView.Adapter<EmojiRowViewHolder>() {
private var emojis = Collections.emptyList<Emoji>() private var emojis = Collections.emptyList<Emoji>()
fun addEmojis(emojis: List<Emoji>) { fun addEmojis(emojis: List<Emoji>) {
...@@ -66,7 +66,7 @@ class CategoryPagerAdapter(val listener: Listener) : PagerAdapter() { ...@@ -66,7 +66,7 @@ class CategoryPagerAdapter(val listener: Listener) : PagerAdapter() {
override fun getItemCount(): Int = emojis.size override fun getItemCount(): Int = emojis.size
} }
class EmojiRowViewHolder(itemView: View, val itemCount: Int, val spanCount: Int, val listener: Listener) : RecyclerView.ViewHolder(itemView) { class EmojiRowViewHolder(itemView: View, val itemCount: Int, val spanCount: Int, val listener: EmojiKeyboardListener) : RecyclerView.ViewHolder(itemView) {
private val emojiView: TextView = itemView.findViewById(R.id.emoji) private val emojiView: TextView = itemView.findViewById(R.id.emoji)
fun bind(emoji: Emoji) { fun bind(emoji: Emoji) {
......
package chat.rocket.android.widget.emoji
interface EmojiKeyboardListener {
/**
* When an emoji is selected on the picker.
*
* @param emoji The selected emoji
*/
fun onEmojiAdded(emoji: Emoji)
/**
* When backspace key is clicked.
*
* @param keyCode The key code pressed as defined
*
* @see android.view.KeyEvent
*/
fun onNonEmojiKeyPressed(keyCode: Int)
}
\ No newline at end of file
...@@ -21,14 +21,14 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -21,14 +21,14 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
private lateinit var searchView: View private lateinit var searchView: View
private lateinit var backspaceView: View private lateinit var backspaceView: View
private lateinit var parentContainer: ViewGroup private lateinit var parentContainer: ViewGroup
var listener: Listener? = null var listener: EmojiKeyboardListener? = null
companion object { companion object {
const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS" const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
} }
override fun onCreateView(inflater: LayoutInflater): View { override fun onCreateView(inflater: LayoutInflater): View {
val view = inflater.inflate(R.layout.emoji_popup_layout, null, false) val view = inflater.inflate(R.layout.emoji_keyboard, null)
parentContainer = view.findViewById(R.id.emoji_keyboard_container) parentContainer = view.findViewById(R.id.emoji_keyboard_container)
viewPager = view.findViewById(R.id.pager_categories) viewPager = view.findViewById(R.id.pager_categories)
searchView = view.findViewById(R.id.emoji_search) searchView = view.findViewById(R.id.emoji_search)
...@@ -55,20 +55,17 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -55,20 +55,17 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
private fun setupViewPager() { private fun setupViewPager() {
context.let { context.let {
val callback = when (it) { val callback = when (it) {
is Listener -> it is EmojiKeyboardListener -> it
else -> { else -> {
val fragments = (it as AppCompatActivity).supportFragmentManager.fragments val fragments = (it as AppCompatActivity).supportFragmentManager.fragments
if (fragments == null || fragments.size == 0 || !(fragments[0] is Listener)) { if (fragments == null || fragments.size == 0 || !(fragments[0] is EmojiKeyboardListener)) {
throw IllegalStateException("activity/fragment should implement Listener interface") throw IllegalStateException("activity/fragment should implement Listener interface")
} }
fragments[0] as Listener fragments[0] as EmojiKeyboardListener
} }
} }
viewPager.adapter = CategoryPagerAdapter(object : Listener {
override fun onNonEmojiKeyPressed(keyCode: Int) {
// do nothing
}
viewPager.adapter = CategoryPagerAdapter(object : EmojiListenerAdapter() {
override fun onEmojiAdded(emoji: Emoji) { override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji) EmojiRepository.addToRecents(emoji)
callback.onEmojiAdded(emoji) callback.onEmojiAdded(emoji)
...@@ -78,14 +75,14 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -78,14 +75,14 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
for (category in EmojiCategory.values()) { for (category in EmojiCategory.values()) {
val tab = tabLayout.getTabAt(category.ordinal) val tab = tabLayout.getTabAt(category.ordinal)
val tabView = LayoutInflater.from(context).inflate(R.layout.emoji_picker_tab, null) val tabView = LayoutInflater.from(context).inflate(R.layout.emoji_picker_tab, null)
tab?.setCustomView(tabView) tab?.customView = tabView
val textView = tabView.findViewById(R.id.image_category) as ImageView val textView = tabView.findViewById(R.id.image_category) as ImageView
textView.setImageResource(category.resourceIcon()) textView.setImageResource(category.resourceIcon())
} }
val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else
EmojiCategory.RECENTS.ordinal EmojiCategory.RECENTS.ordinal
viewPager.setCurrentItem(currentTab) viewPager.currentItem = currentTab
} }
} }
...@@ -132,22 +129,4 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow ...@@ -132,22 +129,4 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
} }
} }
interface Listener {
/**
* When an emoji is selected on the picker.
*
* @param emoji The selected emoji
*/
fun onEmojiAdded(emoji: Emoji)
/**
* When backspace key is clicked.
*
* @param keyCode The key code pressed as defined
*
* @see android.view.KeyEvent
*/
fun onNonEmojiKeyPressed(keyCode: Int)
}
} }
\ No newline at end of file
package chat.rocket.android.widget.emoji
abstract class EmojiListenerAdapter : EmojiKeyboardListener {
override fun onEmojiAdded(emoji: Emoji) {
// this space is for rent
}
override fun onNonEmojiKeyPressed(keyCode: Int) {
// this space is for rent
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.support.design.widget.TabLayout
import android.support.v4.view.ViewPager
import android.view.LayoutInflater
import android.view.Window
import android.view.WindowManager
import android.widget.ImageView
import chat.rocket.android.R
class EmojiPickerPopup(context: Context) : Dialog(context) {
private lateinit var viewPager: ViewPager
private lateinit var tabLayout: TabLayout
var listener: EmojiKeyboardListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.emoji_picker)
viewPager = findViewById(R.id.pager_categories)
tabLayout = findViewById(R.id.tabs)
tabLayout.setupWithViewPager(viewPager)
setupViewPager()
setSize()
}
private fun setSize() {
val lp = WindowManager.LayoutParams()
lp.copyFrom(window.attributes)
val dialogWidth = lp.width
val dialogHeight = context.resources.getDimensionPixelSize(R.dimen.picker_popup_height)
window.setLayout(dialogWidth, dialogHeight)
}
private fun setupViewPager() {
viewPager.adapter = CategoryPagerAdapter(object : EmojiListenerAdapter() {
override fun onEmojiAdded(emoji: Emoji) {
EmojiRepository.addToRecents(emoji)
dismiss()
listener?.onEmojiAdded(emoji)
}
})
for (category in EmojiCategory.values()) {
val tab = tabLayout.getTabAt(category.ordinal)
val tabView = LayoutInflater.from(context).inflate(R.layout.emoji_picker_tab, null)
tab?.customView = tabView
val textView = tabView.findViewById(R.id.image_category) as ImageView
textView.setImageResource(category.resourceIcon())
}
val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else
EmojiCategory.RECENTS.ordinal
viewPager.currentItem = currentTab
}
}
\ No newline at end of file
package chat.rocket.android.widget.emoji
interface EmojiReactionListener {
/**
* Callback when an emoji is picked in respect to message by the given id.
*
* @param messageId The id of the message being reacted.
* @param emoji The emoji used to react.
*/
fun onReactionAdded(messageId: String, emoji: Emoji)
/**
* Callback when an added reaction is touched.
*
* @param messageId The id of the message with the reaction.
* @param emojiShortname The shortname of the emoji (:grin:, :smiley:, etc).
*/
fun onReactionTouched(messageId: String, emojiShortname: String)
}
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="20.0"
android:viewportWidth="20.0">
<path
android:fillColor="#868585"
android:fillType="evenOdd"
android:pathData="M12,8m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
<path
android:fillColor="#868585"
android:fillType="evenOdd"
android:pathData="M8,8m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
<path
android:fillType="evenOdd"
android:pathData="M10,3a7,7 0,1 0,0 14,7 7,0 0,0 7,-7M7.172,12.328a4,4 0,0 0,5.656 0"
android:strokeColor="#868585"
android:strokeWidth="1.5" />
<path
android:fillType="evenOdd"
android:pathData="M16.2,1.2v5.2m-2.6,-2.6h5.2"
android:strokeColor="#868585"
android:strokeLineCap="square"
android:strokeWidth="1.5" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
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" />
</vector>
<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>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
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" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size
android:width="24dp"
android:height="24dp" />
<solid android:color="#efeeee" />
<corners android:radius="4dp"/>
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:theme="@style/AppTheme"
tools:context=".settings.password.ui.PasswordActivity">
<include
android:id="@+id/layout_app_bar"
layout="@layout/app_bar_password" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
\ No newline at end of file
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".webview.WebViewActivity"> tools:context=".webview.ui.WebViewActivity">
<include <include
android:id="@+id/layout_app_bar" android:id="@+id/layout_app_bar"
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
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"
android:drawablePadding="@dimen/text_view_drawable_padding"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="@color/white" android:textColor="@color/white"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.AppBarLayout 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="@color/colorPrimary"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationIcon="?android:attr/homeAsUpIndicator"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ActionModeStyle">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text_change_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="@string/title_password" />
</RelativeLayout>
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
\ No newline at end of file
...@@ -11,35 +11,18 @@ ...@@ -11,35 +11,18 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:background="@color/colorDividerMessageComposer" android:background="@color/colorDividerMessageComposer"
app:layout_constraintBottom_toTopOf="@+id/tabs" app:layout_constraintBottom_toTopOf="@+id/picker_container"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<android.support.design.widget.TabLayout <include
android:id="@+id/tabs" android:id="@+id/picker_container"
android:layout_width="0dp" layout="@layout/emoji_picker"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/pager_categories"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tabBackground="@color/whitesmoke"
app:tabGravity="fill"
app:tabMaxWidth="48dp"
app:tabMode="scrollable" />
<android.support.v4.view.ViewPager
android:id="@+id/pager_categories"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:background="@color/white"
app:layout_constraintBottom_toTopOf="@+id/emoji_actions_container" app:layout_constraintBottom_toTopOf="@+id/emoji_actions_container"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabs" /> app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout <RelativeLayout
android:id="@+id/emoji_actions_container" android:id="@+id/emoji_actions_container"
...@@ -60,8 +43,8 @@ ...@@ -60,8 +43,8 @@
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:padding="8dp" android:padding="8dp"
android:visibility="invisible" android:src="@drawable/ic_search_gray_24px"
android:src="@drawable/ic_search_gray_24px" /> android:visibility="invisible" />
<ImageView <ImageView
android:id="@+id/emoji_backspace" android:id="@+id/emoji_backspace"
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/picker_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabBackground="@color/whitesmoke"
app:tabGravity="fill"
app:tabMaxWidth="48dp"
app:tabMode="scrollable" />
<android.support.v4.view.ViewPager
android:id="@+id/pager_categories"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:background="@color/white" />
</LinearLayout>
\ No newline at end of file
...@@ -45,6 +45,19 @@ ...@@ -45,6 +45,19 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_username_or_email" /> app:layout_constraintTop_toBottomOf="@+id/text_username_or_email" />
<Button
android:id="@+id/button_cas"
style="@style/Authentication.Button"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="16dp"
android:text="@string/action_login_or_sign_up"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/text_new_to_rocket_chat" android:id="@+id/text_new_to_rocket_chat"
android:layout_width="wrap_content" android:layout_width="wrap_content"
...@@ -57,7 +70,7 @@ ...@@ -57,7 +70,7 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password" app:layout_constraintTop_toBottomOf="@+id/button_cas"
tools:visibility="visible" /> tools:visibility="visible" />
<com.wang.avi.AVLoadingIndicatorView <com.wang.avi.AVLoadingIndicatorView
...@@ -69,7 +82,7 @@ ...@@ -69,7 +82,7 @@
app:indicatorName="BallPulseIndicator" app:indicatorName="BallPulseIndicator"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password" app:layout_constraintTop_toBottomOf="@+id/button_cas"
tools:visibility="visible" /> tools:visibility="visible" />
<LinearLayout <LinearLayout
...@@ -195,9 +208,8 @@ ...@@ -195,9 +208,8 @@
android:id="@+id/button_log_in" android:id="@+id/button_log_in"
style="@style/Authentication.Button" style="@style/Authentication.Button"
android:text="@string/title_log_in" android:text="@string/title_log_in"
android:visibility="visible" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent" />
tools:visibility="gone" />
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
</ScrollView> </ScrollView>
\ No newline at end of file
...@@ -4,7 +4,8 @@ ...@@ -4,7 +4,8 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout" android:id="@+id/root_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
tools:context=".chatroom.ui.ChatRoomFragment">
<com.wang.avi.AVLoadingIndicatorView <com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading" android:id="@+id/view_loading"
......
...@@ -4,9 +4,8 @@ ...@@ -4,9 +4,8 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/member_bottom_sheet" android:id="@+id/member_bottom_sheet"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="340dp" android:layout_height="match_parent"
app:behavior_hideable="true" android:paddingBottom="16dp"
app:behavior_peekHeight="0dp"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"> app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<com.facebook.drawee.view.SimpleDraweeView <com.facebook.drawee.view.SimpleDraweeView
...@@ -14,15 +13,24 @@ ...@@ -14,15 +13,24 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dp" /> android:layout_height="200dp" />
<LinearLayout
android:id="@+id/name_and_username_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorBackgroundMemberContainer"
android:orientation="vertical"
android:paddingBottom="10dp"
android:paddingStart="16dp"
android:paddingTop="10dp"
app:layout_constraintBottom_toBottomOf="@+id/image_bottom_sheet_avatar"
app:layout_constraintLeft_toLeftOf="parent">
<TextView <TextView
android:id="@+id/text_bottom_sheet_member_name" android:id="@+id/text_bottom_sheet_member_name"
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:layout_marginStart="16dp"
android:textColor="@color/white" android:textColor="@color/white"
app:layout_constraintBottom_toTopOf="@+id/text_bottom_sheet_member_username"
app:layout_constraintLeft_toLeftOf="parent"
tools:text="Ronald Perkins" /> tools:text="Ronald Perkins" />
<TextView <TextView
...@@ -30,12 +38,10 @@ ...@@ -30,12 +38,10 @@
style="@style/Sender.Name.TextView" style="@style/Sender.Name.TextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="10dp" android:layout_marginTop="5dp"
android:layout_marginStart="16dp"
android:textColor="@color/white" android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="@+id/image_bottom_sheet_avatar"
app:layout_constraintLeft_toLeftOf="parent"
tools:text="\@ronaldPerkins" /> tools:text="\@ronaldPerkins" />
</LinearLayout>
<TextView <TextView
android:id="@+id/text_email_address" android:id="@+id/text_email_address"
......
...@@ -9,16 +9,15 @@ ...@@ -9,16 +9,15 @@
<android.support.v7.widget.RecyclerView <android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
android:scrollbars="vertical" />
<include layout="@layout/member_bottom_sheet" />
<com.wang.avi.AVLoadingIndicatorView <com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading" android:id="@+id/view_loading"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:layout_gravity="center"
app:indicatorColor="@color/black" app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator" /> app:indicatorName="BallPulseIndicator" />
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.design.widget.TextInputLayout
android:id="@+id/layout_new_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:theme="@style/EditText.Password"
app:layout_constraintBottom_toTopOf="@id/middle_guide"
app:layout_constraintStart_toEndOf="@id/start_guide"
app:layout_constraintEnd_toStartOf="@id/end_guide">
<EditText
android:id="@+id/text_new_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/msg_new_password"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/layout_confirm_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:theme="@style/EditText.Password"
app:layout_constraintTop_toBottomOf="@id/middle_guide"
app:layout_constraintStart_toEndOf="@id/start_guide"
app:layout_constraintEnd_toStartOf="@id/end_guide">
<EditText
android:id="@+id/text_confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/msg_confirm_password"
android:inputType="textPassword" />
</android.support.design.widget.TextInputLayout>
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/middle_guide"
app:layout_constraintStart_toEndOf="@id/start_guide"
app:layout_constraintEnd_toStartOf="@id/end_guide"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator" />
<android.support.constraint.Guideline
android:id="@+id/middle_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.2" />
<android.support.constraint.Guideline
android:id="@+id/start_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.05" />
<android.support.constraint.Guideline
android:id="@+id/end_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.95" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -46,6 +46,14 @@ ...@@ -46,6 +46,14 @@
android:drawableStart="@drawable/ic_email_black_24dp" android:drawableStart="@drawable/ic_email_black_24dp"
android:hint="@string/msg_email" android:hint="@string/msg_email"
android:inputType="textEmailAddress" /> android:inputType="textEmailAddress" />
<EditText
android:id="@+id/text_avatar_url"
style="@style/Profile.EditText"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_link_black_24dp"
android:hint="@string/msg_avatar_url"
android:inputType="text" />
</LinearLayout> </LinearLayout>
<com.wang.avi.AVLoadingIndicatorView <com.wang.avi.AVLoadingIndicatorView
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/settings_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:entries="@array/settings_actions"/>
</RelativeLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="32dp"
android:layout_height="32dp"
android:paddingBottom="2dp"
android:paddingEnd="4dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingStart="4dp"
android:paddingTop="2dp"
android:src="@drawable/ic_add_reaction" />
\ No newline at end of file
...@@ -4,10 +4,11 @@ ...@@ -4,10 +4,11 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="12dp" android:background="?android:attr/selectableItemBackground"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins" android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins" android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:layout_marginTop="12dp"> android:paddingTop="@dimen/chat_item_top_and_bottom_padding"
android:paddingBottom="@dimen/chat_item_top_and_bottom_padding">
<include <include
android:id="@+id/layout_avatar" android:id="@+id/layout_avatar"
...@@ -31,6 +32,7 @@ ...@@ -31,6 +32,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:drawablePadding="@dimen/text_view_drawable_padding"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/text_last_message_date_time" app:layout_constraintRight_toLeftOf="@+id/text_last_message_date_time"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
......
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