Commit b55a6a9e authored by Rafael Kellermann Streit's avatar Rafael Kellermann Streit

Fixed conflicts

parents 2adcb4c2 3597aeb3
# Mobile Team Releases Planning
This document describes the suggested method by which our mobile teams should plan for releases.
**What’s a release?**
The release is every version of the app that’s sent to the store as a production release. The release is always a major/minor update, like from (1.0.0 to 1.1.0 or to 2.0.0). Patch releases (from 1.0.0 to 1.0.1) won’t follow these steps and are considered hotfixes releases. Our versioning is following the [Semantic Versioning 2.0.0](https://semver.org) guide.
**What are the important days of a release?**
The first release candidate (TestFlight and Beta) needs be done by 27th of each month. The release will always happen on day 5th of each month, unless there’s some critical crash/bug happening.
**What happens if something could not be done in time for the release candidate?**
In general, if it’s a new feature, it’ll be postponed for the next release only. Under extreme circumstances, when it will result in significant business impact a extraordinary release could happen.
**When do we plan the release features/improvements/bugs?**
Every month can be a different day between 27th and 5th to plan the next release. The leader of the team will schedule the session and all the team will be able to participate in the planning. At this moment, most of the issues will be assigned to each member of the team.
Example (in April, 2018):
| Day | Description |
|------------|-------------------------|
| 27th Mar ~ 5th Apr | Planning new cycle |
| 5th Apr | Start new release cycle |
| 27th Apr | Release candidate |
| 5th May | Production release |
**How do we organize a release?**
Every release is a Project in GitHub. There are 6 boards on each project:
- **Desirable (temporary):** what we want to have on the release. This is very useful while planning. This is where everybody can add features/improvements that wanna see on the release;
- **Blocked:** when something is blocked (waiting asset, waiting API, etc) the issue will be on this board;
- **To-do:** after planning, all to-do issues come here;
- **In progress:** when something is in progress, the issue/PR will be on this board;
- **Review/QA:** when something is done and waiting for review or waiting to be tested, the issue/PR will be on this board;
- **Done:** when the issue is closed (merged), the issue/PR will be on this board;
**What happens when the release candidate is shipped?**
All changes in develop needs to be merged into the branch beta at this point. A new tag needs to be created following the pattern: “2.1.0-beta1”.
**What happens if there’s no bug/crash on the release candidate?**
That’s great, congrats! This time can be used in a creative way: write more tests, code maintenance that sometimes is required, resolving issues to the next release, planning, ideas and experiments.
**What happens when the release is done?**
Project and milestones are closed, all the changes are merged to the branch master and the tag is created, following the release’s pattern of the repository.
## Hotfix Releases
**When a hotfix release happen?**
Hotfix release will happen when a critical bug or crash is found in the production version of the app.
**How to handle hotfix releases?**
Simply open an issue on GitHub describing the issue, the issue is usually closed from a pull-request getting merged and a new milestone is created with the minor update, including all PRs required to the hotfix be completed. Once the milestone is completed, it can be closed and the release tag can be created.
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" /> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission <permission
......
...@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.login.presentation ...@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.OauthHelper import chat.rocket.android.helper.OauthHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
...@@ -190,64 +189,60 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -190,64 +189,60 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
private fun doAuthentication(loginType: Int) { private fun doAuthentication(loginType: Int) {
launchUI(strategy) { launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) { view.disableUserInput()
view.disableUserInput() view.showLoading()
view.showLoading() try {
try { val token = retryIO("login") {
val token = retryIO("login") { when (loginType) {
when (loginType) { TYPE_LOGIN_USER_EMAIL -> {
TYPE_LOGIN_USER_EMAIL -> { if (usernameOrEmail.isEmail()) {
if (usernameOrEmail.isEmail()) { client.loginWithEmail(usernameOrEmail, password)
client.loginWithEmail(usernameOrEmail, password) } else {
if (settings.isLdapAuthenticationEnabled()) {
client.loginWithLdap(usernameOrEmail, password)
} else { } else {
if (settings.isLdapAuthenticationEnabled()) { client.login(usernameOrEmail, password)
client.loginWithLdap(usernameOrEmail, password)
} else {
client.login(usernameOrEmail, password)
}
} }
} }
TYPE_LOGIN_CAS -> {
delay(3, TimeUnit.SECONDS)
client.loginWithCas(credentialToken)
}
TYPE_LOGIN_OAUTH -> {
client.loginWithOauth(credentialToken, credentialSecret)
}
else -> {
throw IllegalStateException("Expected TYPE_LOGIN_USER_EMAIL, TYPE_LOGIN_CAS or TYPE_LOGIN_OAUTH")
}
} }
} TYPE_LOGIN_CAS -> {
val username = retryIO("me()") { client.me().username } delay(3, TimeUnit.SECONDS)
if (username != null) { client.loginWithCas(credentialToken)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username) }
saveAccount(username) TYPE_LOGIN_OAUTH -> {
saveToken(token) client.loginWithOauth(credentialToken, credentialSecret)
registerPushToken()
navigator.toChatList()
} else if (loginType == TYPE_LOGIN_OAUTH) {
navigator.toRegisterUsername(token.userId, token.authToken)
}
} catch (exception: RocketChatException) {
when (exception) {
is RocketChatTwoFactorException -> {
navigator.toTwoFA(usernameOrEmail, password)
} }
else -> { else -> {
exception.message?.let { throw IllegalStateException("Expected TYPE_LOGIN_USER_EMAIL, TYPE_LOGIN_CAS or TYPE_LOGIN_OAUTH")
view.showMessage(it) }
}.ifNull { }
view.showGenericErrorMessage() }
} val username = retryIO("me()") { client.me().username }
if (username != null) {
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username)
saveAccount(username)
saveToken(token)
registerPushToken()
navigator.toChatList()
} else if (loginType == TYPE_LOGIN_OAUTH) {
navigator.toRegisterUsername(token.userId, token.authToken)
}
} catch (exception: RocketChatException) {
when (exception) {
is RocketChatTwoFactorException -> {
navigator.toTwoFA(usernameOrEmail, password)
}
else -> {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
} }
} }
} finally {
view.hideLoading()
view.enableUserInput()
} }
} else { } finally {
view.showNoInternetConnection() view.hideLoading()
view.enableUserInput()
} }
} }
} }
......
package chat.rocket.android.authentication.login.presentation package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.authentication.server.presentation.VersionCheckView import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
interface LoginView : LoadingView, MessageView, InternetView, VersionCheckView { interface LoginView : LoadingView, MessageView, VersionCheckView {
/** /**
* Shows the form view (i.e the username/email and password fields) if it is enabled by the server settings. * Shows the form view (i.e the username/email and password fields) if it is enabled by the server settings.
......
...@@ -105,10 +105,6 @@ class LoginFragment : Fragment(), LoginView { ...@@ -105,10 +105,6 @@ class LoginFragment : Fragment(), LoginView {
view_loading.setVisible(false) view_loading.setVisible(false)
} }
override fun showNoInternetConnection() {
showMessage(R.string.msg_no_internet_connection)
}
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
showToast(resId) showToast(resId)
} }
......
...@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.registerusername.presentation ...@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.registerusername.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
...@@ -40,30 +39,26 @@ class RegisterUsernamePresenter @Inject constructor( ...@@ -40,30 +39,26 @@ class RegisterUsernamePresenter @Inject constructor(
view.alertBlankUsername() view.alertBlankUsername()
} else { } else {
launchUI(strategy) { launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) { view.showLoading()
view.showLoading() try {
try { val me = retryIO("updateOwnBasicInformation(username = $username)") {
val me = retryIO("updateOwnBasicInformation(username = $username)") { client.updateOwnBasicInformation(username = username)
client.updateOwnBasicInformation(username = username)
}
val registeredUsername = me.username
if (registeredUsername != null) {
saveAccount(registeredUsername)
tokenRepository.save(currentServer, Token(userId, authToken))
registerPushToken()
navigator.toChatList()
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
} }
} else { val registeredUsername = me.username
view.showNoInternetConnection() if (registeredUsername != null) {
saveAccount(registeredUsername)
tokenRepository.save(currentServer, Token(userId, authToken))
registerPushToken()
navigator.toChatList()
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
} }
} }
} }
......
package chat.rocket.android.authentication.registerusername.presentation package chat.rocket.android.authentication.registerusername.presentation
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
interface RegisterUsernameView : LoadingView, MessageView, InternetView { interface RegisterUsernameView : LoadingView, MessageView {
/** /**
* Alerts the user about a blank username. * Alerts the user about a blank username.
......
...@@ -85,10 +85,6 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView { ...@@ -85,10 +85,6 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
showMessage(getString(R.string.msg_generic_error)) showMessage(getString(R.string.msg_generic_error))
} }
override fun showNoInternetConnection() {
showMessage(getString(R.string.msg_no_internet_connection))
}
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
activity?.apply { activity?.apply {
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this) val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
......
...@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.server.presentation ...@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.server.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.server.domain.GetAccountsInteractor import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor import chat.rocket.android.server.domain.SaveCurrentServerInteractor
...@@ -29,23 +28,19 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -29,23 +28,19 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
return@launchUI return@launchUI
} }
if (NetworkHelper.hasInternetAccess()) { view.showLoading()
view.showLoading() try {
try { refreshSettingsInteractor.refresh(server)
refreshSettingsInteractor.refresh(server) serverInteractor.save(server)
serverInteractor.save(server) navigator.toLogin()
navigator.toLogin() } catch (ex: Exception) {
} catch (ex: Exception) { ex.message?.let {
ex.message?.let { view.showMessage(it)
view.showMessage(it) }.ifNull {
}.ifNull { view.showGenericErrorMessage()
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
} }
} else { } finally {
view.showNoInternetConnection() view.hideLoading()
} }
} }
} }
......
package chat.rocket.android.authentication.server.presentation package chat.rocket.android.authentication.server.presentation
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
interface ServerView : LoadingView, MessageView, InternetView { interface ServerView : LoadingView, MessageView {
/** /**
* Shows an invalid server URL message. * Shows an invalid server URL message.
......
...@@ -68,10 +68,6 @@ class ServerFragment : Fragment(), ServerView { ...@@ -68,10 +68,6 @@ class ServerFragment : Fragment(), ServerView {
showMessage(getString(R.string.msg_generic_error)) showMessage(getString(R.string.msg_generic_error))
} }
override fun showNoInternetConnection() {
showMessage(getString(R.string.msg_no_internet_connection))
}
private fun enableUserInput(value: Boolean) { private fun enableUserInput(value: Boolean) {
button_connect.isEnabled = value button_connect.isEnabled = value
text_server_url.isEnabled = value text_server_url.isEnabled = value
......
...@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.signup.presentation ...@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.signup.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
...@@ -57,31 +56,26 @@ class SignupPresenter @Inject constructor(private val view: SignupView, ...@@ -57,31 +56,26 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
else -> { else -> {
val client = factory.create(server) val client = factory.create(server)
launchUI(strategy) { launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) { view.showLoading()
view.showLoading() try {
// TODO This function returns a user so should we save it?
try { retryIO("signup") { client.signup(email, name, username, password) }
// TODO This function returns a user so should we save it? // TODO This function returns a user token so should we save it?
retryIO("signup") { client.signup(email, name, username, password) } retryIO("login") { client.login(username, password) }
// TODO This function returns a user token so should we save it? val me = retryIO("me") { client.me() }
retryIO("login") { client.login(username, password) } localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
val me = retryIO("me") { client.me() } saveAccount(me)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username) registerPushToken()
saveAccount(me) navigator.toChatList()
registerPushToken() } catch (exception: RocketChatException) {
navigator.toChatList() exception.message?.let {
} catch (exception: RocketChatException) { view.showMessage(it)
exception.message?.let { }.ifNull {
view.showMessage(it) view.showGenericErrorMessage()
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
} }
} else { } finally {
view.showNoInternetConnection() view.hideLoading()
} }
} }
} }
......
package chat.rocket.android.authentication.signup.presentation package chat.rocket.android.authentication.signup.presentation
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
interface SignupView : LoadingView, MessageView, InternetView { interface SignupView : LoadingView, MessageView {
/** /**
* Alerts the user about a blank name. * Alerts the user about a blank name.
......
...@@ -109,10 +109,6 @@ class SignupFragment : Fragment(), SignupView { ...@@ -109,10 +109,6 @@ class SignupFragment : Fragment(), SignupView {
showMessage(getString(R.string.msg_generic_error)) showMessage(getString(R.string.msg_generic_error))
} }
override fun showNoInternetConnection() {
Toast.makeText(activity, getString(R.string.msg_no_internet_connection), Toast.LENGTH_SHORT).show()
}
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
activity?.apply { activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this) val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this)
......
...@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.twofactor.presentation ...@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.twofactor.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
...@@ -48,33 +47,29 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView, ...@@ -48,33 +47,29 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
else -> { else -> {
launchUI(strategy) { launchUI(strategy) {
val client = factory.create(server) val client = factory.create(server)
if (NetworkHelper.hasInternetAccess()) { view.showLoading()
view.showLoading() try {
try { // The token is saved via the client TokenProvider
// The token is saved via the client TokenProvider val token = retryIO("login") {
val token = retryIO("login") { client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
client.login(usernameOrEmail, password, twoFactorAuthenticationCode) }
} val me = retryIO("me") { client.me() }
val me = retryIO("me") { client.me() } saveAccount(me)
saveAccount(me) tokenRepository.save(server, token)
tokenRepository.save(server, token) registerPushToken()
registerPushToken() navigator.toChatList()
navigator.toChatList() } catch (exception: RocketChatException) {
} catch (exception: RocketChatException) { if (exception is RocketChatAuthException) {
if (exception is RocketChatAuthException) { view.alertInvalidTwoFactorAuthenticationCode()
view.alertInvalidTwoFactorAuthenticationCode() } else {
} else { exception.message?.let {
exception.message?.let { view.showMessage(it)
view.showMessage(it) }.ifNull {
}.ifNull { view.showGenericErrorMessage()
view.showGenericErrorMessage()
}
} }
} finally {
view.hideLoading()
} }
} else { } finally {
view.showNoInternetConnection() view.hideLoading()
} }
} }
} }
......
package chat.rocket.android.authentication.twofactor.presentation package chat.rocket.android.authentication.twofactor.presentation
import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
interface TwoFAView : LoadingView, MessageView, InternetView { interface TwoFAView : LoadingView, MessageView {
/** /**
* Alerts the user about a blank Two Factor Authentication code. * Alerts the user about a blank Two Factor Authentication code.
......
...@@ -91,8 +91,6 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -91,8 +91,6 @@ class TwoFAFragment : Fragment(), TwoFAView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection))
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
activity?.apply { activity?.apply {
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, this) val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, this)
......
package chat.rocket.android.chatroom.ui package chat.rocket.android.chatroom.ui
import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.support.annotation.DrawableRes import android.support.annotation.DrawableRes
import android.support.v4.app.ActivityCompat
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
...@@ -224,10 +228,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -224,10 +228,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private val layoutChangeListener = View.OnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom -> private val layoutChangeListener = View.OnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
val y = oldBottom - bottom val y = oldBottom - bottom
if (y.absoluteValue > 0 && isAdded) { if (Math.abs(y) > 0 && isAdded) {
// if y is positive the keyboard is up else it's down // if y is positive the keyboard is up else it's down
recycler_view.post { recycler_view.post {
if (y > 0 || verticalScrollOffset.get().absoluteValue >= y.absoluteValue) { if (y > 0 || Math.abs(verticalScrollOffset.get()) >= Math.abs(y)) {
recycler_view.scrollBy(0, y) recycler_view.scrollBy(0, y)
} else { } else {
recycler_view.scrollBy(0, verticalScrollOffset.get()) recycler_view.scrollBy(0, verticalScrollOffset.get())
...@@ -435,10 +439,31 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -435,10 +439,31 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
override fun showFileSelection(filter: Array<String>) { override fun showFileSelection(filter: Array<String>) {
val intent = Intent(Intent.ACTION_GET_CONTENT) activity?.let {
intent.type = "*/*" if (ContextCompat.checkSelfPermission(it, Manifest.permission.READ_EXTERNAL_STORAGE)
intent.putExtra(Intent.EXTRA_MIME_TYPES, filter) != PackageManager.PERMISSION_GRANTED) {
startActivityForResult(intent, REQUEST_CODE_FOR_PERFORM_SAF) ActivityCompat.requestPermissions(it,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
1)
} else {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_MIME_TYPES, filter)
startActivityForResult(intent, REQUEST_CODE_FOR_PERFORM_SAF)
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
1 -> {
if (!(grantResults.isNotEmpty() && grantResults.first() == PackageManager.PERMISSION_GRANTED)) {
handler.postDelayed({
hideAttachmentOptions()
}, 400)
}
}
}
} }
override fun showInvalidFileSize(fileSize: Int, maxFileSize: Int) { override fun showInvalidFileSize(fileSize: Int, maxFileSize: Int) {
......
...@@ -68,8 +68,8 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -68,8 +68,8 @@ class ViewModelMapper @Inject constructor(private val context: Context,
} }
mapMessage(message).let { mapMessage(message).let {
if (list.size > 0) { if (list.isNotEmpty()) {
it.preview = list[0].preview it.preview = list.first().preview
} }
list.add(it) list.add(it)
} }
...@@ -213,7 +213,7 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -213,7 +213,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
private suspend fun stripMessageQuotes(message: Message): Message { private suspend fun stripMessageQuotes(message: Message): Message {
val baseUrl = settings.baseUrl() val baseUrl = settings.baseUrl()
return message.copy( return message.copy(
message = message.message.replace("\\[\\s\\]\\($baseUrl.*\\)".toRegex(), "").trim() message = message.message.replace("\\[[^\\]]+\\]\\($baseUrl[^)]+\\)".toRegex(), "").trim()
) )
} }
......
package chat.rocket.android.core.behaviours
interface InternetView {
fun showNoInternetConnection()
}
\ No newline at end of file
package chat.rocket.android.helper
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Socket
object NetworkHelper {
/**
* Checks whether there is internet access.
*
* The original author of this code is Levit and you can see his answer here: https://stackoverflow.com/a/27312494/4744263
*
* @return true if there is internet access, false otherwise.
*/
suspend fun hasInternetAccess(): Boolean = withContext(CommonPool) {
try {
val socket = Socket()
val inetSocketAddress = InetSocketAddress("8.8.8.8", 53)
socket.connect(inetSocketAddress, 1500)
socket.close()
true
} catch (e: IOException) {
false
}
}
}
\ No newline at end of file
...@@ -9,10 +9,11 @@ import kotlinx.coroutines.experimental.Job ...@@ -9,10 +9,11 @@ import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
class ConnectionManager(internal val client: RocketChatClient) { class ConnectionManager(internal val client: RocketChatClient) {
private val statusChannelList = ArrayList<Channel<State>>() private val statusChannelList = CopyOnWriteArrayList<Channel<State>>()
private val statusChannel = Channel<State>() private val statusChannel = Channel<State>()
private var connectJob: Job? = null private var connectJob: Job? = null
......
...@@ -9,10 +9,7 @@ import android.provider.DocumentsContract ...@@ -9,10 +9,7 @@ import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import java.io.FileInputStream import java.io.*
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
fun Uri.getFileName(context: Context): String? { fun Uri.getFileName(context: Context): String? {
val cursor = context.contentResolver.query(this, null, null, null, null, null) val cursor = context.contentResolver.query(this, null, null, null, null, null)
...@@ -27,16 +24,22 @@ fun Uri.getFileName(context: Context): String? { ...@@ -27,16 +24,22 @@ fun Uri.getFileName(context: Context): String? {
} }
fun Uri.getFileSize(context: Context): Int { fun Uri.getFileSize(context: Context): Int {
val cursor = context.contentResolver.query(this, null, null, null, null, null) var fileSize: String? = null
if (scheme == ContentResolver.SCHEME_CONTENT) {
val fileSize = cursor?.use { try {
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) val fileInputStream = context.contentResolver.openInputStream(this)
if (cursor.moveToFirst()) { fileSize = fileInputStream.available().toString()
if (!cursor.isNull(sizeIndex)) { } catch (e: Exception) {
return@use cursor.getString(sizeIndex) e.printStackTrace()
} }
} else if (scheme == ContentResolver.SCHEME_FILE) {
val path = this.path
try {
val f = File(path)
fileSize = f.length().toString()
} catch (e: Exception) {
e.printStackTrace()
} }
return@use null
} }
return fileSize?.toIntOrNull() ?: -1 return fileSize?.toIntOrNull() ?: -1
} }
...@@ -46,7 +49,11 @@ fun Uri.getMimeType(context: Context): String { ...@@ -46,7 +49,11 @@ fun Uri.getMimeType(context: Context): String {
context.contentResolver.getType(this) context.contentResolver.getType(this)
} else { } else {
val fileExtension = MimeTypeMap.getFileExtensionFromUrl(toString()) val fileExtension = MimeTypeMap.getFileExtensionFromUrl(toString())
MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase()) if (fileExtension != null) {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase())
} else {
"application/octet-stream"
}
} }
} }
......
...@@ -203,11 +203,6 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -203,11 +203,6 @@ class SuggestionsView : FrameLayout, TextWatcher {
completionOffset.set(NO_STATE_INDEX) completionOffset.set(NO_STATE_INDEX)
} }
collapse() collapse()
// Re-enable keyboard suggestions.
val editText = editor?.get()
if (editText != null) {
editText.inputType = editText.inputType and InputType.TYPE_TEXT_VARIATION_FILTER.inv()
}
} }
private fun insertSuggestionOnEditor(item: SuggestionModel) { private fun insertSuggestionOnEditor(item: SuggestionModel) {
......
...@@ -2,22 +2,26 @@ ...@@ -2,22 +2,26 @@
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<android.support.design.widget.TextInputLayout <android.support.design.widget.TextInputLayout
android:id="@+id/layout_new_password" android:id="@+id/layout_new_password"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/EditText.Password" android:theme="@style/EditText.Password"
app:layout_constraintBottom_toTopOf="@id/middle_guide" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/start_guide" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/end_guide"> app:layout_constraintTop_toTopOf="parent"
android:layout_margin="16dp"
>
<EditText <EditText
android:id="@+id/text_new_password" android:id="@+id/text_new_password"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/msg_new_password" android:hint="@string/msg_new_password"
android:inputType="textPassword" /> android:inputType="textPassword"
/>
</android.support.design.widget.TextInputLayout> </android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout <android.support.design.widget.TextInputLayout
...@@ -25,9 +29,11 @@ ...@@ -25,9 +29,11 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="@style/EditText.Password" android:theme="@style/EditText.Password"
app:layout_constraintTop_toBottomOf="@id/middle_guide" app:layout_constraintTop_toBottomOf="@+id/layout_new_password"
app:layout_constraintStart_toEndOf="@id/start_guide" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/end_guide"> app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="16dp"
>
<EditText <EditText
android:id="@+id/text_confirm_password" android:id="@+id/text_confirm_password"
android:layout_width="match_parent" android:layout_width="match_parent"
...@@ -40,31 +46,12 @@ ...@@ -40,31 +46,12 @@
android:id="@+id/view_loading" android:id="@+id/view_loading"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:visibility="visible"
android:visibility="gone" android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/middle_guide"
app:layout_constraintStart_toEndOf="@id/start_guide"
app:layout_constraintEnd_toStartOf="@id/end_guide"
app:indicatorColor="@color/black" app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator" /> app:indicatorName="BallPulseIndicator"
app:layout_constraintStart_toStartOf="parent"
<android.support.constraint.Guideline app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/middle_guide" app:layout_constraintTop_toTopOf="parent"
android:layout_width="wrap_content" app:layout_constraintBottom_toBottomOf="parent"/>
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.2" />
<android.support.constraint.Guideline
android:id="@+id/start_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.05" />
<android.support.constraint.Guideline
android:id="@+id/end_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.95" />
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -10,45 +10,52 @@ ...@@ -10,45 +10,52 @@
android:id="@+id/text_online" android:id="@+id/text_online"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="10dp" android:paddingBottom="8dp"
android:layout_marginEnd="16dp" android:paddingEnd="16dp"
android:layout_marginStart="16dp" android:paddingStart="16dp"
android:layout_marginTop="16dp" android:paddingTop="8dp"
android:drawablePadding="10dp" android:drawablePadding="10dp"
android:drawableStart="@drawable/ic_status_online_24dp" android:drawableStart="@drawable/ic_status_online_24dp"
android:text="@string/action_online" /> android:text="@string/action_online"
android:background="?selectableItemBackground"/>
<TextView <TextView
android:id="@+id/text_away" android:id="@+id/text_away"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="10dp" android:paddingBottom="8dp"
android:layout_marginEnd="16dp" android:paddingEnd="16dp"
android:layout_marginStart="16dp" android:paddingStart="16dp"
android:paddingTop="8dp"
android:drawablePadding="10dp" android:drawablePadding="10dp"
android:drawableStart="@drawable/ic_status_away_24dp" android:drawableStart="@drawable/ic_status_away_24dp"
android:text="@string/action_away" /> android:text="@string/action_away"
android:background="?selectableItemBackground"/>
<TextView <TextView
android:id="@+id/text_busy" android:id="@+id/text_busy"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="10dp" android:paddingBottom="8dp"
android:layout_marginEnd="16dp" android:paddingEnd="16dp"
android:layout_marginStart="16dp" android:paddingStart="16dp"
android:paddingTop="8dp"
android:drawablePadding="10dp" android:drawablePadding="10dp"
android:drawableStart="@drawable/ic_status_busy_24dp" android:drawableStart="@drawable/ic_status_busy_24dp"
android:text="@string/action_busy" /> android:text="@string/action_busy"
android:background="?selectableItemBackground"/>
<TextView <TextView
android:id="@+id/text_invisible" android:id="@+id/text_invisible"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" android:paddingBottom="8dp"
android:layout_marginEnd="16dp" android:paddingEnd="16dp"
android:layout_marginStart="16dp" android:paddingStart="16dp"
android:paddingTop="8dp"
android:drawablePadding="10dp" android:drawablePadding="10dp"
android:drawableStart="@drawable/ic_status_invisible_24dp" android:drawableStart="@drawable/ic_status_invisible_24dp"
android:text="@string/action_invisible" /> android:text="@string/action_invisible"
android:background="?selectableItemBackground"/>
</LinearLayout> </LinearLayout>
\ No newline at end of file
...@@ -41,7 +41,6 @@ ...@@ -41,7 +41,6 @@
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_no_internet_connection">कोई इंटरनेट कनेक्शन नहीं है</string>
<string name="msg_generic_error">क्षमा करें, एक त्रुटि हुई है, कृपया पुनः प्रयास करें</string> <string name="msg_generic_error">क्षमा करें, एक त्रुटि हुई है, कृपया पुनः प्रयास करें</string>
<string name="msg_no_data_to_display">डेटा प्रदर्शित करने के लिए उपलब्ध नहीं हैं</string> <string name="msg_no_data_to_display">डेटा प्रदर्शित करने के लिए उपलब्ध नहीं हैं</string>
<string name="msg_profile_update_successfully">प्रोफ़ाइल सफलतापूर्वक अपडेट हो गया है</string> <string name="msg_profile_update_successfully">प्रोफ़ाइल सफलतापूर्वक अपडेट हो गया है</string>
......
...@@ -41,7 +41,6 @@ ...@@ -41,7 +41,6 @@
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_no_internet_connection">Sem conexão à internet</string>
<string name="msg_generic_error">Desculpe, ocorreu um erro, tente novamente</string> <string name="msg_generic_error">Desculpe, ocorreu um erro, tente novamente</string>
<string name="msg_no_data_to_display">Nenhum dado para exibir</string> <string name="msg_no_data_to_display">Nenhum dado para exibir</string>
<string name="msg_profile_update_successfully">Perfil atualizado com sucesso</string> <string name="msg_profile_update_successfully">Perfil atualizado com sucesso</string>
......
...@@ -42,7 +42,6 @@ ...@@ -42,7 +42,6 @@
</string-array> </string-array>
<!-- Regular information messages --> <!-- Regular information messages -->
<string name="msg_no_internet_connection">No internet connection</string>
<string name="msg_generic_error">Sorry, an error has occurred, please try again</string> <string name="msg_generic_error">Sorry, an error has occurred, please try again</string>
<string name="msg_no_data_to_display">No data to display</string> <string name="msg_no_data_to_display">No data to display</string>
<string name="msg_profile_update_successfully">Profile update successfully</string> <string name="msg_profile_update_successfully">Profile update successfully</string>
......
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