Commit 841a165d authored by Leonardo Aramaki's avatar Leonardo Aramaki

Merge branch 'develop-2.x' into feature/autocomplete

parents c04ea638 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" />
......
...@@ -4,17 +4,20 @@ import chat.rocket.android.authentication.domain.model.TokenModel ...@@ -4,17 +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.model.Token
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.* import chat.rocket.core.internal.rest.*
import kotlinx.coroutines.experimental.delay
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
class LoginPresenter @Inject constructor(private val view: LoginView, class LoginPresenter @Inject constructor(private val view: LoginView,
...@@ -28,51 +31,69 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -28,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()
}
if (settings.isRegistrationEnabledForNewUsers()) {
view.showSignUpView()
view.setupSignUpView()
} }
view.showSignUpView(settings.registrationEnabled()) if (settings.isCasAuthenticationEnabled()) {
val token = generateRandomString(17)
view.setupCasButtonListener(UrlHelper.getCasUrl(settings.casLoginUrl(), server, token), token)
view.showCasButton()
}
var hasSocial = false var totalSocialAccountsEnabled = 0
if (settings.facebookEnabled()) { 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) {
...@@ -90,34 +111,23 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -90,34 +111,23 @@ 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 {
var token: Token? = null val token = if (usernameOrEmail.isEmailValid()) {
if (usernameOrEmail.isEmailValid()) { client.loginWithEmail(usernameOrEmail, password)
token = client.loginWithEmail(usernameOrEmail, password)
} else { } else {
val settings = settingsInteractor.get(server) val settings = settingsInteractor.get(server)
if (settings != null) { if (settings.isLdapAuthenticationEnabled()) {
token = if (settings.ldapEnabled()) { client.loginWithLdap(usernameOrEmail, password)
client.loginWithLdap(usernameOrEmail, password)
} else {
client.login(usernameOrEmail, password)
}
} else { } else {
navigator.toServerScreen() client.login(usernameOrEmail, password)
} }
} }
if (token != null) { saveToken(server, TokenModel(token.userId, token.authToken), client.me().username)
val me = client.me() registerPushToken()
multiServerRepository.save(server, TokenModel(token.userId, token.authToken)) navigator.toChatList()
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
registerPushToken()
navigator.toChatList()
} else {
view.showGenericErrorMessage()
}
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
when (exception) { when (exception) {
is RocketChatTwoFactorException -> { is RocketChatTwoFactorException -> {
...@@ -133,6 +143,7 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -133,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()
...@@ -142,8 +153,46 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -142,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.
......
package chat.rocket.android.authentication.login.ui package chat.rocket.android.authentication.login.ui
import DrawableHelper import DrawableHelper
import android.content.Context import android.app.Activity
import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
...@@ -12,30 +13,24 @@ import android.view.ViewGroup ...@@ -12,30 +13,24 @@ import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ScrollView import android.widget.ScrollView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.login.presentation.LoginPresenter import chat.rocket.android.authentication.login.presentation.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.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.inflate import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.webview.cas.ui.webViewIntent
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.* import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject import javax.inject.Inject
internal const val REQUEST_CODE_FOR_CAS = 1
class LoginFragment : Fragment(), LoginView { class LoginFragment : Fragment(), LoginView {
@Inject lateinit var presenter: LoginPresenter @Inject lateinit var presenter: LoginPresenter
@Inject lateinit var appContext: Context // TODO we really need it? Check alternatives...
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
areLoginOptionsNeeded() areLoginOptionsNeeded()
} }
private var isGlobalLayoutListenerSetUp = false private var isGlobalLayoutListenerSetUp = false
companion object { companion object {
...@@ -47,7 +42,8 @@ class LoginFragment : Fragment(), LoginView { ...@@ -47,7 +42,8 @@ class LoginFragment : Fragment(), LoginView {
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_log_in) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
container?.inflate(R.layout.fragment_authentication_log_in)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
...@@ -56,54 +52,143 @@ class LoginFragment : Fragment(), LoginView { ...@@ -56,54 +52,143 @@ class LoginFragment : Fragment(), LoginView {
tintEditTextDrawableStart() tintEditTextDrawableStart()
} }
presenter.setup() presenter.setupView()
showThreeSocialMethods() }
override fun onDestroyView() {
super.onDestroyView()
if (isGlobalLayoutListenerSetUp) {
scroll_view.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
isGlobalLayoutListenerSetUp = false
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
if (resultCode == Activity.RESULT_OK) {
data?.apply {
presenter.authenticateWithCas(getStringExtra("cas_token"))
}
}
}
}
private fun tintEditTextDrawableStart() {
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_assignment_ind_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val drawables = arrayOf(personDrawable, lockDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_username_or_email, text_password), drawables)
}
}
override fun showLoading() {
view_loading.setVisible(true)
}
override fun hideLoading() {
view_loading.setVisible(false)
}
override fun showNoInternetConnection() {
showMessage(R.string.msg_no_internet_connection)
}
override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() {
showMessage(R.string.msg_generic_error)
}
override fun showFormView() {
text_username_or_email.setVisible(true)
text_password.setVisible(true)
}
override fun hideFormView() {
text_username_or_email.setVisible(false)
text_password.setVisible(false)
}
override fun setupLoginButtonListener() {
button_log_in.setOnClickListener { button_log_in.setOnClickListener {
presenter.authenticate(text_username_or_email.textContent, text_password.textContent) presenter.authenticate(text_username_or_email.textContent, text_password.textContent)
} }
}
setupFabListener() override fun enableUserInput() {
setupSignUpListener() button_log_in.isEnabled = true
text_username_or_email.isEnabled = true
text_password.isEnabled = true
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) { override fun disableUserInput() {
super.onViewStateRestored(savedInstanceState) button_log_in.isEnabled = false
text_username_or_email.isEnabled = false
text_password.isEnabled = false
}
areLoginOptionsNeeded() override fun showCasButton() {
button_cas.setVisible(true)
} }
override fun onDestroyView() { override fun hideCasButton() {
super.onDestroyView() button_cas.setVisible(false)
if (isGlobalLayoutListenerSetUp) { }
scroll_view.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
isGlobalLayoutListenerSetUp = false override fun setupCasButtonListener(casUrl: String, casToken: String) {
button_cas.setOnClickListener {
startActivityForResult(context?.webViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
} }
} }
override fun showOauthView(value: Boolean) { override fun showSignUpView() {
if (value) { text_new_to_rocket_chat.setVisible(true)
social_accounts_container.setVisible(true) }
button_fab.setVisible(true)
// We need to setup the layout to hide and show the oauth interface when the soft keyboard is shown override fun setupSignUpView() {
// (means that the user touched the text_username_or_email or text_password EditText to fill that respective fields). val signUp = getString(R.string.title_sign_up)
if (!isGlobalLayoutListenerSetUp) { val newToRocketChat = String.format(getString(R.string.msg_new_user), signUp)
scroll_view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
isGlobalLayoutListenerSetUp = true text_new_to_rocket_chat.text = newToRocketChat
}
} else { val signUpListener = object : ClickableSpan() {
social_accounts_container.setVisible(false) override fun onClick(view: View) = presenter.signup()
button_fab.setVisible(false)
} }
TextHelper.addLink(text_new_to_rocket_chat, arrayOf(signUp), arrayOf(signUpListener))
} }
override fun setupFabListener() { override fun hideSignUpView() {
button_fab.setOnClickListener({ text_new_to_rocket_chat.setVisible(false)
button_fab.hide() }
showRemainingSocialAccountsView()
scrollToBottom() override fun showOauthView() {
}) showThreeSocialAccountsMethods()
social_accounts_container.setVisible(true)
}
override fun hideOauthView() {
social_accounts_container.setVisible(false)
button_fab.setVisible(false)
}
override fun showLoginButton() {
button_log_in.setVisible(true)
}
override fun hideLoginButton() {
button_log_in.setVisible(false)
} }
override fun enableLoginByFacebook() { override fun enableLoginByFacebook() {
...@@ -134,99 +219,71 @@ class LoginFragment : Fragment(), LoginView { ...@@ -134,99 +219,71 @@ class LoginFragment : Fragment(), LoginView {
button_gitlab.isEnabled = true button_gitlab.isEnabled = true
} }
override fun showSignUpView(value: Boolean) = text_new_to_rocket_chat.setVisible(value) override fun setupFabListener() {
button_fab.setVisible(true)
button_fab.setOnClickListener({
button_fab.hide()
showRemainingSocialAccountsView()
scrollToBottom()
})
}
override fun setupGlobalListener() {
// We need to setup the layout to hide and show the oauth interface when the soft keyboard is shown
// (means that the user touched the text_username_or_email or text_password EditText to fill that respective fields).
if (!isGlobalLayoutListenerSetUp) {
scroll_view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
isGlobalLayoutListenerSetUp = true
}
}
override fun alertWrongUsernameOrEmail() { override fun alertWrongUsernameOrEmail() {
AnimationHelper.vibrateSmartPhone(appContext) vibrateSmartPhone()
AnimationHelper.shakeView(text_username_or_email) text_username_or_email.shake()
text_username_or_email.requestFocus() text_username_or_email.requestFocus()
} }
override fun alertWrongPassword() { override fun alertWrongPassword() {
AnimationHelper.vibrateSmartPhone(appContext) vibrateSmartPhone()
AnimationHelper.shakeView(text_password) text_password.shake()
text_password.requestFocus() text_password.requestFocus()
} }
override fun showLoading() { private fun showRemainingSocialAccountsView() {
enableUserInput(false) social_accounts_container.postDelayed({
view_loading.setVisible(true) (0..social_accounts_container.childCount)
.mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
.filter { it.isEnabled }
.forEach { it.visibility = View.VISIBLE }
}, 1000)
} }
override fun hideLoading() { // Scrolling to the bottom of the screen.
view_loading.setVisible(false) private fun scrollToBottom() {
enableUserInput(true) scroll_view.postDelayed({
scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}, 1250)
} }
override fun showMessage(resId: Int) = showToast(resId)
override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection))
private fun areLoginOptionsNeeded() { private fun areLoginOptionsNeeded() {
if (!isEditTextEmpty() || KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) { if (!isEditTextEmpty() || KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) {
showSignUpView(false) hideSignUpView()
showOauthView(false) hideOauthView()
showLoginButton(true) showLoginButton()
} else { } else {
showSignUpView(true) showSignUpView()
showOauthView(true) showOauthView()
showLoginButton(false) hideLoginButton()
} }
} }
private fun tintEditTextDrawableStart() {
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_assignment_ind_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val drawables = arrayOf(personDrawable, lockDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_username_or_email, text_password), drawables)
}
}
private fun showLoginButton(value: Boolean) {
button_log_in.setVisible(value)
}
private fun setupSignUpListener() {
val signUp = getString(R.string.title_sign_up)
val newToRocketChat = String.format(getString(R.string.msg_new_user), signUp)
text_new_to_rocket_chat.text = newToRocketChat
val signUpListener = object : ClickableSpan() {
override fun onClick(view: View) = presenter.signup()
}
TextHelper.addLink(text_new_to_rocket_chat, arrayOf(signUp), arrayOf(signUpListener))
}
private fun enableUserInput(value: Boolean) {
button_log_in.isEnabled = value
text_username_or_email.isEnabled = value
text_password.isEnabled = value
}
// Returns true if *all* EditTexts are empty. // Returns true if *all* EditTexts are empty.
private fun isEditTextEmpty(): Boolean = text_username_or_email.textContent.isBlank() && text_password.textContent.isEmpty() private fun isEditTextEmpty(): Boolean {
return text_username_or_email.textContent.isBlank() && text_password.textContent.isEmpty()
private fun showRemainingSocialAccountsView() {
social_accounts_container.postDelayed({
for (i in 0..social_accounts_container.childCount) {
val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue
if (view.isEnabled) view.visibility = View.VISIBLE
}
}, 1000)
} }
private fun showThreeSocialMethods() { private fun showThreeSocialAccountsMethods() {
var count = 0 var count = 0
for (i in 0..social_accounts_container.childCount) { for (i in 0..social_accounts_container.childCount) {
val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue
...@@ -236,10 +293,4 @@ class LoginFragment : Fragment(), LoginView { ...@@ -236,10 +293,4 @@ class LoginFragment : Fragment(), LoginView {
} }
} }
} }
}
private fun scrollToBottom() { \ No newline at end of file
scroll_view.postDelayed({
scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}, 1250)
}
}
...@@ -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,14 +37,11 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int ...@@ -37,14 +37,11 @@ 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()
} }
fun toServerScreen() { fun toServerScreen() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
} }
} }
\ No newline at end of file
...@@ -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_username) text_name.shake()
text_username.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,15 +109,17 @@ class SignupFragment : Fragment(), SignupView { ...@@ -114,15 +109,17 @@ 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)
DrawableHelper.wrapDrawables(drawables) val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.tintDrawables(drawables, appContext, R.color.colorDrawableTintGrey) DrawableHelper.wrapDrawables(drawables)
DrawableHelper.compoundDrawables(arrayOf(text_username, text_username, text_password, text_email), drawables) DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_password, text_email), drawables)
}
} }
private fun setUpNewUserAgreementListener() { private fun setUpNewUserAgreementListener() {
......
...@@ -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,16 +7,22 @@ import chat.rocket.android.R ...@@ -7,16 +7,22 @@ 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
init { init {
...@@ -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,15 +7,18 @@ import chat.rocket.android.R ...@@ -7,15 +7,18 @@ 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
class ChatRoomAdapter( 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<*>>) {
this.dataSet.addAll(0, dataSet) val item = dataSet.firstOrNull { newItem ->
notifyItemRangeInserted(0, dataSet.size) this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1
}
if (item == null) {
this.dataSet.addAll(0, dataSet)
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) {
...@@ -20,6 +22,9 @@ class MessageViewHolder( ...@@ -20,6 +22,9 @@ class MessageViewHolder(
override fun bindViews(data: MessageViewModel) { override fun bindViews(data: MessageViewModel) {
with(itemView) { with(itemView) {
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) {
......
...@@ -6,8 +6,8 @@ import chat.rocket.android.chatroom.adapter.AutoCompleteType ...@@ -6,8 +6,8 @@ import chat.rocket.android.chatroom.adapter.AutoCompleteType
import chat.rocket.android.chatroom.adapter.PEOPLE import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.ROOMS import chat.rocket.android.chatroom.adapter.ROOMS
import chat.rocket.android.chatroom.domain.UriInteractor import chat.rocket.android.chatroom.domain.UriInteractor
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
...@@ -458,6 +458,23 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -458,6 +458,23 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} }
} }
/**
* 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)
......
...@@ -104,4 +104,5 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -104,4 +104,5 @@ interface ChatRoomView : LoadingView, MessageView {
fun showConnectionState(state: State) fun showConnectionState(state: State)
fun populateMembers(members: List<PeopleViewModel>) fun populateMembers(members: List<PeopleViewModel>)
fun populateRooms(chatRooms: List<ChatRoomViewModel>) fun populateRooms(chatRooms: List<ChatRoomViewModel>)
fun showReactionsPopup(messageId: String)
} }
\ No newline at end of file
...@@ -20,14 +20,16 @@ import dagger.android.DispatchingAndroidInjector ...@@ -20,14 +20,16 @@ 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)
} }
} }
...@@ -35,6 +37,7 @@ private const val INTENT_CHAT_ROOM_ID = "chat_room_id" ...@@ -35,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>
...@@ -47,6 +50,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -47,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)
...@@ -70,8 +74,10 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -70,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)
} }
} }
......
...@@ -23,15 +23,13 @@ import chat.rocket.android.chatroom.presentation.ChatRoomPresenter ...@@ -23,15 +23,13 @@ import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.BaseViewModel import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel import chat.rocket.android.chatroom.viewmodel.ChatRoomViewModel
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.PeopleViewModel import chat.rocket.android.chatroom.viewmodel.PeopleViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.* 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 dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
...@@ -42,13 +40,14 @@ import kotlinx.android.synthetic.main.message_list.* ...@@ -42,13 +40,14 @@ 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)
} }
} }
} }
...@@ -58,19 +57,18 @@ private const val BUNDLE_CHAT_ROOM_NAME = "chat_room_name" ...@@ -58,19 +57,18 @@ 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
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
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 lateinit var emojiKeyboardPopup: EmojiKeyboardPopup private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
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
...@@ -95,6 +93,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -95,6 +93,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" }
} }
...@@ -159,9 +158,31 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -159,9 +158,31 @@ 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
// Loop over received messages to determine first unread
for (i in dataSet.indices) {
val msgModel = dataSet[i]
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
...@@ -178,6 +199,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -178,6 +199,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)
} }
...@@ -222,6 +244,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -222,6 +244,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) {
...@@ -296,6 +321,26 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener { ...@@ -296,6 +321,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
......
...@@ -11,7 +11,10 @@ data class MessageViewModel( ...@@ -11,7 +11,10 @@ data class MessageViewModel(
override val time: CharSequence, override val time: CharSequence,
override val senderName: CharSequence, override val senderName: CharSequence,
override val content: CharSequence, override val content: CharSequence,
override val isPinned: Boolean override val isPinned: Boolean,
override var reactions: List<ReactionViewModel>,
override var 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
...@@ -5,13 +5,15 @@ import chat.rocket.core.model.Message ...@@ -5,13 +5,15 @@ import chat.rocket.core.model.Message
import chat.rocket.core.model.url.Url import chat.rocket.core.model.url.Url
data class UrlPreviewViewModel( data class UrlPreviewViewModel(
override val message: Message, override val message: Message,
override val rawData: Url, override val rawData: Url,
override val messageId: String, override val messageId: String,
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,16 +140,45 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -138,16 +140,45 @@ 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)
}
} }
} }
} }
} }
val content = getContent(context, message, quote) val content = getContent(context, getMessageWithoutQuoteMarkdown(message), quote)
MessageViewModel(message = message, rawData = message, messageId = message.id, MessageViewModel(message = getMessageWithoutQuoteMarkdown(message), rawData = message,
avatar = avatar!!, time = time, senderName = sender, messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned) content = content, isPinned = message.pinned, 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 {
...@@ -194,7 +225,7 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -194,7 +225,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.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,9 +11,7 @@ import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider ...@@ -11,9 +11,7 @@ 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
...@@ -37,8 +35,6 @@ abstract class ActivityBuilder { ...@@ -37,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
]) ])
......
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 android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.ui.MainActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
class MainActivityModule {
// @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
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,6 +2,7 @@ package chat.rocket.android.main.presentation ...@@ -2,6 +2,7 @@ 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
...@@ -27,4 +28,9 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C ...@@ -27,4 +28,9 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C
SettingsFragment.newInstance() 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,
...@@ -30,19 +35,21 @@ class MainPresenter @Inject constructor(private val navigator: MainNavigator, ...@@ -30,19 +35,21 @@ class MainPresenter @Inject constructor(private val navigator: MainNavigator,
* 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
...@@ -7,25 +8,21 @@ import android.support.v7.app.AppCompatActivity ...@@ -7,25 +8,21 @@ import android.support.v7.app.AppCompatActivity
import android.view.Gravity import android.view.Gravity
import android.view.MenuItem import android.view.MenuItem
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
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.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.main.presentation.MainPresenter import chat.rocket.android.main.presentation.MainPresenter
import chat.rocket.android.main.presentation.MainView import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.util.extensions.showToast 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
...@@ -68,6 +65,8 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector { ...@@ -68,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() {
......
...@@ -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
...@@ -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,23 +46,29 @@ const val ALLOW_MESSAGE_EDITING = "Message_AllowEditing" ...@@ -42,23 +46,29 @@ 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
fun PublicSettings.ldapEnabled(): Boolean = this[LDAP_ENABLE]?.value == true
// Message settings // Message settings
fun PublicSettings.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true fun PublicSettings.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
...@@ -67,11 +77,6 @@ fun PublicSettings.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING ...@@ -67,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.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
...@@ -63,4 +72,37 @@ fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float ...@@ -63,4 +72,37 @@ 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>
<?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
...@@ -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"
......
...@@ -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
<?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"
......
...@@ -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="6dp" android:background="?android:attr/selectableItemBackground"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins" android:paddingBottom="@dimen/member_item_top_and_bottom_padding"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins" android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:layout_marginTop="6dp"> android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/member_item_top_and_bottom_padding">
<include <include
android:id="@+id/layout_avatar" android:id="@+id/layout_avatar"
......
...@@ -4,10 +4,13 @@ ...@@ -4,10 +4,13 @@
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="6dp" android:background="?android:attr/selectableItemBackground"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins" android:clickable="true"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins" android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:layout_marginTop="6dp"> android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:focusable="true">
<include <include
android:id="@+id/layout_avatar" android:id="@+id/layout_avatar"
...@@ -16,7 +19,30 @@ ...@@ -16,7 +19,30 @@
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toBottomOf="@id/new_messages_notif" />
<LinearLayout
android:id="@+id/new_messages_notif"
tools:visibility="visible"
android:visibility="gone"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<View
android:layout_gravity="center"
android:layout_height="1dp"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_marginRight="4dp"
android:background="@color/red"/>
<TextView
android:layout_width="wrap_content"
android:text="@string/msg_unread_messages"
android:layout_height="wrap_content"
android:textColor="@color/red" />
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/top_container" android:id="@+id/top_container"
...@@ -24,6 +50,7 @@ ...@@ -24,6 +50,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:orientation="horizontal" android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@id/new_messages_notif"
app:layout_constraintLeft_toRightOf="@+id/layout_avatar"> app:layout_constraintLeft_toRightOf="@+id/layout_avatar">
<TextView <TextView
...@@ -47,11 +74,18 @@ ...@@ -47,11 +74,18 @@
style="@style/Message.TextView" style="@style/Message.TextView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="2dp" android:layout_marginBottom="2dp"
android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="@id/top_container" app:layout_constraintLeft_toLeftOf="@id/top_container"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_container" app:layout_constraintTop_toBottomOf="@+id/top_container"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" /> tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<include layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@+id/text_content"
app:layout_constraintStart_toStartOf="@+id/text_content"
app:layout_constraintTop_toBottomOf="@+id/text_content" />
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
\ 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="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="2dp"
android:layout_marginRight="2dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:descendantFocusability="beforeDescendants"
android:background="@drawable/rounded_background"
android:orientation="horizontal">
<TextView
android:id="@+id/text_emoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingLeft="4dp"
android:paddingStart="4dp"
android:textColor="#868585"
android:textSize="16sp"
tools:text=":)" />
<TextView
android:id="@+id/text_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingBottom="4dp"
android:paddingEnd="4dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingStart="4dp"
android:paddingTop="4dp"
android:textColor="#868585"
android:textSize="16sp"
android:textStyle="bold"
tools:text="12" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view_reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
\ No newline at end of file
...@@ -5,9 +5,10 @@ ...@@ -5,9 +5,10 @@
android:id="@+id/attachment_container" android:id="@+id/attachment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="72dp" android:background="?android:attr/selectableItemBackground"
android:orientation="vertical"
android:paddingEnd="@dimen/screen_edge_left_and_right_margins" android:paddingEnd="@dimen/screen_edge_left_and_right_margins"
android:orientation="vertical"> android:paddingStart="72dp">
<com.facebook.drawee.view.SimpleDraweeView <com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_attachment" android:id="@+id/image_attachment"
...@@ -38,4 +39,9 @@ ...@@ -38,4 +39,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="Filename.png" /> tools:text="Filename.png" />
<include
layout="@layout/layout_reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/url_preview_layout" android:id="@+id/url_preview_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="72dp" android:background="?android:attr/selectableItemBackground"
android:paddingEnd="24dp"> android:paddingEnd="24dp"
android:paddingStart="72dp">
<com.facebook.drawee.view.SimpleDraweeView <com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_preview" android:id="@+id/image_preview"
android:layout_width="70dp" android:layout_width="70dp"
android:layout_height="50dp" android:layout_height="50dp"
app:actualImageScaleType="centerCrop"/> app:actualImageScaleType="centerCrop" />
<TextView <TextView
android:id="@+id/text_host" android:id="@+id/text_host"
...@@ -21,26 +21,33 @@ ...@@ -21,26 +21,33 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:textColor="@color/colorSecondaryText" android:textColor="@color/colorSecondaryText"
tools:text="www.uol.com.br"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_preview" /> app:layout_constraintStart_toEndOf="@+id/image_preview"
tools:text="www.uol.com.br" />
<TextView <TextView
android:id="@+id/text_title" android:id="@+id/text_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/colorAccent" android:textColor="@color/colorAccent"
tools:text="Web page title"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host" app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_host"/> app:layout_constraintTop_toBottomOf="@id/text_host"
tools:text="Web page title" />
<TextView <TextView
android:id="@+id/text_description" android:id="@+id/text_description"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="description"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host" app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_title"/> app:layout_constraintTop_toBottomOf="@id/text_title"
tools:text="description" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/image_preview"
app:layout_constraintTop_toBottomOf="@+id/text_description" />
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -22,6 +22,11 @@ ...@@ -22,6 +22,11 @@
android:icon="@drawable/ic_content_copy_black_24px" android:icon="@drawable/ic_content_copy_black_24px"
android:title="@string/action_msg_copy" /> android:title="@string/action_msg_copy" />
<item
android:id="@+id/action_menu_msg_react"
android:icon="@drawable/ic_add_reaction"
android:title="@string/action_msg_add_reaction" />
<!--<item--> <!--<item-->
<!--android:id="@+id/action_menu_msg_share"--> <!--android:id="@+id/action_menu_msg_share"-->
<!--andrtextIconicon="@drawable/ic_share_black_24px"--> <!--andrtextIconicon="@drawable/ic_share_black_24px"-->
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
<string name="title_sign_in_your_server">Faça login no seu servidor</string> <string name="title_sign_in_your_server">Faça login no seu servidor</string>
<string name="title_log_in">Entrar</string> <string name="title_log_in">Entrar</string>
<string name="title_sign_up">Inscreva-se</string> <string name="title_sign_up">Inscreva-se</string>
<string name="title_authentication">Autenticação</string>
<string name="title_legal_terms">Termos Legais</string> <string name="title_legal_terms">Termos Legais</string>
<string name="title_chats">Chats</string> <string name="title_chats">Chats</string>
<string name="title_profile">Perfil</string> <string name="title_profile">Perfil</string>
...@@ -14,6 +15,7 @@ ...@@ -14,6 +15,7 @@
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Conectar</string> <string name="action_connect">Conectar</string>
<string name="action_login_or_sign_up">Toque este botão para fazer login ou criar uma conta</string>
<string name="action_terms_of_service">Termos de Serviço</string> <string name="action_terms_of_service">Termos de Serviço</string>
<string name="action_privacy_policy">Política de Privacidade</string> <string name="action_privacy_policy">Política de Privacidade</string>
<string name="action_search">Pesquisar</string> <string name="action_search">Pesquisar</string>
...@@ -64,6 +66,7 @@ ...@@ -64,6 +66,7 @@
<string name="msg_utc_offset">Deslocamento de UTC</string> <string name="msg_utc_offset">Deslocamento de UTC</string>
<string name="msg_new_password">Informe a nova senha</string> <string name="msg_new_password">Informe a nova senha</string>
<string name="msg_confirm_password">Confirme a nova senha</string> <string name="msg_confirm_password">Confirme a nova senha</string>
<string name="msg_unread_messages">Mensagens não lidas</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string> <string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string>
...@@ -82,10 +85,11 @@ ...@@ -82,10 +85,11 @@
<string name="action_msg_quote">Citar</string> <string name="action_msg_quote">Citar</string>
<string name="action_msg_delete">Remover</string> <string name="action_msg_delete">Remover</string>
<string name="action_msg_pin">Fixar Mensagem</string> <string name="action_msg_pin">Fixar Mensagem</string>
<string name="action_msg_unpin">Desafixar Messagem</string> <string name="action_msg_unpin">Desafixar Mensagem</string>
<string name="action_msg_star">Favoritar Mensagem</string> <string name="action_msg_star">Favoritar Mensagem</string>
<string name="action_msg_share">Compartilhar</string> <string name="action_msg_share">Compartilhar</string>
<string name="action_title_editing">Editando Mensagem</string> <string name="action_title_editing">Editando Mensagem</string>
<string name="action_msg_add_reaction">Adicionar reação</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">Edição não permitida</string> <string name="permission_editing_not_allowed">Edição não permitida</string>
......
...@@ -4,6 +4,11 @@ ...@@ -4,6 +4,11 @@
<!-- Default screen margins, per the Android Design guidelines. --> <!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="screen_edge_left_and_right_margins">16dp</dimen> <dimen name="screen_edge_left_and_right_margins">16dp</dimen>
<dimen name="screen_edge_left_and_right_padding">16dp</dimen>
<dimen name="chat_item_top_and_bottom_padding">12dp</dimen>
<dimen name="message_item_top_and_bottom_padding">6dp</dimen>
<dimen name="member_item_top_and_bottom_padding">6dp</dimen>
<dimen name="edit_text_margin">10dp</dimen> <dimen name="edit_text_margin">10dp</dimen>
<dimen name="edit_text_drawable_padding">16dp</dimen> <dimen name="edit_text_drawable_padding">16dp</dimen>
...@@ -19,6 +24,8 @@ ...@@ -19,6 +24,8 @@
<!-- Emoji --> <!-- Emoji -->
<dimen name="picker_padding_bottom">16dp</dimen> <dimen name="picker_padding_bottom">16dp</dimen>
<dimen name="supposed_keyboard_height">252dp</dimen> <dimen name="supposed_keyboard_height">252dp</dimen>
<dimen name="picker_popup_height">250dp</dimen>
<dimen name="picker_popup_width">300dp</dimen>
<!-- Message --> <!-- Message -->
<dimen name="padding_quote">8dp</dimen> <dimen name="padding_quote">8dp</dimen>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<string name="title_sign_in_your_server">Sign in your server</string> <string name="title_sign_in_your_server">Sign in your server</string>
<string name="title_log_in">Log in</string> <string name="title_log_in">Log in</string>
<string name="title_sign_up">Sign up</string> <string name="title_sign_up">Sign up</string>
<string name="title_authentication">Authentication</string>
<string name="title_legal_terms">Legal Terms</string> <string name="title_legal_terms">Legal Terms</string>
<string name="title_chats">Chats</string> <string name="title_chats">Chats</string>
<string name="title_profile">Profile</string> <string name="title_profile">Profile</string>
...@@ -15,6 +16,7 @@ ...@@ -15,6 +16,7 @@
<!-- Actions --> <!-- Actions -->
<string name="action_connect">Connect</string> <string name="action_connect">Connect</string>
<string name="action_login_or_sign_up">Tap this button to log in or create an account</string>
<string name="action_terms_of_service">Terms of Service</string> <string name="action_terms_of_service">Terms of Service</string>
<string name="action_privacy_policy">Privacy Policy</string> <string name="action_privacy_policy">Privacy Policy</string>
<string name="action_search">Search</string> <string name="action_search">Search</string>
...@@ -66,6 +68,7 @@ ...@@ -66,6 +68,7 @@
<string name="msg_utc_offset">UTC offset</string> <string name="msg_utc_offset">UTC offset</string>
<string name="msg_new_password">Enter New Password</string> <string name="msg_new_password">Enter New Password</string>
<string name="msg_confirm_password">Confirm New Password</string> <string name="msg_confirm_password">Confirm New Password</string>
<string name="msg_unread_messages">Unread messages</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string> <string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string>
...@@ -88,6 +91,7 @@ ...@@ -88,6 +91,7 @@
<string name="action_msg_star">Star Message</string> <string name="action_msg_star">Star Message</string>
<string name="action_msg_share">Share</string> <string name="action_msg_share">Share</string>
<string name="action_title_editing">Editing Message</string> <string name="action_title_editing">Editing Message</string>
<string name="action_msg_add_reaction">Add reaction</string>
<!-- Permission messages --> <!-- Permission messages -->
<string name="permission_editing_not_allowed">Editing is not allowed</string> <string name="permission_editing_not_allowed">Editing is not allowed</string>
......
...@@ -30,6 +30,7 @@ ext { ...@@ -30,6 +30,7 @@ ext {
markwon : '1.0.3', markwon : '1.0.3',
sheetMenu : '1.3.3', sheetMenu : '1.3.3',
aVLoadingIndicatorView : '2.1.3', aVLoadingIndicatorView : '2.1.3',
flexbox : '0.3.2',
// For testing // For testing
junit : '4.12', junit : '4.12',
...@@ -49,6 +50,7 @@ ext { ...@@ -49,6 +50,7 @@ ext {
design : "com.android.support:design:${versions.support}", design : "com.android.support:design:${versions.support}",
constraintLayout : "com.android.support.constraint:constraint-layout:${versions.constraintLayout}", constraintLayout : "com.android.support.constraint:constraint-layout:${versions.constraintLayout}",
cardView : "com.android.support:cardview-v7:${versions.support}", cardView : "com.android.support:cardview-v7:${versions.support}",
flexbox : "com.google.android:flexbox:${versions.flexbox}",
androidKtx : "androidx.core:core-ktx:${versions.androidKtx}", androidKtx : "androidx.core:core-ktx:${versions.androidKtx}",
......
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