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 {
implementation libraries.design
implementation libraries.constraintLayout
implementation libraries.cardView
implementation libraries.flexbox
implementation libraries.androidKtx
......
......@@ -41,7 +41,12 @@
android:theme="@style/AppTheme" />
<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:theme="@style/AppTheme" />
......
......@@ -4,17 +4,20 @@ import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.generateRandomString
import chat.rocket.android.util.extensions.isEmailValid
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatTwoFactorException
import chat.rocket.common.model.Token
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.*
import kotlinx.coroutines.experimental.delay
import java.util.concurrent.TimeUnit
import javax.inject.Inject
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()
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
fun setup() {
fun setupView() {
val server = serverInteractor.get()
if (server == null) {
navigator.toServerScreen()
return
}
val settings = settingsInteractor.get(server)
if (settings == null) {
navigator.toServerScreen()
return
if (settings.isLoginFormEnabled()) {
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
if (settings.facebookEnabled()) {
var totalSocialAccountsEnabled = 0
if (settings.isFacebookAuthenticationEnabled()) {
view.enableLoginByFacebook()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.githubEnabled()) {
if (settings.isGithubAuthenticationEnabled()) {
view.enableLoginByGithub()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.googleEnabled()) {
if (settings.isGoogleAuthenticationEnabled()) {
view.enableLoginByGoogle()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.linkedinEnabled()) {
if (settings.isLinkedinAuthenticationEnabled()) {
view.enableLoginByLinkedin()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.meteorEnabled()) {
if (settings.isMeteorAuthenticationEnabled()) {
view.enableLoginByMeteor()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.twitterEnabled()) {
if (settings.isTwitterAuthenticationEnabled()) {
view.enableLoginByTwitter()
hasSocial = true
totalSocialAccountsEnabled++
}
if (settings.gitlabEnabled()) {
if (settings.isGitlabAuthenticationEnabled()) {
view.enableLoginByGitlab()
hasSocial = true
totalSocialAccountsEnabled++
}
if (totalSocialAccountsEnabled > 0) {
view.showOauthView()
if (totalSocialAccountsEnabled > 3) {
view.setupFabListener()
}
}
view.showOauthView(hasSocial)
}
fun authenticate(usernameOrEmail: String, password: String) {
......@@ -90,34 +111,23 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
else -> {
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.disableUserInput()
view.showLoading()
try {
var token: Token? = null
if (usernameOrEmail.isEmailValid()) {
token = client.loginWithEmail(usernameOrEmail, password)
val token = if (usernameOrEmail.isEmailValid()) {
client.loginWithEmail(usernameOrEmail, password)
} else {
val settings = settingsInteractor.get(server)
if (settings != null) {
token = if (settings.ldapEnabled()) {
if (settings.isLdapAuthenticationEnabled()) {
client.loginWithLdap(usernameOrEmail, password)
} else {
client.login(usernameOrEmail, password)
}
} else {
navigator.toServerScreen()
}
}
if (token != null) {
val me = client.me()
multiServerRepository.save(server, TokenModel(token.userId, token.authToken))
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
saveToken(server, TokenModel(token.userId, token.authToken), client.me().username)
registerPushToken()
navigator.toChatList()
} else {
view.showGenericErrorMessage()
}
} catch (exception: RocketChatException) {
when (exception) {
is RocketChatTwoFactorException -> {
......@@ -133,6 +143,7 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
}
} finally {
view.hideLoading()
view.enableUserInput()
}
} else {
view.showNoInternetConnection()
......@@ -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()
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() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it)
......
......@@ -7,62 +7,135 @@ import chat.rocket.android.core.behaviours.MessageView
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.
* 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()
/**
* Shows the login by Github view.
* Shows the "login by Github" view if it is enabled by the server settings.
*/
fun enableLoginByGithub()
/**
* Shows the login by Google view.
* Shows the "login by Google" view if it is enabled by the server settings.
*/
fun enableLoginByGoogle()
/**
* Shows the login by Linkedin view.
* Shows the "login by Linkedin" view if it is enabled by the server settings.
*/
fun enableLoginByLinkedin()
/**
* Shows the login by Meteor view.
* Shows the "login by Meteor" view if it is enabled by the server settings.
*/
fun enableLoginByMeteor()
/**
* Shows the login by Twitter view.
* Shows the "login by Twitter" view if it is enabled by the server settings.
*/
fun enableLoginByTwitter()
/**
* Shows the login by Gitlab view.
* Shows the "login by Gitlab" view if it is enabled by the server settings.
*/
fun enableLoginByGitlab()
/**
* Shows the sign up view if the server settings allow the new users registration.
*
* @param value True to show the sign up view, false otherwise.
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*/
fun showSignUpView(value: Boolean)
fun setupFabListener()
fun setupGlobalListener()
/**
* Alerts the user about a wrong inputted username or email.
......
package chat.rocket.android.authentication.login.ui
import DrawableHelper
import android.content.Context
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
......@@ -12,30 +13,24 @@ import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.ImageButton
import android.widget.ScrollView
import chat.rocket.android.R
import chat.rocket.android.authentication.login.presentation.LoginPresenter
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.TextHelper
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.*
import chat.rocket.android.webview.cas.ui.webViewIntent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject
internal const val REQUEST_CODE_FOR_CAS = 1
class LoginFragment : Fragment(), LoginView {
@Inject lateinit var presenter: LoginPresenter
@Inject lateinit var appContext: Context // TODO we really need it? Check alternatives...
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
areLoginOptionsNeeded()
}
private var isGlobalLayoutListenerSetUp = false
companion object {
......@@ -47,7 +42,8 @@ class LoginFragment : Fragment(), LoginView {
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?) {
super.onViewCreated(view, savedInstanceState)
......@@ -56,54 +52,143 @@ class LoginFragment : Fragment(), LoginView {
tintEditTextDrawableStart()
}
presenter.setup()
showThreeSocialMethods()
presenter.setupView()
}
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 {
presenter.authenticate(text_username_or_email.textContent, text_password.textContent)
}
}
setupFabListener()
setupSignUpListener()
override fun enableUserInput() {
button_log_in.isEnabled = true
text_username_or_email.isEnabled = true
text_password.isEnabled = true
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
override fun disableUserInput() {
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() {
super.onDestroyView()
if (isGlobalLayoutListenerSetUp) {
scroll_view.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
isGlobalLayoutListenerSetUp = false
override fun hideCasButton() {
button_cas.setVisible(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) {
if (value) {
social_accounts_container.setVisible(true)
button_fab.setVisible(true)
override fun showSignUpView() {
text_new_to_rocket_chat.setVisible(true)
}
// 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 setupSignUpView() {
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()
}
} else {
TextHelper.addLink(text_new_to_rocket_chat, arrayOf(signUp), arrayOf(signUpListener))
}
override fun hideSignUpView() {
text_new_to_rocket_chat.setVisible(false)
}
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 setupFabListener() {
button_fab.setOnClickListener({
button_fab.hide()
showRemainingSocialAccountsView()
scrollToBottom()
})
override fun hideLoginButton() {
button_log_in.setVisible(false)
}
override fun enableLoginByFacebook() {
......@@ -134,99 +219,71 @@ class LoginFragment : Fragment(), LoginView {
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() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_username_or_email)
vibrateSmartPhone()
text_username_or_email.shake()
text_username_or_email.requestFocus()
}
override fun alertWrongPassword() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_password)
vibrateSmartPhone()
text_password.shake()
text_password.requestFocus()
}
override fun showLoading() {
enableUserInput(false)
view_loading.setVisible(true)
private fun showRemainingSocialAccountsView() {
social_accounts_container.postDelayed({
(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() {
view_loading.setVisible(false)
enableUserInput(true)
// Scrolling to the bottom of the screen.
private fun scrollToBottom() {
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() {
if (!isEditTextEmpty() || KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) {
showSignUpView(false)
showOauthView(false)
showLoginButton(true)
hideSignUpView()
hideOauthView()
showLoginButton()
} else {
showSignUpView(true)
showOauthView(true)
showLoginButton(false)
showSignUpView()
showOauthView()
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.
private fun isEditTextEmpty(): Boolean = 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 isEditTextEmpty(): Boolean {
return text_username_or_email.textContent.isBlank() && text_password.textContent.isEmpty()
}
private fun showThreeSocialMethods() {
private fun showThreeSocialAccountsMethods() {
var count = 0
for (i in 0..social_accounts_container.childCount) {
val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue
......@@ -236,10 +293,4 @@ class LoginFragment : Fragment(), LoginView {
}
}
}
private fun scrollToBottom() {
scroll_view.postDelayed({
scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}, 1250)
}
}
\ No newline at end of file
......@@ -9,7 +9,7 @@ import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.main.ui.MainActivity
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) {
......@@ -37,10 +37,7 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int
}
fun toChatList() {
val chatList = Intent(activity, MainActivity::class.java).apply {
//TODO any parameter to pass
}
activity.startActivity(chatList)
activity.startActivity(Intent(activity, MainActivity::class.java))
activity.finish()
}
......
......@@ -17,16 +17,14 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val refreshSettingsInteractor: RefreshSettingsInteractor) {
fun connect(server: String) {
if (!UrlHelper.isValidUrl(server)) {
view.showInvalidServerUrl()
view.showInvalidServerUrlMessage()
} else {
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
try {
refreshSettingsInteractor.refresh(server)
serverInteractor.save(server)
navigator.toLogin()
} catch (ex: Exception) {
ex.message?.let {
......
......@@ -7,7 +7,7 @@ import chat.rocket.android.core.behaviours.MessageView
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 {
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?) {
super.onViewCreated(view, savedInstanceState)
......@@ -43,7 +44,7 @@ class ServerFragment : Fragment(), ServerView {
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() {
enableUserInput(false)
......
package chat.rocket.android.authentication.signup.ui
import DrawableHelper
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
......@@ -11,19 +10,15 @@ import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.signup.presentation.SignupPresenter
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.TextHelper
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
import javax.inject.Inject
class SignupFragment : Fragment(), SignupView {
@Inject lateinit var presenter: SignupPresenter
@Inject lateinit var appContext: Context
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(constraint_layout.rootView)) {
......@@ -68,26 +63,26 @@ class SignupFragment : Fragment(), SignupView {
}
override fun alertBlankName() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_username)
text_username.requestFocus()
vibrateSmartPhone()
text_name.shake()
text_name.requestFocus()
}
override fun alertBlankUsername() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_username)
vibrateSmartPhone()
text_username.shake()
text_username.requestFocus()
}
override fun alertEmptyPassword() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_password)
vibrateSmartPhone()
text_password.shake()
text_password.requestFocus()
}
override fun alertBlankEmail() {
AnimationHelper.vibrateSmartPhone(appContext)
AnimationHelper.shakeView(text_email)
vibrateSmartPhone()
text_email.shake()
text_email.requestFocus()
}
......@@ -114,15 +109,17 @@ class SignupFragment : Fragment(), SignupView {
}
private fun tintEditTextDrawableStart() {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, appContext)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, appContext)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, appContext)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, appContext)
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, this)
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, appContext, R.color.colorDrawableTintGrey)
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() {
......
......@@ -12,10 +12,7 @@ import android.view.inputmethod.InputMethodManager
import chat.rocket.android.R
import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter
import chat.rocket.android.authentication.twofactor.presentation.TwoFAView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_two_fa.*
import javax.inject.Inject
......@@ -65,10 +62,8 @@ class TwoFAFragment : Fragment(), TwoFAView {
}
override fun alertBlankTwoFactorAuthenticationCode() {
activity?.let {
AnimationHelper.vibrateSmartPhone(it)
AnimationHelper.shakeView(text_two_factor_auth)
}
vibrateSmartPhone()
text_two_factor_auth.shake()
}
override fun alertInvalidTwoFactorAuthenticationCode() = showMessage(getString(R.string.msg_invalid_2fa_code))
......
......@@ -4,10 +4,13 @@ import android.view.View
import chat.rocket.android.chatroom.viewmodel.AudioAttachmentViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_attachment.view.*
class AudioAttachmentViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<AudioAttachmentViewModel>(itemView, listener) {
class AudioAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<AudioAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
......
......@@ -7,14 +7,20 @@ import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
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.isSystemMessage
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexboxLayoutManager
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
abstract class BaseViewHolder<T : BaseViewModel<*>>(
itemView: View,
private val listener: ActionsListener
private val listener: ActionsListener,
var reactionListener: EmojiReactionListener? = null
) : RecyclerView.ViewHolder(itemView),
MenuItem.OnMenuItemClickListener {
var data: T? = null
......@@ -26,6 +32,39 @@ abstract class BaseViewHolder<T : BaseViewModel<*>>(
fun bind(data: T) {
this.data = 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)
......
......@@ -7,7 +7,9 @@ import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.viewmodel.*
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.isSystemMessage
import timber.log.Timber
import java.security.InvalidParameterException
......@@ -15,7 +17,8 @@ class ChatRoomAdapter(
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseViewModel<*>>()
......@@ -25,26 +28,26 @@ class ChatRoomAdapter(
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
return when(viewType.toViewType()) {
return when (viewType.toViewType()) {
BaseViewModel.ViewType.MESSAGE -> {
val view = parent.inflate(R.layout.item_message)
MessageViewHolder(view, actionsListener)
MessageViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.IMAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
ImageAttachmentViewHolder(view, actionsListener)
ImageAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.AUDIO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
AudioAttachmentViewHolder(view, actionsListener)
AudioAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.VIDEO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
VideoAttachmentViewHolder(view, actionsListener)
VideoAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view, actionsListener)
UrlPreviewViewHolder(view, actionsListener, reactionListener)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
......@@ -61,6 +64,23 @@ class ChatRoomAdapter(
}
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) {
is MessageViewHolder -> holder.bind(dataSet[position] as MessageViewModel)
is ImageAttachmentViewHolder -> holder.bind(dataSet[position] as ImageAttachmentViewModel)
......@@ -86,16 +106,32 @@ class ChatRoomAdapter(
}
fun prependData(dataSet: List<BaseViewModel<*>>) {
val item = dataSet.firstOrNull { newItem ->
this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1
}
if (item == null) {
this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size)
}
}
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")
if (index > -1) {
message.nextDownStreamMessage = dataSet[index].nextDownStreamMessage
dataSet[index] = message
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(
}
}
}
R.id.action_menu_msg_react -> presenter?.showReactions(id)
else -> TODO("Not implemented")
}
}
......
......@@ -2,11 +2,14 @@ package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.*
class ImageAttachmentViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener) {
class ImageAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
init {
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
import android.text.method.LinkMovementMethod
import android.view.View
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.item_message.view.*
class MessageViewHolder(
itemView: View,
listener: ActionsListener
) : BaseViewHolder<MessageViewModel>(itemView, listener) {
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
......@@ -20,6 +22,9 @@ class MessageViewHolder(
override fun bindViews(data: MessageViewModel) {
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_sender.text = data.senderName
text_content.text = data.content
......
......@@ -6,10 +6,13 @@ import android.view.View
import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_url_preview.view.*
class UrlPreviewViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<UrlPreviewViewModel>(itemView, listener) {
class UrlPreviewViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<UrlPreviewViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
......
......@@ -4,10 +4,13 @@ import android.view.View
import chat.rocket.android.chatroom.viewmodel.VideoAttachmentViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_attachment.view.*
class VideoAttachmentViewHolder(itemView: View, listener: ActionsListener)
: BaseViewHolder<VideoAttachmentViewModel>(itemView, listener) {
class VideoAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<VideoAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
......
......@@ -6,8 +6,8 @@ import chat.rocket.android.chatroom.adapter.AutoCompleteType
import chat.rocket.android.chatroom.adapter.PEOPLE
import chat.rocket.android.chatroom.adapter.ROOMS
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.PeopleViewModel
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.UrlHelper
......@@ -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) {
launchUI(strategy) {
val viewModelStreamedMessage = mapper.map(streamedMessage)
......
......@@ -104,4 +104,5 @@ interface ChatRoomView : LoadingView, MessageView {
fun showConnectionState(state: State)
fun populateMembers(members: List<PeopleViewModel>)
fun populateRooms(chatRooms: List<ChatRoomViewModel>)
fun showReactionsPopup(messageId: String)
}
\ No newline at end of file
......@@ -20,14 +20,16 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.*
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 {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
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"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
......@@ -47,6 +50,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
......@@ -70,8 +74,10 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
setupToolbar()
chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
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
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
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.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.emoji.ComposerEditText
import chat.rocket.android.widget.emoji.Emoji
import chat.rocket.android.widget.emoji.EmojiKeyboardPopup
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.*
import chat.rocket.core.internal.realtime.State
import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable
......@@ -42,13 +40,14 @@ import kotlinx.android.synthetic.main.message_list.*
import timber.log.Timber
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 {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_NAME, chatRoomName)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
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"
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 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 parser: MessageParser
private lateinit var adapter: ChatRoomAdapter
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private lateinit var emojiKeyboardPopup: EmojiKeyboardPopup
private var chatRoomLastSeen: Long = -1
private lateinit var actionSnackbar: ActionSnackbar
private var citation: String? = null
private var editingMessageId: String? = null
......@@ -95,6 +93,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
chatRoomName = bundle.getString(BUNDLE_CHAT_ROOM_NAME)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
isChatRoomReadOnly = bundle.getBoolean(BUNDLE_IS_CHAT_ROOM_READ_ONLY)
chatRoomLastSeen = bundle.getLong(BUNDLE_CHAT_ROOM_LAST_SEEN)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
......@@ -159,9 +158,31 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
}
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 {
if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter)
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter,
reactionListener = this@ChatRoomFragment)
recycler_view.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
linearLayoutManager.stackFromEnd = true
......@@ -178,6 +199,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
val oldMessagesCount = adapter.itemCount
adapter.appendData(dataSet)
recycler_view.scrollToPosition(92)
if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
recycler_view.scrollToPosition(0)
}
......@@ -222,6 +244,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardPopup.Listener {
override fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>) {
adapter.updateItem(message.last())
if (message.size > 1) {
adapter.prependData(listOf(message.first()))
}
}
override fun dispatchDeleteMessage(msgId: String) {
......@@ -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) {
button_add_reaction.setImageResource(drawableId)
button_add_reaction.setTag(drawableId)
......
......@@ -10,8 +10,10 @@ data class AudioAttachmentViewModel(
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
override val layoutId: Int
......
......@@ -9,6 +9,8 @@ interface BaseViewModel<out T> {
val messageId: String
val viewType: Int
val layoutId: Int
var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>?
enum class ViewType(val viewType: Int) {
MESSAGE(0),
......
......@@ -10,7 +10,9 @@ data class ImageAttachmentViewModel(
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
......@@ -11,7 +11,10 @@ data class MessageViewModel(
override val time: CharSequence,
override val senderName: 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> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType
......
package chat.rocket.android.chatroom.viewmodel
data class ReactionViewModel(
val messageId: String,
val shortname: String,
val unicode: CharSequence,
val count: Int,
val usernames: List<String> = emptyList()
)
\ No newline at end of file
......@@ -11,7 +11,9 @@ data class UrlPreviewViewModel(
val title: CharSequence?,
val hostname: String,
val description: CharSequence?,
val thumbUrl: String?
val thumbUrl: String?,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseViewModel<Url> {
override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
......@@ -10,7 +10,9 @@ data class VideoAttachmentViewModel(
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null
) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
......@@ -14,6 +14,7 @@ import chat.rocket.android.helper.MessageParser
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
......@@ -83,7 +84,8 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val title = url.meta?.title
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<*>? {
......@@ -99,11 +101,11 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val id = "${message.id}_${attachment.titleLink}".hashCode().toLong()
return when (attachment) {
is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id)
attachmentUrl, attachmentTitle ?: "", id, getReactions(message))
is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id)
attachmentUrl, attachmentTitle ?: "", id, getReactions(message))
is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle ?: "", id)
attachmentUrl, attachmentTitle ?: "", id, getReactions(message))
else -> null
}
}
......@@ -138,16 +140,45 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val quoteUrl = HttpUrl.parse(url.url)
val serverUrl = HttpUrl.parse(baseUrl)
if (quoteUrl != null && serverUrl != null) {
quote = makeQuote(quoteUrl, serverUrl)
quote = makeQuote(quoteUrl, serverUrl)?.let {
getMessageWithoutQuoteMarkdown(it)
}
}
}
}
}
val content = getContent(context, getMessageWithoutQuoteMarkdown(message), quote)
MessageViewModel(message = getMessageWithoutQuoteMarkdown(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
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()
}
val content = getContent(context, message, quote)
MessageViewModel(message = message, rawData = message, messageId = message.id,
avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned)
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 {
......@@ -194,7 +225,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
var quoteViewModel: MessageViewModel? = null
if (quote != null) {
val quoteMessage: Message = quote
quoteViewModel = map(quoteMessage).first { it is MessageViewModel } as MessageViewModel
quoteViewModel = mapMessage(quoteMessage)
}
return parser.renderMarkdown(message.message, quoteViewModel, currentUsername)
}
......
......@@ -3,11 +3,9 @@ package chat.rocket.android.chatrooms.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
......@@ -22,9 +20,4 @@ class ChatRoomsFragmentModule {
fun provideLifecycleOwner(frag: ChatRoomsFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
\ No newline at end of file
package chat.rocket.android.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
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.infraestructure.ConnectionManager
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
......@@ -24,7 +25,7 @@ import javax.inject.Inject
class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val strategy: CancelStrategy,
private val navigator: ChatRoomsNavigator,
private val navigator: MainNavigator,
private val serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
......@@ -68,7 +69,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
}
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
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.PinnedMessagesActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
import chat.rocket.android.chatrooms.di.ChatRoomsModule
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.ui.MainActivity
import chat.rocket.android.members.di.MembersFragmentProvider
......@@ -37,8 +35,6 @@ abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(modules = [MainModule::class,
MainActivityProvider::class,
ChatRoomsModule::class,
ChatRoomsFragmentProvider::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
parentNode.appendChild(quoteNode)
quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length))
quoteNode = parser.parse("> ${toLenientMarkdown(quote.rawData.message)}")
quoteNode.accept(EmojiVisitor(builder))
quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
}
parentNode.accept(LinkVisitor(builder))
......
......@@ -13,6 +13,17 @@ object UrlHelper {
*/
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.
*
......
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
import android.arch.lifecycle.LifecycleOwner
import android.content.Context
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.main.presentation.MainNavigator
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 MainModule {
......@@ -13,4 +17,13 @@ class MainModule {
@Provides
@PerActivity
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
import android.content.Context
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.ui.ProfileFragment
......@@ -27,4 +28,9 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C
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
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.disconnect
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.unregisterPushToken
import timber.log.Timber
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 localRepository: LocalRepository,
managerFactory: ConnectionManagerFactory,
......@@ -30,19 +35,21 @@ class MainPresenter @Inject constructor(private val navigator: MainNavigator,
* Logout from current server.
*/
fun logout() {
// TODO: inject CancelStrategy, and MainView.
// launchUI(strategy) {
launchUI(strategy) {
try {
// clearTokens()
// client.logout()
clearTokens()
client.logout()
//TODO: Add the code to unsubscribe to all subscriptions.
client.disconnect()
// view.onLogout()
} catch (e: RocketChatException) {
Timber.e(e)
// view.showMessage(e.message!!)
view.onLogout()
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
// }
}
private suspend fun clearTokens() {
......
package chat.rocket.android.main.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
......@@ -7,25 +8,21 @@ import android.support.v7.app.AppCompatActivity
import android.view.Gravity
import android.view.MenuItem
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.main.presentation.MainPresenter
import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.util.extensions.showToast
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar.*
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 presenter: MainPresenter
private var isFragmentAdded: Boolean = false
......@@ -68,6 +65,8 @@ class MainActivity : AppCompatActivity(), MainView, HasSupportFragmentInjector {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun activityInjector(): AndroidInjector<Activity> = activityDispatchingAndroidInjector
override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector
private fun setupToolbar() {
......
......@@ -8,7 +8,7 @@ import chat.rocket.core.model.Value
import javax.inject.Inject
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()
fun mapToViewModelList(memberList: List<User>): List<MemberViewModel> {
......
package chat.rocket.android.profile.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.profile.presentation.ProfileView
import chat.rocket.android.profile.ui.ProfileFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
......@@ -22,9 +20,4 @@ class ProfileFragmentModule {
fun provideLifecycleOwner(frag: ProfileFragment): LifecycleOwner {
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
private val repository: SettingsRepository) {
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,
FAVORITE_ROOMS, ACCOUNT_LOGIN_FORM, ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB,
ACCOUNT_GITLAB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR, ACCOUNT_TWITTER, ACCOUNT_WORDPRESS,
LDAP_ENABLE, ACCOUNT_REGISTRATION, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE,
UPLOAD_WHITELIST_MIMETYPES, HIDE_USER_JOIN, HIDE_USER_LEAVE, HIDE_TYPE_AU, HIDE_MUTE_UNMUTE,
HIDE_TYPE_RU, ACCOUNT_CUSTOM_FIELDS, ALLOW_MESSAGE_DELETING, ALLOW_MESSAGE_EDITING,
ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
FAVORITE_ROOMS, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE, UPLOAD_WHITELIST_MIMETYPES,
HIDE_USER_JOIN, HIDE_USER_LEAVE,
HIDE_TYPE_AU, HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ALLOW_MESSAGE_DELETING,
ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
suspend fun refresh(server: String) {
withContext(CommonPool) {
......
......@@ -7,20 +7,25 @@ typealias PublicSettings = Map<String, Value<Any>>
interface SettingsRepository {
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_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_METEOR = "Accounts_OAuth_Meteor"
const val ACCOUNT_TWITTER = "Accounts_OAuth_Twitter"
const val ACCOUNT_WORDPRESS = "Accounts_OAuth_Wordpress"
const val ACCOUNT_REGISTRATION = "Accounts_RegistrationForm"
const val ACCOUNT_LOGIN_FORM = "Accounts_ShowFormLogin"
const val ACCOUNT_CUSTOM_FIELDS = "Accounts_CustomFields"
const val ACCOUNT_GITLAB = "Accounts_OAuth_Gitlab"
const val SITE_URL = "Site_Url"
const val SITE_NAME = "Site_Name"
......@@ -28,7 +33,6 @@ const val FAVICON_512 = "Assets_favicon_512"
const val USE_REALNAME = "UI_Use_Real_Name"
const val ALLOW_ROOM_NAME_SPECIAL_CHARS = "UI_Allow_room_names_with_special_chars"
const val FAVORITE_ROOMS = "Favorite_Rooms"
const val LDAP_ENABLE = "LDAP_Enable"
const val UPLOAD_STORAGE_TYPE = "FileUpload_Storage_Type"
const val UPLOAD_MAX_FILE_SIZE = "FileUpload_MaxFileSize"
const val UPLOAD_WHITELIST_MIMETYPES = "FileUpload_MediaTypeWhiteList"
......@@ -42,23 +46,29 @@ const val ALLOW_MESSAGE_EDITING = "Message_AllowEditing"
const val SHOW_DELETED_STATUS = "Message_ShowDeletedStatus"
const val SHOW_EDITED_STATUS = "Message_ShowEditedStatus"
const val ALLOW_MESSAGE_PINNING = "Message_AllowPinning"
/*
* Extension functions for Public Settings.
*
* 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
*/
fun PublicSettings.googleEnabled(): Boolean = this[ACCOUNT_GOOGLE]?.value == true
fun PublicSettings.facebookEnabled(): Boolean = this[ACCOUNT_FACEBOOK]?.value == true
fun PublicSettings.githubEnabled(): Boolean = this[ACCOUNT_GITHUB]?.value == true
fun PublicSettings.linkedinEnabled(): Boolean = this[ACCOUNT_LINKEDIN]?.value == true
fun PublicSettings.meteorEnabled(): Boolean = this[ACCOUNT_METEOR]?.value == true
fun PublicSettings.twitterEnabled(): Boolean = this[ACCOUNT_TWITTER]?.value == true
fun PublicSettings.gitlabEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true
fun PublicSettings.wordpressEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun PublicSettings.isLdapAuthenticationEnabled(): Boolean = this[LDAP_ENABLE]?.value == true
fun PublicSettings.isCasAuthenticationEnabled(): Boolean = this[CAS_ENABLE]?.value == true
fun PublicSettings.casLoginUrl(): String = this[CAS_LOGIN_URL]?.value.toString()
fun PublicSettings.isRegistrationEnabledForNewUsers(): Boolean = this[ACCOUNT_REGISTRATION]?.value == "Public"
fun PublicSettings.isLoginFormEnabled(): Boolean = this[ACCOUNT_LOGIN_FORM]?.value == true
fun PublicSettings.isPasswordResetEnabled(): Boolean = this[ACCOUNT_PASSWORD_RESET]?.value == true
fun PublicSettings.isGoogleAuthenticationEnabled(): Boolean = this[ACCOUNT_GOOGLE]?.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.ldapEnabled(): Boolean = this[LDAP_ENABLE]?.value == true
// Message settings
fun PublicSettings.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
......@@ -67,11 +77,6 @@ fun PublicSettings.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING
fun PublicSettings.allowedMessageEditing(): Boolean = this[ALLOW_MESSAGE_EDITING]?.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> {
val values = this[UPLOAD_WHITELIST_MIMETYPES]?.value
values?.let { it as String }?.split(",")?.let {
......
......@@ -14,12 +14,10 @@ class SharedPreferencesSettingsRepository(private val localRepository: LocalRepo
localRepository.save("$SETTINGS_KEY$url", adapter.toJson(settings))
}
override fun get(url: String): PublicSettings? {
val settings = localRepository.get("$SETTINGS_KEY$url")
settings?.let {
return adapter.fromJson(it)
override fun get(url: String): PublicSettings {
val settings = localRepository.get("$SETTINGS_KEY$url")!!
settings.let {
return adapter.fromJson(it)!!
}
return null
}
}
\ No newline at end of file
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.ViewAnimationUtils
import android.view.animation.AccelerateInterpolator
......@@ -64,3 +73,36 @@ fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float
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
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import ru.noties.markwon.Markwon
import java.security.SecureRandom
fun String.ifEmpty(value: String): String {
if (isEmpty()) {
......@@ -34,6 +35,17 @@ fun EditText.erase() {
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
get() = text.toString()
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.content.Context
......@@ -19,6 +19,7 @@ fun Context.webViewIntent(webPageUrl: String): Intent {
private const val INTENT_WEB_PAGE_URL = "web_page_url"
// Simple WebView to load URL.
class WebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
......
......@@ -9,10 +9,10 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.widget.emoji.EmojiKeyboardPopup.Listener
import java.util.*
class CategoryPagerAdapter(val listener: Listener) : PagerAdapter() {
class CategoryPagerAdapter(val listener: EmojiKeyboardListener) : PagerAdapter() {
override fun isViewFromObject(view: View, obj: Any): Boolean {
return view == obj
}
......@@ -46,7 +46,7 @@ class CategoryPagerAdapter(val listener: Listener) : PagerAdapter() {
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>()
fun addEmojis(emojis: List<Emoji>) {
......@@ -66,7 +66,7 @@ class CategoryPagerAdapter(val listener: Listener) : PagerAdapter() {
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)
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
private lateinit var searchView: View
private lateinit var backspaceView: View
private lateinit var parentContainer: ViewGroup
var listener: Listener? = null
var listener: EmojiKeyboardListener? = null
companion object {
const val PREF_EMOJI_RECENTS = "PREF_EMOJI_RECENTS"
}
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)
viewPager = view.findViewById(R.id.pager_categories)
searchView = view.findViewById(R.id.emoji_search)
......@@ -55,20 +55,17 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
private fun setupViewPager() {
context.let {
val callback = when (it) {
is Listener -> it
is EmojiKeyboardListener -> it
else -> {
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")
}
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) {
EmojiRepository.addToRecents(emoji)
callback.onEmojiAdded(emoji)
......@@ -78,14 +75,14 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
for (category in EmojiCategory.values()) {
val tab = tabLayout.getTabAt(category.ordinal)
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
textView.setImageResource(category.resourceIcon())
}
val currentTab = if (EmojiRepository.getRecents().isEmpty()) EmojiCategory.PEOPLE.ordinal else
EmojiCategory.RECENTS.ordinal
viewPager.setCurrentItem(currentTab)
viewPager.currentItem = currentTab
}
}
......@@ -132,22 +129,4 @@ class EmojiKeyboardPopup(context: Context, view: View) : OverKeyboardPopupWindow
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 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".webview.WebViewActivity">
tools:context=".webview.ui.WebViewActivity">
<include
android:id="@+id/layout_app_bar"
......
......@@ -11,35 +11,18 @@
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/colorDividerMessageComposer"
app:layout_constraintBottom_toTopOf="@+id/tabs"
app:layout_constraintBottom_toTopOf="@+id/picker_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="0dp"
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"
<include
android:id="@+id/picker_container"
layout="@layout/emoji_picker"
app:layout_constraintBottom_toTopOf="@+id/emoji_actions_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabs" />
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
android:id="@+id/emoji_actions_container"
......@@ -60,8 +43,8 @@
android:clickable="true"
android:focusable="true"
android:padding="8dp"
android:visibility="invisible"
android:src="@drawable/ic_search_gray_24px" />
android:src="@drawable/ic_search_gray_24px"
android:visibility="invisible" />
<ImageView
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 @@
app:layout_constraintRight_toRightOf="parent"
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
android:id="@+id/text_new_to_rocket_chat"
android:layout_width="wrap_content"
......@@ -57,7 +70,7 @@
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password"
app:layout_constraintTop_toBottomOf="@+id/button_cas"
tools:visibility="visible" />
<com.wang.avi.AVLoadingIndicatorView
......@@ -69,7 +82,7 @@
app:indicatorName="BallPulseIndicator"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_password"
app:layout_constraintTop_toBottomOf="@+id/button_cas"
tools:visibility="visible" />
<LinearLayout
......@@ -195,9 +208,8 @@
android:id="@+id/button_log_in"
style="@style/Authentication.Button"
android:text="@string/title_log_in"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
tools:visibility="gone" />
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" />
</android.support.constraint.ConstraintLayout>
</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 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="12dp">
android:background="?android:attr/selectableItemBackground"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/chat_item_top_and_bottom_padding"
android:paddingBottom="@dimen/chat_item_top_and_bottom_padding">
<include
android:id="@+id/layout_avatar"
......
......@@ -4,10 +4,11 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="6dp">
android:background="?android:attr/selectableItemBackground"
android:paddingBottom="@dimen/member_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/member_item_top_and_bottom_padding">
<include
android:id="@+id/layout_avatar"
......
......@@ -4,10 +4,13 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="6dp">
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
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
android:id="@+id/layout_avatar"
......@@ -16,7 +19,30 @@
android:layout_height="40dp"
android:layout_marginTop="5dp"
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
android:id="@+id/top_container"
......@@ -24,6 +50,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@id/new_messages_notif"
app:layout_constraintLeft_toRightOf="@+id/layout_avatar">
<TextView
......@@ -47,11 +74,18 @@
style="@style/Message.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="2dp"
android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="@id/top_container"
app:layout_constraintRight_toRightOf="parent"
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!" />
<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>
\ 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 @@
android:id="@+id/attachment_container"
android:layout_width="match_parent"
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:orientation="vertical">
android:paddingStart="72dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_attachment"
......@@ -38,4 +39,9 @@
android:layout_height="wrap_content"
tools:text="Filename.png" />
<include
layout="@layout/layout_reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/url_preview_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="72dp"
android:paddingEnd="24dp">
android:background="?android:attr/selectableItemBackground"
android:paddingEnd="24dp"
android:paddingStart="72dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_preview"
android:layout_width="70dp"
android:layout_height="50dp"
app:actualImageScaleType="centerCrop"/>
app:actualImageScaleType="centerCrop" />
<TextView
android:id="@+id/text_host"
......@@ -21,26 +21,33 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/colorSecondaryText"
tools:text="www.uol.com.br"
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
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/colorAccent"
tools:text="Web page title"
app:layout_constraintEnd_toEndOf="parent"
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
android:id="@+id/text_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="description"
app:layout_constraintEnd_toEndOf="parent"
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>
\ No newline at end of file
......@@ -22,6 +22,11 @@
android:icon="@drawable/ic_content_copy_black_24px"
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-->
<!--android:id="@+id/action_menu_msg_share"-->
<!--andrtextIconicon="@drawable/ic_share_black_24px"-->
......
......@@ -4,6 +4,7 @@
<string name="title_sign_in_your_server">Faça login no seu servidor</string>
<string name="title_log_in">Entrar</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_chats">Chats</string>
<string name="title_profile">Perfil</string>
......@@ -14,6 +15,7 @@
<!-- Actions -->
<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_privacy_policy">Política de Privacidade</string>
<string name="action_search">Pesquisar</string>
......@@ -64,6 +66,7 @@
<string name="msg_utc_offset">Deslocamento de UTC</string>
<string name="msg_new_password">Informe 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 -->
<string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string>
......@@ -82,10 +85,11 @@
<string name="action_msg_quote">Citar</string>
<string name="action_msg_delete">Remover</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_share">Compartilhar</string>
<string name="action_title_editing">Editando Mensagem</string>
<string name="action_msg_add_reaction">Adicionar reação</string>
<!-- Permission messages -->
<string name="permission_editing_not_allowed">Edição não permitida</string>
......
......@@ -4,6 +4,11 @@
<!-- 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_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_drawable_padding">16dp</dimen>
......@@ -19,6 +24,8 @@
<!-- Emoji -->
<dimen name="picker_padding_bottom">16dp</dimen>
<dimen name="supposed_keyboard_height">252dp</dimen>
<dimen name="picker_popup_height">250dp</dimen>
<dimen name="picker_popup_width">300dp</dimen>
<!-- Message -->
<dimen name="padding_quote">8dp</dimen>
......
......@@ -5,6 +5,7 @@
<string name="title_sign_in_your_server">Sign in your server</string>
<string name="title_log_in">Log in</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_chats">Chats</string>
<string name="title_profile">Profile</string>
......@@ -15,6 +16,7 @@
<!-- Actions -->
<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_privacy_policy">Privacy Policy</string>
<string name="action_search">Search</string>
......@@ -66,6 +68,7 @@
<string name="msg_utc_offset">UTC offset</string>
<string name="msg_new_password">Enter New Password</string>
<string name="msg_confirm_password">Confirm New Password</string>
<string name="msg_unread_messages">Unread messages</string>
<!-- System messages -->
<string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string>
......@@ -88,6 +91,7 @@
<string name="action_msg_star">Star Message</string>
<string name="action_msg_share">Share</string>
<string name="action_title_editing">Editing Message</string>
<string name="action_msg_add_reaction">Add reaction</string>
<!-- Permission messages -->
<string name="permission_editing_not_allowed">Editing is not allowed</string>
......
......@@ -30,6 +30,7 @@ ext {
markwon : '1.0.3',
sheetMenu : '1.3.3',
aVLoadingIndicatorView : '2.1.3',
flexbox : '0.3.2',
// For testing
junit : '4.12',
......@@ -49,6 +50,7 @@ ext {
design : "com.android.support:design:${versions.support}",
constraintLayout : "com.android.support.constraint:constraint-layout:${versions.constraintLayout}",
cardView : "com.android.support:cardview-v7:${versions.support}",
flexbox : "com.google.android:flexbox:${versions.flexbox}",
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