Commit 3f67f9c2 authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Merge branch 'develop-2.x' of...

Merge branch 'develop-2.x' of https://github.com/RocketChat/Rocket.Chat.Android into feature/document-upload
parents 89f38952 03387f6c
# Rocket.Chat Android native application ![Rocket.Chat logo](https://raw.githubusercontent.com/RocketChat/Rocket.Chat.Artwork/master/Logos/logo-dark.svg?sanitize=true)
[![CircleCI](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop.svg?style=shield)](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop) [![Build Status](https://travis-ci.org/RocketChat/Rocket.Chat.Android.svg?branch=develop)](https://travis-ci.org/RocketChat/Rocket.Chat.Android) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a81156a8682e4649994270d3670c3c83)](https://www.codacy.com/app/matheusjardimb/Rocket.Chat.Android) # Rocket.Chat Android native application
# Get it from Google Play [![CircleCI](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop.svg?style=shield)](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop) [![Build Status](https://travis-ci.org/RocketChat/Rocket.Chat.Android.svg?branch=develop)](https://travis-ci.org/RocketChat/Rocket.Chat.Android) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a81156a8682e4649994270d3670c3c83)](https://www.codacy.com/app/matheusjardimb/Rocket.Chat.Android)
[![Rocket.Chat on Google Play](https://user-images.githubusercontent.com/551004/29770692-a20975c6-8bc6-11e7-8ab0-1cde275496e0.png)](https://play.google.com/store/apps/details?id=chat.rocket.android) ## Description
Currently, the app is maintained in two branches, namely `v1+` and `v2+`. The `v1+` is maintained in the `develop` branch and the `v2+` is maintained in the `develop-2.x` branch. The older version is written partially in `java` and `kotlin`, but we intend to write the latest version completely in `kotlin`.
Clone the repository by running `git clone https://github.com/RocketChat/Rocket.Chat.Android.git` in your terminal. To build the v1.0+ of the app, run `git checkout develop` and to build the v2.0+, run `git checkout develop-2.x`.
Since both the versions use `kotlin` for some or all of their classes, following are the common prerequisites for both versions:
## How to build ## How to build
- Android Studio 3.0+ comes with built in kotlin support, so install the latest version (3.0+) of Android Studio (recommended). For older versions, you need to manually install kotlin plugin. Go to `File > Settings > Plugins` and search for `kotlin` and install it. You'll need to restart the IDE in order to see the changes.
Retrolambda needs java8 to be installed on your system - Make sure that you have the latest **gradle** and the **android plugin** versions installed. Go to `File > Project Structure > Project` and make sure that you have the latest versions installed. Refer [this](https://developer.android.com/studio/releases/gradle-plugin.html#updating-gradle) to see the compatible versions.
- Kotlin is already configured in the project. To check, go to `Tools > Kotlin > Configure Kotlin in project`. A message saying kotlin is already configured in the project pops up. You can update kotlin to the latest version by going to `Tools > Kotlin > Configure Kotlin updates` and download the latest version of kotlin.
### Instructions specific to version
#### v1.0+
- After checking out to `develop` branch as mentioned above, simply import the project in Android Studio.
#### v2+
- This version requires the [Kotlin SDK](https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK) for Rocket.Chat. Clone the Kotlin SDK in the **same directory** as the android repository by running `git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git`. Make sure that the android repository and the kotlin sdk have the same immediate parent directory.
- First, a build is required for the SDK. Change your directory to the SDK directory by running `cd Rocket.Chat.Kotlin.SDK/` in your terminal. Any of the following approaches can be followed to successfully build the SDK.
- **Command Line** - (Within the kotlin SDK directory) Run `./gradlew clean && ./gradlew assemble` to successfully build the project.
- **Android Studio** - Import the project in Android Studio. Go to `Build > Make Project` to build the SDK successfully.
After following the above methods, follow the following steps in your terminal window:
``` ```
export ANDROID_HOME=/path/to/android/sdk cd ..
cd Rocket.Chat.Android/app/libs
git clone https://github.com/RocketChat/Rocket.Chat.Android.git ls
cd Rocket.Chat.Android
echo "sdk.dir="$ANDROID_HOME > local.properties
./gradlew assembleDebug
``` ```
Two `jar` files will be found in this directory (the `common` and `core` jar files), this indicates that the SDK was built correctly.
- After the SDK has been built successfully, import the project in Android Studio and build it by following `Build > Make Project`.
## How to run
### Command Line
- Connect your physical device to your pc via USB or start an emulator. Run `adb devices` in terminal. You should see your device in the list of devices.
- In order to build the debug apk, run `./gradlew assembleDebug`. This would generate a debug apk which can be found under `Rocket.Chat.Android/app/build/outputs/apk/debug` folder with the name `app-debug.apk`.
- In order to build and install the apk directly to the connected device, run `./gradlew installDebug`.
### Android Studio
- After importing the project in android studio, go to `Run > Run app` and then select your device, or create a new virtual device by following the wizard.
## Bug report & Feature request ## Bug report & Feature request
...@@ -27,4 +47,4 @@ Please report via [GitHub issue](https://github.com/RocketChat/Rocket.Chat.Andro ...@@ -27,4 +47,4 @@ Please report via [GitHub issue](https://github.com/RocketChat/Rocket.Chat.Andro
## Coding Style ## Coding Style
Please follow our [coding style](https://github.com/RocketChat/Rocket.Chat.Android/blob/develop/CODING_STYLE.md) when contributing. Please follow our [coding style](https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md) when contributing.
...@@ -99,8 +99,14 @@ dependencies { ...@@ -99,8 +99,14 @@ dependencies {
implementation libraries.textDrawable implementation libraries.textDrawable
implementation libraries.markwon
implementation libraries.markwonImageLoader
implementation libraries.moshiLazyAdapters implementation libraries.moshiLazyAdapters
implementation libraries.sheetMenu
implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') { implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
transitive = true transitive = true
} }
......
package chat.rocket.android;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("chat.rocket.android", appContext.getPackageName());
}
}
...@@ -36,17 +36,21 @@ ...@@ -36,17 +36,21 @@
</activity> </activity>
<activity <activity
android:name=".webview.WebViewActivity" android:name=".main.ui.MainActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity <activity
android:name=".main.ui.MainActivity" android:name=".webview.WebViewActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity <activity
android:name=".chatroom.ui.ChatRoomActivity" android:name=".chatroom.ui.ChatRoomActivity"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity
android:name=".chatroom.ui.PinnedMessagesActivity"
android:theme="@style/AppTheme" />
<receiver <receiver
android:name="com.google.android.gms.gcm.GcmReceiver" android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true" android:exported="true"
...@@ -78,7 +82,6 @@ ...@@ -78,7 +82,6 @@
<meta-data <meta-data
android:name="io.fabric.ApiKey" android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" /> android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
</application> </application>
</manifest> </manifest>
\ No newline at end of file
...@@ -7,12 +7,13 @@ import chat.rocket.android.helper.NetworkHelper ...@@ -7,12 +7,13 @@ 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.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatTwoFactorException import chat.rocket.common.RocketChatTwoFactorException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.registerPushToken
import javax.inject.Inject import javax.inject.Inject
...@@ -93,7 +94,12 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -93,7 +94,12 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
try { try {
val token = client.login(usernameOrEmail, password) val token = client.login(usernameOrEmail, password)
multiServerRepository.save(server, TokenModel(token.userId, token.authToken)) val me = client.me()
multiServerRepository.save(
server,
TokenModel(token.userId, token.authToken)
)
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
...@@ -105,8 +111,8 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -105,8 +111,8 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
exception.message?.let { exception.message?.let {
view.showMessage(it) view.showMessage(it)
}.ifNull { }.ifNull {
view.showGenericErrorMessage() view.showGenericErrorMessage()
} }
} }
} }
} finally { } finally {
......
...@@ -6,10 +6,12 @@ import android.os.Build ...@@ -6,10 +6,12 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.view.* import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.login.presentation.LoginPresenter import chat.rocket.android.authentication.login.presentation.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView import chat.rocket.android.authentication.login.presentation.LoginView
...@@ -18,6 +20,7 @@ import chat.rocket.android.helper.KeyboardHelper ...@@ -18,6 +20,7 @@ import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible 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.textContent
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.* import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
...@@ -167,8 +170,9 @@ class LoginFragment : Fragment(), LoginView { ...@@ -167,8 +170,9 @@ class LoginFragment : Fragment(), LoginView {
enableUserInput(true) enableUserInput(true)
} }
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() 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 showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
...@@ -216,7 +220,7 @@ class LoginFragment : Fragment(), LoginView { ...@@ -216,7 +220,7 @@ class LoginFragment : Fragment(), LoginView {
} }
// Returns true if *all* EditTexts are empty. // Returns true if *all* EditTexts are empty.
private fun isEditTextEmpty(): Boolean = text_username_or_email.textContent.isBlank() && text_password.textContent.isEmpty() private fun isEditTextEmpty(): Boolean = text_username_or_email.textContent.isBlank() && text_password.textContent.isEmpty()
private fun showRemainingSocialAccountsView() { private fun showRemainingSocialAccountsView() {
social_accounts_container.postDelayed({ social_accounts_container.postDelayed({
...@@ -232,4 +236,4 @@ class LoginFragment : Fragment(), LoginView { ...@@ -232,4 +236,4 @@ class LoginFragment : Fragment(), LoginView {
scroll_view.fullScroll(ScrollView.FOCUS_DOWN) scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}, 1250) }, 1250)
} }
} }
\ No newline at end of file
...@@ -8,7 +8,7 @@ import chat.rocket.android.authentication.signup.ui.SignupFragment ...@@ -8,7 +8,7 @@ import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.util.addFragmentBackStack import chat.rocket.android.util.extensions.addFragmentBackStack
import chat.rocket.android.webview.webViewIntent import chat.rocket.android.webview.webViewIntent
class AuthenticationNavigator(internal val activity: AuthenticationActivity, internal val context: Context) { class AuthenticationNavigator(internal val activity: AuthenticationActivity, internal val context: Context) {
......
...@@ -6,7 +6,7 @@ import chat.rocket.android.helper.NetworkHelper ...@@ -6,7 +6,7 @@ import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.settings import chat.rocket.core.internal.rest.settings
...@@ -23,7 +23,8 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -23,7 +23,8 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private var settingsFilter = arrayOf(SITE_URL, SITE_NAME, FAVICON_512, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS, FAVORITE_ROOMS, private var settingsFilter = arrayOf(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_LOGIN_FORM, ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB, ACCOUNT_GITLAB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR,
ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, LDAP_ENABLE, ACCOUNT_REGISTRATION, STORAGE_TYPE, HIDE_USER_JOIN, HIDE_USER_LEAVE, HIDE_TYPE_AU, ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, LDAP_ENABLE, ACCOUNT_REGISTRATION, STORAGE_TYPE, HIDE_USER_JOIN, HIDE_USER_LEAVE, HIDE_TYPE_AU,
HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ACCOUNT_CUSTOM_FIELDS) HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ACCOUNT_CUSTOM_FIELDS, ALLOW_MESSAGE_DELETING, ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING,
SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
fun connect(server: String) { fun connect(server: String) {
if (!UrlHelper.isValidUrl(server)) { if (!UrlHelper.isValidUrl(server)) {
...@@ -55,8 +56,8 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -55,8 +56,8 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
exception.message?.let { exception.message?.let {
view.showMessage(it) view.showMessage(it)
}.ifNull { }.ifNull {
view.showGenericErrorMessage() view.showGenericErrorMessage()
} }
} finally { } finally {
view.hideLoading() view.hideLoading()
} }
......
...@@ -6,7 +6,6 @@ import android.view.LayoutInflater ...@@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.server.presentation.ServerPresenter import chat.rocket.android.authentication.server.presentation.ServerPresenter
import chat.rocket.android.authentication.server.presentation.ServerView import chat.rocket.android.authentication.server.presentation.ServerView
...@@ -56,7 +55,9 @@ class ServerFragment : Fragment(), ServerView { ...@@ -56,7 +55,9 @@ class ServerFragment : Fragment(), ServerView {
enableUserInput(true) enableUserInput(true)
} }
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() 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 showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
...@@ -7,11 +7,12 @@ import chat.rocket.android.helper.UrlHelper ...@@ -7,11 +7,12 @@ import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.internal.rest.signup import chat.rocket.core.internal.rest.signup
import javax.inject.Inject import javax.inject.Inject
...@@ -49,16 +50,26 @@ class SignupPresenter @Inject constructor(private val view: SignupView, ...@@ -49,16 +50,26 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
view.showLoading() view.showLoading()
try { try {
client.signup(email, name, username, password) // TODO This function returns a user so should we save it? client.signup(
client.login(username, password) // TODO This function returns a user token so should we save it? email,
name,
username,
password
) // TODO This function returns a user so should we save it?
client.login(
username,
password
) // TODO This function returns a user token so should we save it?
val me = client.me()
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
exception.message?.let { exception.message?.let {
view.showMessage(it) view.showMessage(it)
}.ifNull { }.ifNull {
view.showGenericErrorMessage() view.showGenericErrorMessage()
} }
} finally { } finally {
view.hideLoading() view.hideLoading()
......
...@@ -16,6 +16,7 @@ import chat.rocket.android.helper.KeyboardHelper ...@@ -16,6 +16,7 @@ import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.showToast
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.* import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
import javax.inject.Inject import javax.inject.Inject
...@@ -100,9 +101,9 @@ class SignupFragment : Fragment(), SignupView { ...@@ -100,9 +101,9 @@ class SignupFragment : Fragment(), SignupView {
enableUserInput(true) enableUserInput(true)
} }
override fun showMessage(message: String) { override fun showMessage(resId: Int) = showToast(resId)
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
} override fun showMessage(message: String) = showToast(message)
override fun showGenericErrorMessage() { override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error)) showMessage(getString(R.string.msg_generic_error))
......
...@@ -8,7 +8,7 @@ import chat.rocket.android.infrastructure.LocalRepository ...@@ -8,7 +8,7 @@ import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatAuthException import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
...@@ -43,8 +43,12 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView, ...@@ -43,8 +43,12 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
view.showLoading() view.showLoading()
try { try {
// The token is saved via the client TokenProvider // The token is saved via the client TokenProvider
val token = client.login(usernameOrEmail, password, twoFactorAuthenticationCode) val token =
multiServerRepository.save(server, TokenModel(token.userId, token.authToken)) client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
multiServerRepository.save(
server,
TokenModel(token.userId, token.authToken)
)
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
...@@ -54,8 +58,8 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView, ...@@ -54,8 +58,8 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
exception.message?.let { exception.message?.let {
view.showMessage(it) view.showMessage(it)
}.ifNull { }.ifNull {
view.showGenericErrorMessage() view.showGenericErrorMessage()
} }
} }
} finally { } finally {
view.hideLoading() view.hideLoading()
......
...@@ -9,13 +9,13 @@ import android.view.LayoutInflater ...@@ -9,13 +9,13 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter
import chat.rocket.android.authentication.twofactor.presentation.TwoFAView import chat.rocket.android.authentication.twofactor.presentation.TwoFAView
import chat.rocket.android.helper.AnimationHelper import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.showToast
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_two_fa.* import kotlinx.android.synthetic.main.fragment_authentication_two_fa.*
import javax.inject.Inject import javax.inject.Inject
...@@ -83,7 +83,9 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -83,7 +83,9 @@ class TwoFAFragment : Fragment(), TwoFAView {
enableUserInput(true) enableUserInput(true)
} }
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() 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 showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
...@@ -108,4 +110,4 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -108,4 +110,4 @@ class TwoFAFragment : Fragment(), TwoFAView {
presenter.authenticate(username, password, text_two_factor_auth.textContent) presenter.authenticate(username, password, text_two_factor_auth.textContent)
} }
} }
} }
\ No newline at end of file
...@@ -6,7 +6,7 @@ import android.support.v7.app.AppCompatActivity ...@@ -6,7 +6,7 @@ import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.presentation.AuthenticationPresenter import chat.rocket.android.authentication.presentation.AuthenticationPresenter
import chat.rocket.android.authentication.server.ui.ServerFragment import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.util.addFragment import chat.rocket.android.util.extensions.addFragment
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
......
package chat.rocket.android.chatroom.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.PinnedMessagesView
import chat.rocket.android.chatroom.ui.PinnedMessagesFragment
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
class PinnedMessagesFragmentModule {
@Provides
fun provideLifecycleOwner(frag: PinnedMessagesFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
@Provides
fun providePinnedMessageView(frag: PinnedMessagesFragment): PinnedMessagesView {
return frag
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.di
import chat.rocket.android.chatroom.ui.PinnedMessagesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class PinnedMessagesFragmentProvider {
@ContributesAndroidInjector(modules = [PinnedMessagesFragmentModule::class])
abstract fun providePinnedMessageFragment(): PinnedMessagesFragment
}
\ No newline at end of file
...@@ -35,6 +35,13 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -35,6 +35,13 @@ interface ChatRoomView : LoadingView, MessageView {
*/ */
fun showNewMessage(message: MessageViewModel) fun showNewMessage(message: MessageViewModel)
/**
* Dispatch to the recycler views adapter that we should remove a message.
*
* @param msgId The id of the message to be removed.
*/
fun dispatchDeleteMessage(msgId: String)
/** /**
* Dispatch a update to the recycler views adapter about a changed message. * Dispatch a update to the recycler views adapter about a changed message.
* *
...@@ -42,7 +49,27 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -42,7 +49,27 @@ interface ChatRoomView : LoadingView, MessageView {
*/ */
fun dispatchUpdateMessage(index: Int, message: MessageViewModel) fun dispatchUpdateMessage(index: Int, message: MessageViewModel)
fun disableMessageInput() /**
* Show reply status above the message composer.
*
* @param username The username or name of the user to reply/quote to.
* @param replyMarkdown The markdown of the message reply.
* @param quotedMessage The message to quote.
*/
fun showReplyingAction(username: String, replyMarkdown: String, quotedMessage: String)
/**
* Copy message to clipboard.
*
* @param message The message to copy.
*/
fun copyToClipboard(message: String)
/**
* Show edit status above the message composer.
*/
fun showEditingAction(roomId: String, messageId: String, text: String)
fun disableMessageInput()
fun enableMessageInput(clear: Boolean = false) fun enableMessageInput(clear: Boolean = false)
} }
\ No newline at end of file
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getRoomPinnedMessages
import chat.rocket.core.model.Value
import timber.log.Timber
import javax.inject.Inject
class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessagesView,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor,
private val mapper: MessageViewModelMapper,
factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor) {
private val client = factory.create(serverInteractor.get()!!)
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!!
private var pinnedMessagesListOffset: Int = 0
/**
* Load all pinned messages for the given room id.
*
* @param roomId The id of the room to get pinned messages from.
*/
fun loadPinnedMessages(roomId: String) {
launchUI(strategy) {
try {
val serverUrl = serverInteractor.get()!!
val chatRoom = roomsInteractor.getById(serverUrl, roomId)
chatRoom?.let { room ->
view.showLoading()
val pinnedMessages =
client.getRoomPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
pinnedMessagesListOffset = pinnedMessages.offset.toInt()
val messageList = mapper.mapToViewModelList(pinnedMessages.result, settings)
.filterNot { it.isSystemMessage }
view.showPinnedMessages(messageList)
view.hideLoading()
}.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server.")
}
} catch (e: RocketChatException) {
Timber.e(e)
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.presentation
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface PinnedMessagesView : MessageView, LoadingView {
/**
* Show list of pinned messages for the current room.
*
* @param pinnedMessages The list of pinned messages.
*/
fun showPinnedMessages(pinnedMessages: List<MessageViewModel>)
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import android.graphics.drawable.Drawable
import android.support.design.widget.BaseTransientBottomBar
import android.support.v4.view.ViewCompat
import android.text.Spannable
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.helper.MessageParser
import chat.rocket.android.util.extensions.content
import ru.noties.markwon.Markwon
class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
companion object {
fun make(parentViewGroup: ViewGroup, content: String = "", parser: MessageParser): ActionSnackbar {
val context = parentViewGroup.context
val view = LayoutInflater.from(context).inflate(R.layout.message_action_bar, parentViewGroup, false)
val actionSnackbar = ActionSnackbar(parentViewGroup, view, CallbackImpl(view))
actionSnackbar.parser = parser
actionSnackbar.messageTextView = view.findViewById(R.id.text_view_action_text) as TextView
actionSnackbar.titleTextView = view.findViewById(R.id.text_view_action_title) as TextView
actionSnackbar.cancelView = view.findViewById(R.id.image_view_action_cancel_quote) as ImageView
actionSnackbar.duration = BaseTransientBottomBar.LENGTH_INDEFINITE
val spannable = Markwon.markdown(context, content).trim()
actionSnackbar.marginDrawable = context.getDrawable(R.drawable.quote)
actionSnackbar.messageTextView.content = spannable
return actionSnackbar
}
}
lateinit var parser: MessageParser
lateinit var cancelView: View
private lateinit var messageTextView: TextView
private lateinit var titleTextView: TextView
private lateinit var marginDrawable: Drawable
var text: String = ""
set(value) {
val spannable = parser.renderMarkdown(value) as Spannable
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
messageTextView.content = spannable
}
var title: String = ""
set(value) {
val spannable = Markwon.markdown(this.context, value) as Spannable
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
titleTextView.content = spannable
}
override fun dismiss() {
super.dismiss()
text = ""
title = ""
}
private constructor(parentViewGroup: ViewGroup, content: View, contentViewCallback: BaseTransientBottomBar.ContentViewCallback) :
super(parentViewGroup, content, contentViewCallback)
class CallbackImpl(val content: View) : BaseTransientBottomBar.ContentViewCallback {
override fun animateContentOut(delay: Int, duration: Int) {
ViewCompat.setScaleY(content, 1f)
ViewCompat.animate(content)
.scaleY(0f)
.setDuration(duration.toLong())
.startDelay = delay.toLong()
}
override fun animateContentIn(delay: Int, duration: Int) {
ViewCompat.setScaleY(content, 0f)
ViewCompat.animate(content)
.scaleY(1f)
.setDuration(duration.toLong())
.startDelay = delay.toLong()
}
}
}
\ No newline at end of file
...@@ -6,7 +6,7 @@ import android.os.Bundle ...@@ -6,7 +6,7 @@ import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.util.addFragment import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
...@@ -15,6 +15,7 @@ import dagger.android.support.HasSupportFragmentInjector ...@@ -15,6 +15,7 @@ import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.* import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject import javax.inject.Inject
fun Context.chatRoomIntent(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Intent { fun Context.chatRoomIntent(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Intent {
return Intent(this, ChatRoomActivity::class.java).apply { return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId) putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
...@@ -67,6 +68,8 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -67,6 +68,8 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
} }
private fun setupToolbar(chatRoomName: String) { private fun setupToolbar(chatRoomName: String) {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName text_room_name.textContent = chatRoomName
toolbar.setNavigationOnClickListener { toolbar.setNavigationOnClickListener {
finishActivity() finishActivity()
......
package chat.rocket.android.chatroom.ui package chat.rocket.android.chatroom.ui
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.text.method.LinkMovementMethod
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
import chat.rocket.android.chatroom.viewmodel.AttachmentType import chat.rocket.android.chatroom.viewmodel.AttachmentType
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.player.PlayerActivity import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.content
import com.facebook.drawee.view.SimpleDraweeView import com.facebook.drawee.view.SimpleDraweeView
import com.stfalcon.frescoimageviewer.ImageViewer import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.avatar.view.* import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.* import kotlinx.android.synthetic.main.item_message.view.*
import kotlinx.android.synthetic.main.message_attachment.view.* import kotlinx.android.synthetic.main.message_attachment.view.*
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
class ChatRoomAdapter: RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() { class ChatRoomAdapter(private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter) : RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
private val dataSet = ArrayList<MessageViewModel>() private val dataSet = ArrayList<MessageViewModel>()
init { init {
...@@ -24,19 +34,16 @@ class ChatRoomAdapter: RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() { ...@@ -24,19 +34,16 @@ class ChatRoomAdapter: RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(parent.inflate(R.layout.item_message)) ViewHolder(parent.inflate(R.layout.item_message), roomType, roomName, presenter)
override fun onBindViewHolder(holder: ViewHolder, position: Int) = override fun onBindViewHolder(holder: ViewHolder, position: Int) =
holder.bind(dataSet[position], dataSet.getOrNull(position + 1)) holder.bind(dataSet[position], dataSet.getOrNull(position + 1))
override fun getItemCount(): Int = override fun getItemCount(): Int = dataSet.size
dataSet.size
override fun getItemViewType(position: Int): Int = override fun getItemViewType(position: Int): Int = position
position
override fun getItemId(position: Int): Long = override fun getItemId(position: Int): Long = dataSet[position].id.hashCode().toLong()
dataSet[position].id.hashCode().toLong()
fun addDataSet(dataSet: List<MessageViewModel>) { fun addDataSet(dataSet: List<MessageViewModel>) {
val previousDataSetSize = this.dataSet.size val previousDataSetSize = this.dataSet.size
...@@ -49,17 +56,34 @@ class ChatRoomAdapter: RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() { ...@@ -49,17 +56,34 @@ class ChatRoomAdapter: RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
notifyItemInserted(0) notifyItemInserted(0)
} }
fun updateItem(index: Int, message: MessageViewModel) { fun updateItem(message: MessageViewModel) {
dataSet[index] = message val index = dataSet.indexOfFirst { it.id == message.id }
notifyItemChanged(index) if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
} }
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.id == messageId }
if (index > -1) {
dataSet.removeAt(index)
notifyItemRemoved(index)
}
}
class ViewHolder(itemView: View,
val roomType: String,
val roomName: String,
val presenter: ChatRoomPresenter) : RecyclerView.ViewHolder(itemView), MenuItem.OnMenuItemClickListener {
private lateinit var messageViewModel: MessageViewModel
fun bind(message: MessageViewModel, nextMessage: MessageViewModel?) = with(itemView) { fun bind(message: MessageViewModel, nextMessage: MessageViewModel?) = with(itemView) {
messageViewModel = message
image_avatar.setImageURI(message.avatarUri) image_avatar.setImageURI(message.avatarUri)
text_sender.text = message.senderName text_sender.text = message.senderName
text_message_time.text = message.time text_message_time.content = message.time
if (nextMessage != null) { if (nextMessage != null) {
if (isSequential(message, nextMessage)) { if (isSequential(message, nextMessage)) {
...@@ -69,8 +93,47 @@ class ChatRoomAdapter: RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() { ...@@ -69,8 +93,47 @@ class ChatRoomAdapter: RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
} }
} }
text_content.text = message.content text_content.content = message.content
text_content.movementMethod = LinkMovementMethod()
bindAttachment(message, message_attachment, image_attachment, audio_video_attachment, file_name) bindAttachment(message, message_attachment, image_attachment, audio_video_attachment, file_name)
text_content.setOnClickListener {
if (!message.isSystemMessage) {
val menuItems = it.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = message.isPinned
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
setChecked(isPinned)
}
val adapter = ActionListAdapter(menuItems, this@ViewHolder)
BottomSheetMenu(adapter).apply {
}.show(it.context)
}
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {
messageViewModel.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter.citeMessage(roomType, roomName, id, false)
R.id.action_menu_msg_reply -> presenter.citeMessage(roomType, roomName, id, true)
R.id.action_menu_msg_copy -> presenter.copyMessage(id)
R.id.action_menu_msg_edit -> presenter.editMessage(roomId, id, getOriginalMessage())
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
presenter.pinMessage(id)
} else {
presenter.unpinMessage(id)
}
}
}
else -> TODO("Not implemented")
}
}
return true
} }
private fun isSequential(message: MessageViewModel, nextMessage: MessageViewModel): Boolean = private fun isSequential(message: MessageViewModel, nextMessage: MessageViewModel): Boolean =
......
...@@ -3,22 +3,27 @@ package chat.rocket.android.chatroom.ui ...@@ -3,22 +3,27 @@ package chat.rocket.android.chatroom.ui
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater import android.view.*
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.helper.MessageParser
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 dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_room.* import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.* import kotlinx.android.synthetic.main.message_attachment_options.*
...@@ -26,6 +31,7 @@ import kotlinx.android.synthetic.main.message_composer.* ...@@ -26,6 +31,7 @@ import kotlinx.android.synthetic.main.message_composer.*
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Fragment { fun newInstance(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Fragment {
return ChatRoomFragment().apply { return ChatRoomFragment().apply {
arguments = Bundle(1).apply { arguments = Bundle(1).apply {
...@@ -45,11 +51,18 @@ private const val REQUEST_CODE_FOR_PERFORM_SAF = 42 ...@@ -45,11 +51,18 @@ private const val REQUEST_CODE_FOR_PERFORM_SAF = 42
class ChatRoomFragment : Fragment(), ChatRoomView { class ChatRoomFragment : Fragment(), ChatRoomView {
@Inject lateinit var presenter: ChatRoomPresenter @Inject lateinit var presenter: ChatRoomPresenter
@Inject lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
private lateinit var chatRoomType: String private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false private var isChatRoomReadOnly: Boolean = false
private lateinit var adapter: ChatRoomAdapter
private lateinit var actionSnackbar: ActionSnackbar
private var citation: String? = null
private var editingMessageId: String? = null
// For reveal and unreveal anim. // For reveal and unreveal anim.
private val hypotenuse by lazy { Math.hypot(relative_layout.width.toDouble(), relative_layout.height.toDouble()).toFloat() } private val hypotenuse by lazy { Math.hypot(relative_layout.width.toDouble(), relative_layout.height.toDouble()).toFloat() }
private val max by lazy { Math.max(layout_message_attachment_options.width.toDouble(), layout_message_attachment_options.height.toDouble()).toFloat() } private val max by lazy { Math.max(layout_message_attachment_options.width.toDouble(), layout_message_attachment_options.height.toDouble()).toFloat() }
...@@ -70,6 +83,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -70,6 +83,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
} else { } else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" } requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
} }
setHasOptionsMenu(true)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_chat_room) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_chat_room)
...@@ -79,6 +93,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -79,6 +93,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
presenter.loadMessages(chatRoomId, chatRoomType) presenter.loadMessages(chatRoomId, chatRoomType)
handler = Handler() handler = Handler()
setupComposer() setupComposer()
setupActionSnackbar()
} }
override fun onDestroyView() { override fun onDestroyView() {
...@@ -94,10 +109,29 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -94,10 +109,29 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
} }
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.chatroom_actions, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_pinned_messages -> {
val intent = Intent(activity, PinnedMessagesActivity::class.java).apply {
putExtra(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putExtra(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
putExtra(BUNDLE_CHAT_ROOM_NAME, chatRoomName)
}
startActivity(intent)
}
}
return true
}
override fun showMessages(dataSet: List<MessageViewModel>) { override fun showMessages(dataSet: List<MessageViewModel>) {
activity?.apply { activity?.apply {
if (recycler_view.adapter == null) { if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter() adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter)
recycler_view.adapter = adapter recycler_view.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
recycler_view.layoutManager = linearLayoutManager recycler_view.layoutManager = linearLayoutManager
...@@ -117,24 +151,21 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -117,24 +151,21 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun sendMessage(text: String) { override fun sendMessage(text: String) {
if (!text.isBlank()) { if (!text.isBlank()) {
presenter.sendMessage(chatRoomId, text) presenter.sendMessage(chatRoomId, text, editingMessageId)
} }
} }
override fun uploadFile(uri: Uri) { override fun uploadFile(uri: Uri) {
var fileName: String? = null
var mimeType: String? = null
var fileRealPath: String? = null
activity?.apply { activity?.apply {
fileName = uri.getFileName(this) val fileName = uri.getFileName(this)
mimeType = uri.getMimeType(this) val mimeType = uri.getMimeType(this)
fileRealPath = uri.getRealPathFromURI(this) val fileRealPath = uri.getRealPathFromURI(this)
if (fileName != null && mimeType != null && fileRealPath != null) { if (fileName != null && fileRealPath != null) {
presenter.uploadFile( presenter.uploadFile(
chatRoomId, chatRoomId,
File(fileRealPath), File(fileRealPath),
mimeType.toString(), mimeType,
"", // TODO Just leaving it for now, in the future lets add the possibility to add a message with the file to be uploaded. "", // TODO Just leaving it for now, in the future lets add the possibility to add a message with the file to be uploaded.
fileName.toString() fileName.toString()
) )
...@@ -160,17 +191,50 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -160,17 +191,50 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
} }
override fun dispatchUpdateMessage(index: Int, message: MessageViewModel) { override fun dispatchUpdateMessage(index: Int, message: MessageViewModel) {
adapter.updateItem(index, message) adapter.updateItem(message)
}
override fun dispatchDeleteMessage(msgId: String) {
adapter.removeItem(msgId)
}
override fun showReplyingAction(username: String, replyMarkdown: String, quotedMessage: String) {
activity?.apply {
citation = replyMarkdown
actionSnackbar.title = username
actionSnackbar.text = quotedMessage
actionSnackbar.show()
}
} }
override fun showLoading() = view_loading.setVisible(true) override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisible(false) override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() override fun showMessage(message: String) = showToast(message)
override fun showMessage(resId: Int) = showToast(resId)
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun copyToClipboard(message: String) {
activity?.apply {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText("", message))
}
}
override fun showEditingAction(roomId: String, messageId: String, text: String) {
activity?.apply {
actionSnackbar.title = getString(R.string.action_title_editing)
actionSnackbar.text = text
actionSnackbar.show()
text_message.textContent = text
editingMessageId = messageId
}
}
private fun setupComposer() { private fun setupComposer() {
if (isChatRoomReadOnly) { if (isChatRoomReadOnly) {
text_room_is_read_only.setVisible(true) text_room_is_read_only.setVisible(true)
...@@ -192,7 +256,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -192,7 +256,12 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
} }
}) })
button_send.setOnClickListener { sendMessage(text_message.textContent) } button_send.setOnClickListener {
var textMessage = citation ?: ""
textMessage += text_message.textContent
sendMessage(textMessage)
clearActionMessage()
}
button_show_attachment_options.setOnClickListener { button_show_attachment_options.setOnClickListener {
...@@ -217,6 +286,20 @@ class ChatRoomFragment : Fragment(), ChatRoomView { ...@@ -217,6 +286,20 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
} }
} }
private fun setupActionSnackbar() {
actionSnackbar = ActionSnackbar.make(message_list_container, parser = parser)
actionSnackbar.cancelView.setOnClickListener({
clearActionMessage()
})
}
private fun clearActionMessage() {
citation = null
editingMessageId = null
text_message.text.clear()
actionSnackbar.dismiss()
}
private fun showAttachmentOptions() { private fun showAttachmentOptions() {
view_dim.setVisible(true) view_dim.setVisible(true)
......
package chat.rocket.android.chatroom.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject
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"
class PinnedMessagesActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pinned_messages)
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
chatRoomName = intent.getStringExtra(INTENT_CHAT_ROOM_NAME)
requireNotNull(chatRoomName) { "no chat_room_name provided in Intent extras" }
chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
setupToolbar()
addFragment("PinnedMessagesFragment", R.id.fragment_container) {
newPinnedMessagesFragment(chatRoomId, chatRoomName, chatRoomType)
}
}
override fun onBackPressed() = finishActivity()
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = getString(R.string.title_pinned_messages)
toolbar.setNavigationOnClickListener {
finishActivity()
}
}
private fun finishActivity() {
super.onBackPressed()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import android.support.v7.widget.RecyclerView
import android.text.method.LinkMovementMethod
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.AttachmentType
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import com.facebook.drawee.view.SimpleDraweeView
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
import kotlinx.android.synthetic.main.message_attachment.view.*
class PinnedMessagesAdapter : RecyclerView.Adapter<PinnedMessagesAdapter.ViewHolder>() {
init {
setHasStableIds(true)
}
val dataSet = ArrayList<MessageViewModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(parent.inflate(R.layout.item_message))
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(dataSet[position])
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
onBindViewHolder(holder, position)
}
override fun getItemCount(): Int = dataSet.size
override fun getItemViewType(position: Int): Int = position
fun addDataSet(dataSet: List<MessageViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(previousDataSetSize, dataSet)
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
fun addItem(message: MessageViewModel) {
dataSet.add(0, message)
notifyItemInserted(0)
}
fun updateItem(message: MessageViewModel) {
val index = dataSet.indexOfFirst { it.id == message.id }
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
}
fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.id == messageId }
if (index > -1) {
dataSet.removeAt(index)
notifyItemRemoved(index)
}
}
override fun getItemId(position: Int): Long {
return dataSet[position].id.hashCode().toLong()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private lateinit var messageViewModel: MessageViewModel
fun bind(message: MessageViewModel) = with(itemView) {
messageViewModel = message
image_avatar.setImageURI(message.avatarUri)
text_sender.content = message.senderName
text_message_time.content = message.time
text_content.content = message.content
text_content.movementMethod = LinkMovementMethod()
bindAttachment(message, message_attachment, image_attachment, audio_video_attachment,
file_name)
}
private fun bindAttachment(message: MessageViewModel,
attachment_container: View,
image_attachment: SimpleDraweeView,
audio_video_attachment: View,
file_name: TextView) {
with(message) {
if (attachmentUrl == null || attachmentType == null) {
attachment_container.setVisible(false)
return
}
var imageVisible = false
var videoVisible = false
attachment_container.setVisible(true)
when (message.attachmentType) {
is AttachmentType.Image -> {
imageVisible = true
image_attachment.setImageURI(message.attachmentUrl)
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
ImageViewer.Builder(view.context, listOf(message.attachmentUrl))
.setStartPosition(0)
.show()
}
}
is AttachmentType.Video,
is AttachmentType.Audio -> {
videoVisible = true
audio_video_attachment.setOnClickListener { view ->
message.attachmentUrl?.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
image_attachment.setVisible(imageVisible)
audio_video_attachment.setVisible(videoVisible)
file_name.text = message.attachmentTitle
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.PinnedMessagesPresenter
import chat.rocket.android.chatroom.presentation.PinnedMessagesView
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_pinned_messages.*
import javax.inject.Inject
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
private const val BUNDLE_CHAT_ROOM_NAME = "chat_room_name"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
fun newPinnedMessagesFragment(chatRoomId: String, chatRoomType: String, chatRoomName: String): Fragment {
return PinnedMessagesFragment().apply {
arguments = Bundle(3).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_NAME, chatRoomName)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
}
}
}
class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
@Inject lateinit var presenter: PinnedMessagesPresenter
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private lateinit var adapter: PinnedMessagesAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomName = bundle.getString(BUNDLE_CHAT_ROOM_NAME)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
inflater.inflate(R.layout.fragment_pinned_messages, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.loadPinnedMessages(chatRoomId)
}
override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisible(false)
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 showPinnedMessages(pinnedMessages: List<MessageViewModel>) {
activity?.apply {
if (recycler_view_pinned.adapter == null) {
adapter = PinnedMessagesAdapter()
recycler_view_pinned.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view_pinned.layoutManager = linearLayoutManager
recycler_view_pinned.itemAnimator = DefaultItemAnimator()
if (pinnedMessages.size > 10) {
recycler_view_pinned.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
presenter.loadPinnedMessages(chatRoomId)
}
})
}
}
adapter.addDataSet(pinnedMessages)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui.bottomsheet
import android.support.design.widget.BottomSheetDialog
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.MenuItem
import ru.whalemare.sheetmenu.SheetMenu
import ru.whalemare.sheetmenu.adapter.MenuAdapter
class BottomSheetMenu(adapter: MenuAdapter) : SheetMenu(adapter = adapter) {
override fun processRecycler(recycler: RecyclerView, dialog: BottomSheetDialog) {
if (layoutManager == null) {
layoutManager = LinearLayoutManager(recycler.context, LinearLayoutManager.VERTICAL, false)
}
// Superclass SheetMenu adapter property is nullable MenuAdapter? but this class enforces
// passing one at the constructor, so we assume it's always non-null.
val adapter = adapter!!
val callback = adapter.callback
adapter.callback = MenuItem.OnMenuItemClickListener {
callback?.onMenuItemClick(it)
dialog.cancel()
true
}
recycler.adapter = adapter
recycler.layoutManager = layoutManager
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui.bottomsheet.adapter
import android.view.MenuItem
import chat.rocket.android.R
import chat.rocket.android.util.extensions.setVisible
/**
* An adapter for bottomsheet menu that lists all the actions that could be taken over a chat message.
*/
class ActionListAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuItem.OnMenuItemClickListener) :
ListBottomSheetAdapter(menuItems = menuItems, callback = callback) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = menuItems[position]
if (showIcons) {
holder.imageIcon.setVisible(item.icon != null)
} else {
holder.imageIcon.setVisible(false)
}
holder.imageIcon.setImageDrawable(item.icon)
holder.textTitle.text = item.title
holder.itemView.setOnClickListener {
callback?.onMenuItemClick(item)
}
val deleteTextColor = holder.itemView.context.resources.getColor(R.color.red)
val color = if (item.itemId == R.id.action_menu_msg_delete) deleteTextColor else textColors.get(item.itemId)
holder.textTitle.setTextColor(color)
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui.bottomsheet.adapter
import android.graphics.Color
import android.support.annotation.ColorInt
import android.support.annotation.IdRes
import android.util.SparseIntArray
import android.view.MenuItem
import chat.rocket.android.R
import ru.whalemare.sheetmenu.adapter.MenuAdapter
/**
* A regular bottomsheet adapter with added possibility to hide or show a menu item given its item id.
* Also added the possibility to change text colors for the menu items.
*/
open class ListBottomSheetAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuItem.OnMenuItemClickListener) :
MenuAdapter(menuItems = menuItems, callback = callback, itemLayoutId = R.layout.item_linear, showIcons = true) {
// Maps menu item ids to colors.
protected val textColors: SparseIntArray = SparseIntArray(menuItems.size)
init {
for (item in menuItems) {
textColors.put(item.itemId, Color.BLACK)
}
}
/**
* Hide a menu item and disable it.
*
* @param itemId The id of the menu item to disable and hide.
*/
fun hideMenuItem(@IdRes itemId: Int) {
menuItems.firstOrNull { it.itemId == itemId }?.apply {
setVisible(false)
setEnabled(false)
}
}
/**
* Show a menu item and enable it.
*
* @param itemId The id of the menu item to enable and show.
*/
fun showMenuItem(@IdRes itemId: Int) {
menuItems.firstOrNull { it.itemId == itemId }?.apply {
setVisible(true)
setEnabled(true)
}
}
/**
* Change a menu item text color given by its id to the given color.
*
* @param itemId The id of menu item.
* @param color The color (not the resource color id) of the menu item.
*/
fun setMenuItemTextColor(@IdRes itemId: Int, @ColorInt color: Int) {
val itemIndex = menuItems.indexOfFirst { it.itemId == itemId }
if (itemIndex > -1) {
textColors.put(itemId, color)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel package chat.rocket.android.chatroom.viewmodel
import android.content.Context import android.content.Context
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.TokenRepository import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.Value import chat.rocket.core.model.Value
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject import javax.inject.Inject
class MessageViewModelMapper @Inject constructor(private val context: Context, private val tokenRepository: TokenRepository) { class MessageViewModelMapper @Inject constructor(private val context: Context,
private val tokenRepository: TokenRepository,
private val messageParser: MessageParser,
private val messagesRepository: MessagesRepository,
private val localRepository: LocalRepository,
private val currentServerRepository: CurrentServerRepository) {
suspend fun mapToViewModel(message: Message, settings: Map<String, Value<Any>>?) = MessageViewModel(context, tokenRepository.get(), message, settings) suspend fun mapToViewModel(message: Message, settings: Map<String, Value<Any>>): MessageViewModel = withContext(CommonPool) {
MessageViewModel(
this@MessageViewModelMapper.context,
tokenRepository.get(),
message,
settings,
messageParser,
messagesRepository,
localRepository,
currentServerRepository
)
}
suspend fun mapToViewModelList(messageList: List<Message>, settings: Map<String, Value<Any>>?): List<MessageViewModel> { suspend fun mapToViewModelList(messageList: List<Message>, settings: Map<String, Value<Any>>): List<MessageViewModel> {
return messageList.map { MessageViewModel(context, tokenRepository.get(), it, settings) } return messageList.map { MessageViewModel(context, tokenRepository.get(), it, settings,
messageParser, messagesRepository, localRepository, currentServerRepository) }
} }
} }
\ No newline at end of file
...@@ -6,7 +6,7 @@ import chat.rocket.android.server.domain.GetChatRoomsInteractor ...@@ -6,7 +6,7 @@ import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SaveChatRoomsInteractor import chat.rocket.android.server.domain.SaveChatRoomsInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.model.Subscription import chat.rocket.core.internal.model.Subscription
...@@ -38,9 +38,15 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -38,9 +38,15 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
fun loadChatRooms() { fun loadChatRooms() {
launchUI(strategy) { launchUI(strategy) {
view.showLoading() view.showLoading()
view.updateChatRooms(loadRooms()) try {
subscribeRoomUpdates() view.updateChatRooms(loadRooms())
view.hideLoading() subscribeRoomUpdates()
} catch (e: RocketChatException) {
Timber.e(e)
view.showMessage(e.message!!)
} finally {
view.hideLoading()
}
} }
} }
......
...@@ -9,13 +9,13 @@ import android.support.v7.widget.DefaultItemAnimator ...@@ -9,13 +9,13 @@ import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView import android.support.v7.widget.SearchView
import android.view.* import android.view.*
import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.widget.DividerItemDecoration import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
...@@ -99,7 +99,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -99,7 +99,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override fun hideLoading() = view_loading.setVisible(false) override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() 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 showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
...@@ -155,4 +157,4 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -155,4 +157,4 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
return newRooms[newItemPosition] return newRooms[newItemPosition]
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.core.behaviours package chat.rocket.android.core.behaviours
import android.support.annotation.StringRes
interface MessageView { interface MessageView {
/**
* Show message given by resource id.
*
* @param resId The resource id on strings.xml of the message.
*/
fun showMessage(@StringRes resId: Int)
fun showMessage(message: String) fun showMessage(message: String)
fun showGenericErrorMessage() fun showGenericErrorMessage()
......
...@@ -7,11 +7,13 @@ import chat.rocket.android.authentication.signup.di.SignupFragmentProvider ...@@ -7,11 +7,13 @@ import chat.rocket.android.authentication.signup.di.SignupFragmentProvider
import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider
import chat.rocket.android.chatroom.ui.ChatRoomActivity import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.PinnedMessagesActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
import chat.rocket.android.chatrooms.di.ChatRoomsModule import chat.rocket.android.chatrooms.di.ChatRoomsModule
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.di.ProfileFragmentProvider import chat.rocket.android.profile.di.ProfileFragmentProvider
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
...@@ -35,4 +37,8 @@ abstract class ActivityBuilder { ...@@ -35,4 +37,8 @@ abstract class ActivityBuilder {
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [ChatRoomFragmentProvider::class]) @ContributesAndroidInjector(modules = [ChatRoomFragmentProvider::class])
abstract fun bindChatRoomActivity(): ChatRoomActivity abstract fun bindChatRoomActivity(): ChatRoomActivity
@PerActivity
@ContributesAndroidInjector(modules = [PinnedMessagesFragmentProvider::class])
abstract fun bindPinnedMessagesActivity(): PinnedMessagesActivity
} }
\ No newline at end of file
...@@ -5,22 +5,18 @@ import android.arch.persistence.room.Room ...@@ -5,22 +5,18 @@ import android.arch.persistence.room.Room
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.app.utils.CustomImageFormatConfigurator import chat.rocket.android.app.utils.CustomImageFormatConfigurator
import chat.rocket.android.authentication.infraestructure.MemoryTokenRepository import chat.rocket.android.authentication.infraestructure.MemoryTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.dagger.qualifier.ForFresco import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.helper.FrescoAuthInterceptor import chat.rocket.android.helper.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.server.domain.ChatRoomsRepository import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.CurrentServerRepository import chat.rocket.android.server.infraestructure.*
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository
import chat.rocket.android.server.infraestructure.ServerDao
import chat.rocket.android.server.infraestructure.SharedPreferencesSettingsRepository
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.util.AppJsonAdapterFactory import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger import chat.rocket.android.util.TimberLogger
import chat.rocket.common.util.PlatformLogger import chat.rocket.common.util.PlatformLogger
...@@ -38,7 +34,11 @@ import kotlinx.coroutines.experimental.Job ...@@ -38,7 +34,11 @@ import kotlinx.coroutines.experimental.Job
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.il.AsyncDrawableLoader
import ru.noties.markwon.spans.SpannableTheme
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
...@@ -188,7 +188,7 @@ class AppModule { ...@@ -188,7 +188,7 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideMoshi(): Moshi { fun provideMoshi(): Moshi {
return Moshi.Builder().add(AppJsonAdapterFactory.INSTANCE).build() return Moshi.Builder().add(AppJsonAdapterFactory.INSTANCE).build()
} }
@Provides @Provides
...@@ -196,4 +196,38 @@ class AppModule { ...@@ -196,4 +196,38 @@ class AppModule {
fun provideMultiServerTokenRepository(repository: LocalRepository, moshi: Moshi): MultiServerTokenRepository { fun provideMultiServerTokenRepository(repository: LocalRepository, moshi: Moshi): MultiServerTokenRepository {
return SharedPreferencesMultiServerTokenRepository(repository, moshi) return SharedPreferencesMultiServerTokenRepository(repository, moshi)
} }
@Provides
@Singleton
fun provideMessageRepository(): MessagesRepository {
return MemoryMessagesRepository()
}
@Provides
@Singleton
fun provideConfiguration(context: Application, client: OkHttpClient): SpannableConfiguration {
val res = context.resources
return SpannableConfiguration.builder(context)
.asyncDrawableLoader(AsyncDrawableLoader.builder()
.client(client)
.executorService(Executors.newCachedThreadPool())
.resources(res)
.build())
.theme(SpannableTheme.builder()
.linkColor(res.getColor(R.color.colorAccent))
.build())
.build()
}
@Provides
@Singleton
fun provideMessageParser(context: Application, configuration: SpannableConfiguration): MessageParser {
return MessageParser(context, configuration)
}
@Provides
@Singleton
fun providePermissionInteractor(settingsRepository: SettingsRepository, serverRepository: CurrentServerRepository): GetPermissionsInteractor {
return GetPermissionsInteractor(settingsRepository, serverRepository)
}
} }
\ No newline at end of file
package chat.rocket.android.helper
import android.app.Application
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Layout
import android.text.Spannable
import android.text.Spanned
import android.text.TextPaint
import android.text.style.*
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import org.commonmark.node.BlockQuote
import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder
import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.renderer.SpannableMarkdownVisitor
import java.util.regex.Pattern
import javax.inject.Inject
class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) {
private val parser = Markwon.createParser()
private val regexUsername = Pattern.compile("([^\\S]|^)+(@[\\w.]+)",
Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
private val regexLink = Pattern.compile("(https?:\\/\\/)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&/=]*)",
Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
/**
* Render a markdown text message to Spannable.
*
* @param text The text message containing markdown syntax.
* @param quote An optional message to be quoted either by a quote or reply action.
* @param urls A list of urls to convert to markdown link syntax.
*
* @return A Spannable with the parsed markdown.
*/
fun renderMarkdown(text: String, quote: MessageViewModel? = null, selfUsername: String? = null): CharSequence {
val builder = SpannableBuilder()
var content: String = text
// Replace all url links to markdown url syntax.
val matcher = regexLink.matcher(content)
val consumed = mutableListOf<String>()
while (matcher.find()) {
val link = matcher.group(0)
// skip usernames
if (!link.startsWith("@") && !consumed.contains(link)) {
content = content.replace(link, "[$link]($link)")
consumed.add(link)
}
}
val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
quote?.apply {
var quoteNode = parser.parse("> $senderName $time")
parentNode.appendChild(quoteNode)
quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length))
quoteNode = parser.parse("> ${toLenientMarkdown(quote.getOriginalMessage())}")
quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
}
val result = builder.text()
applySpans(result, selfUsername)
return result
}
private fun applySpans(text: CharSequence, currentUser: String?) {
val matcher = regexUsername.matcher(text)
val result = text as Spannable
while (matcher.find()) {
val user = matcher.group(2)
val start = matcher.start(2)
//TODO: should check if username actually exists prior to applying.
val linkColor = context.resources.getColor(R.color.linkTextColor)
val linkBackgroundColor = context.resources.getColor(R.color.linkBackgroundColor)
val referSelf = currentUser != null && "@$currentUser" == user
val usernameSpan = UsernameClickableSpan(linkBackgroundColor, linkColor, referSelf)
result.setSpan(usernameSpan, start, start + user.length, 0)
}
}
/**
* Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
*/
private fun toLenientMarkdown(text: String): String {
return text.trim().replace("\\*(.+)\\*".toRegex()) { "**${it.groupValues[1].trim()}**" }
.replace("\\~(.+)\\~".toRegex()) { "~~${it.groupValues[1].trim()}~~" }
.replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" }
}
class QuoteMessageSenderVisitor(private val context: Context,
configuration: SpannableConfiguration,
private val builder: SpannableBuilder,
private val senderNameLength: Int) : SpannableMarkdownVisitor(configuration, builder) {
override fun visit(blockQuote: BlockQuote) {
// mark current length
val length = builder.length()
// pass to super to apply markdown
super.visit(blockQuote)
val res = context.resources
val timeOffsetStart = length + senderNameLength + 1
builder.setSpan(QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), length, builder.length())
builder.setSpan(StyleSpan(Typeface.BOLD), length, length + senderNameLength)
builder.setSpan(ForegroundColorSpan(Color.BLACK), length, builder.length())
// set time spans
builder.setSpan(AbsoluteSizeSpan(res.getDimensionPixelSize(R.dimen.message_time_text_size)),
timeOffsetStart, builder.length())
builder.setSpan(ForegroundColorSpan(res.getColor(R.color.darkGray)),
timeOffsetStart, builder.length())
}
}
class QuoteMessageBodyVisitor(private val context: Context,
configuration: SpannableConfiguration,
private val builder: SpannableBuilder) : SpannableMarkdownVisitor(configuration, builder) {
override fun visit(blockQuote: BlockQuote) {
// mark current length
val length = builder.length()
// pass to super to apply markdown
super.visit(blockQuote)
builder.setSpan(QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), length, builder.length())
}
}
class QuoteMarginSpan(quoteDrawable: Drawable, private var pad: Int) : LeadingMarginSpan, LineHeightSpan {
private val drawable: Drawable = quoteDrawable
override fun getLeadingMargin(first: Boolean): Int {
return drawable.intrinsicWidth + pad
}
override fun drawLeadingMargin(c: Canvas, p: Paint, x: Int, dir: Int,
top: Int, baseline: Int, bottom: Int,
text: CharSequence, start: Int, end: Int,
first: Boolean, layout: Layout) {
val st = (text as Spanned).getSpanStart(this)
val ix = x
val itop = layout.getLineTop(layout.getLineForOffset(st))
val dw = drawable.intrinsicWidth
val dh = drawable.intrinsicHeight
// XXX What to do about Paint?
drawable.setBounds(ix, itop, ix + dw, itop + layout.height)
drawable.draw(c)
}
override fun chooseHeight(text: CharSequence, start: Int, end: Int,
spanstartv: Int, v: Int,
fm: Paint.FontMetricsInt) {
if (end == (text as Spanned).getSpanEnd(this)) {
val ht = drawable.intrinsicHeight
var need = ht - (v + fm.descent - fm.ascent - spanstartv)
if (need > 0)
fm.descent += need
need = ht - (v + fm.bottom - fm.top - spanstartv)
if (need > 0)
fm.bottom += need
}
}
}
class UsernameClickableSpan(private val linkBackgroundColor: Int,
private val linkTextColor: Int,
private val referSelf: Boolean) : ClickableSpan() {
override fun onClick(widget: View) {
//TODO: Implement action when clicking on username, like showing user profile.
}
override fun updateDrawState(ds: TextPaint) {
if (referSelf) {
ds.color = Color.WHITE
ds.typeface = Typeface.DEFAULT_BOLD
ds.bgColor = linkTextColor
} else {
ds.color = linkTextColor
ds.bgColor = linkBackgroundColor
}
ds.isUnderlineText = false
}
}
}
\ No newline at end of file
...@@ -6,6 +6,7 @@ interface LocalRepository { ...@@ -6,6 +6,7 @@ interface LocalRepository {
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN" const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
const val TOKEN_KEY = "token_" const val TOKEN_KEY = "token_"
const val SETTINGS_KEY = "settings_" const val SETTINGS_KEY = "settings_"
const val USERNAME_KEY = "my_username"
} }
fun save(key: String, value: String?) fun save(key: String, value: String?)
......
...@@ -20,5 +20,6 @@ class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : L ...@@ -20,5 +20,6 @@ class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : L
clear(LocalRepository.KEY_PUSH_TOKEN) clear(LocalRepository.KEY_PUSH_TOKEN)
clear(LocalRepository.TOKEN_KEY + server) clear(LocalRepository.TOKEN_KEY + server)
clear(LocalRepository.SETTINGS_KEY + server) clear(LocalRepository.SETTINGS_KEY + server)
clear(LocalRepository.USERNAME_KEY + server)
} }
} }
\ No newline at end of file
...@@ -8,7 +8,7 @@ import android.view.MenuItem ...@@ -8,7 +8,7 @@ import android.view.MenuItem
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.profile.ui.ProfileFragment import chat.rocket.android.profile.ui.ProfileFragment
import chat.rocket.android.util.addFragment import chat.rocket.android.util.extensions.addFragment
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
......
...@@ -4,7 +4,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy ...@@ -4,7 +4,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
...@@ -27,13 +27,18 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView, ...@@ -27,13 +27,18 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
val myself = client.me() val myself = client.me()
myselfId = myself.id myselfId = myself.id
val avatarUrl = UrlHelper.getAvatarUrl(serverUrl, myself.username!!) val avatarUrl = UrlHelper.getAvatarUrl(serverUrl, myself.username!!)
view.showProfile(avatarUrl, myself.name!!, myself.username!!, myself.emails?.get(0)?.address!!) view.showProfile(
avatarUrl,
myself.name!!,
myself.username!!,
myself.emails?.get(0)?.address!!
)
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
exception.message?.let { exception.message?.let {
view.showMessage(it) view.showMessage(it)
}.ifNull { }.ifNull {
view.showGenericErrorMessage() view.showGenericErrorMessage()
} }
} finally { } finally {
view.hideLoading() view.hideLoading()
} }
...@@ -51,8 +56,8 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView, ...@@ -51,8 +56,8 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
exception.message?.let { exception.message?.let {
view.showMessage(it) view.showMessage(it)
}.ifNull { }.ifNull {
view.showGenericErrorMessage() view.showGenericErrorMessage()
} }
} finally { } finally {
view.hideLoading() view.hideLoading()
} }
......
...@@ -11,10 +11,7 @@ import chat.rocket.android.R ...@@ -11,10 +11,7 @@ import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.presentation.ProfilePresenter import chat.rocket.android.profile.presentation.ProfilePresenter
import chat.rocket.android.profile.presentation.ProfileView import chat.rocket.android.profile.presentation.ProfileView
import chat.rocket.android.util.extensions.asObservable import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.rxkotlin.Observables import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.app_bar.* import kotlinx.android.synthetic.main.app_bar.*
...@@ -79,7 +76,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -79,7 +76,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
enableUserInput(true) enableUserInput(true)
} }
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show() 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 showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
...@@ -148,4 +147,4 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -148,4 +147,4 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_username.isEnabled = value text_username.isEnabled = value
text_email.isEnabled = value text_email.isEnabled = value
} }
} }
\ No newline at end of file
package chat.rocket.android.server.domain package chat.rocket.android.server.domain
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import kotlinx.coroutines.experimental.async import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject import javax.inject.Inject
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) { class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
fun get(url: String) = repository.get(url) fun get(url: String) = repository.get(url)
suspend fun getByName(url: String, name: String): List<ChatRoom> { /**
val chatRooms = async { * Get a list of chat rooms that contains the name parameter.
val allChatRooms = repository.get(url) *
if (name.isEmpty()) { * @param url The server url.
return@async allChatRooms * @param name The name of chat room to look for or a chat room that contains this name.
} * @return A list of ChatRoom objects with the given name.
return@async allChatRooms.filter { */
it.name.contains(name, true) suspend fun getByName(url: String, name: String): List<ChatRoom> = withContext(CommonPool) {
} val allChatRooms = repository.get(url)
if (name.isEmpty()) {
return@withContext allChatRooms
}
return@withContext allChatRooms.filter {
it.name.contains(name, true)
}
}
/**
* Get a specific room by its id.
*
* @param serverUrl The server url where the room is.
* @param roomId The id of the room to get.
* @return The ChatRoom object or null if we couldn't find any.
*/
suspend fun getById(serverUrl: String, roomId: String): ChatRoom? = withContext(CommonPool) {
val allChatRooms = repository.get(serverUrl)
return@withContext allChatRooms.first {
it.id == roomId
} }
return chatRooms.await()
} }
} }
\ No newline at end of file
package chat.rocket.android.server.domain
import javax.inject.Inject
class GetPermissionsInteractor @Inject constructor(private val settingsRepository: SettingsRepository,
private val currentServerRepository: CurrentServerRepository) {
private fun publicSettings(): PublicSettings? = settingsRepository.get(currentServerRepository.get()!!)
/**
* Check whether user is allowed to delete a message.
*/
fun allowedMessageDeleting() = publicSettings()?.allowedMessageDeleting() ?: false
/**
* Checks whether user is allowed to edit a message.
*/
fun allowedMessageEditing() = publicSettings()?.allowedMessageEditing() ?: false
/**
* Checks whether user is allowed to pin a message to a channel.
*/
fun allowedMessagePinning() = publicSettings()?.allowedMessagePinning() ?: false
/**
* Checks whether should show deleted message status.
*/
fun showDeletedStatus() = publicSettings()?.showDeletedStatus() ?: false
/**
* Checks whether should show edited message status.
*/
fun showEditedStatus() = publicSettings()?.showEditedStatus() ?: false
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.core.model.Message
interface MessagesRepository {
/**
* Get message by its message id.
*
* @param id The id of the message to get.
* @return The Message object given by the id or null if message wasn't found.
*/
fun getById(id: String): Message?
/**
* Get all messages from the current room id.
*
* @param rid The room id.
* @return A list of Message objects for the room with given room id or an empty list.
*/
fun getByRoomId(rid: String): List<Message>
/**
* Get all messages. Use carefully!
* @return All messages or an empty list.
*/
fun getAll(): List<Message>
/**
* Save a single message object.
*
* @param The message object to saveAll.
*/
fun save(message: Message)
/**
* Save a list of messages.
*/
fun saveAll(newMessages: List<Message>)
/**
* Removes all messages.
*/
fun clear()
/**
* Remove message by id.
*
* @param id The id of the message to remove.
*/
fun removeById(id: String)
/**
* Remove all messages from a given room.
*
* @param rid The room id where messages are to be removed.
*/
fun removeByRoomId(rid: String)
}
\ No newline at end of file
...@@ -2,9 +2,11 @@ package chat.rocket.android.server.domain ...@@ -2,9 +2,11 @@ package chat.rocket.android.server.domain
import chat.rocket.core.model.Value import chat.rocket.core.model.Value
typealias PublicSettings = Map<String, Value<Any>>
interface SettingsRepository { interface SettingsRepository {
fun save(url: String, settings: Map<String, Value<Any>>) fun save(url: String, settings: PublicSettings)
fun get(url: String): Map<String, Value<Any>>? fun get(url: String): PublicSettings?
} }
const val ACCOUNT_FACEBOOK = "Accounts_OAuth_Facebook" const val ACCOUNT_FACEBOOK = "Accounts_OAuth_Facebook"
...@@ -32,6 +34,11 @@ const val HIDE_USER_LEAVE = "Message_HideType_ul" ...@@ -32,6 +34,11 @@ const val HIDE_USER_LEAVE = "Message_HideType_ul"
const val HIDE_TYPE_AU = "Message_HideType_au" const val HIDE_TYPE_AU = "Message_HideType_au"
const val HIDE_TYPE_RU = "Message_HideType_ru" const val HIDE_TYPE_RU = "Message_HideType_ru"
const val HIDE_MUTE_UNMUTE = "Message_HideType_mute_unmute" const val HIDE_MUTE_UNMUTE = "Message_HideType_mute_unmute"
const val ALLOW_MESSAGE_DELETING = "Message_AllowDeleting"
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. * Extension functions for Public Settings.
* *
...@@ -47,6 +54,15 @@ fun Map<String, Value<Any>>.twitterEnabled(): Boolean = this[ACCOUNT_TWITTER]?.v ...@@ -47,6 +54,15 @@ fun Map<String, Value<Any>>.twitterEnabled(): Boolean = this[ACCOUNT_TWITTER]?.v
fun Map<String, Value<Any>>.gitlabEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true fun Map<String, Value<Any>>.gitlabEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true
fun Map<String, Value<Any>>.wordpressEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true fun Map<String, Value<Any>>.wordpressEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun Map<String, Value<Any>>.useRealName(): Boolean = this[USE_REALNAME]?.value == true
// Message settings
fun Map<String, Value<Any>>.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
fun Map<String, Value<Any>>.showEditedStatus(): Boolean = this[SHOW_EDITED_STATUS]?.value == true
fun Map<String, Value<Any>>.allowedMessagePinning(): Boolean = this[ALLOW_MESSAGE_PINNING]?.value == true
fun Map<String, Value<Any>>.allowedMessageEditing(): Boolean = this[ALLOW_MESSAGE_EDITING]?.value == true
fun Map<String, Value<Any>>.allowedMessageDeleting(): Boolean = this[ALLOW_MESSAGE_DELETING]?.value == true
fun Map<String, Value<Any>>.registrationEnabled(): Boolean { fun Map<String, Value<Any>>.registrationEnabled(): Boolean {
val value = this[ACCOUNT_REGISTRATION] val value = this[ACCOUNT_REGISTRATION]
return value?.value == "Public" return value?.value == "Public"
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.model.Message
class MemoryMessagesRepository : MessagesRepository {
private val messages: HashMap<String, Message> = HashMap()
override fun getById(id: String): Message? {
return messages[id]
}
override fun getByRoomId(rid: String): List<Message> {
return messages.filter { it.value.roomId == rid }.values.toList()
}
override fun getAll(): List<Message> = messages.values.toList()
override fun save(message: Message) {
messages[message.id] = message
}
override fun saveAll(newMessages: List<Message>) {
for (msg in newMessages) {
messages[msg.id] = msg
}
}
override fun clear() {
messages.clear()
}
override fun removeById(id: String) {
messages.remove(id)
}
override fun removeByRoomId(rid: String) {
val roomMessages = messages.filter { it.value.roomId == rid }.values
roomMessages.forEach {
messages.remove(it.roomId)
}
}
}
\ No newline at end of file
package chat.rocket.android.util package chat.rocket.android.util.extensions
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import kotlinx.coroutines.experimental.CoroutineScope import kotlinx.coroutines.experimental.CoroutineScope
......
...@@ -8,6 +8,7 @@ import android.widget.TextView ...@@ -8,6 +8,7 @@ import android.widget.TextView
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import android.provider.MediaStore import android.provider.MediaStore
import ru.noties.markwon.Markwon
fun String.ifEmpty(value: String): String { fun String.ifEmpty(value: String): String {
if (isEmpty()) { if (isEmpty()) {
...@@ -16,6 +17,13 @@ fun String.ifEmpty(value: String): String { ...@@ -16,6 +17,13 @@ fun String.ifEmpty(value: String): String {
return this return this
} }
fun CharSequence.ifEmpty(value: String): CharSequence {
if (isEmpty()) {
return value
}
return this
}
var TextView.textContent: String var TextView.textContent: String
get() = text.toString() get() = text.toString()
set(value) { set(value) {
...@@ -28,6 +36,16 @@ var TextView.hintContent: String ...@@ -28,6 +36,16 @@ var TextView.hintContent: String
hint = value hint = value
} }
var TextView.content: CharSequence
get() = text
set(value) {
Markwon.unscheduleDrawables(this)
Markwon.unscheduleTableRows(this)
text = value
Markwon.scheduleDrawables(this)
Markwon.scheduleTableRows(this)
}
fun Uri.getFileName(activity: Activity): String? { fun Uri.getFileName(activity: Activity): String? {
val cursor = activity.contentResolver.query(this, null, null, null, null, null) val cursor = activity.contentResolver.query(this, null, null, null, null, null)
......
package chat.rocket.android.util package chat.rocket.android.util.extensions
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.support.annotation.LayoutRes
import android.support.annotation.StringRes
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import chat.rocket.android.R import chat.rocket.android.R
fun View.setVisible(visible: Boolean) {
visibility = if (visible) {
View.VISIBLE
} else {
View.GONE
}
}
fun ViewGroup.inflate(@LayoutRes resource: Int): View = LayoutInflater.from(context).inflate(resource, this, false)
fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) { fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance() val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
...@@ -26,4 +42,12 @@ fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int, newInstan ...@@ -26,4 +42,12 @@ fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int, newInstan
fun Activity.hideKeyboard() { fun Activity.hideKeyboard() {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN) imm.hideSoftInputFromWindow(currentFocus.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN)
} }
\ No newline at end of file
fun Activity.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration)
fun Activity.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = Toast.makeText(this, message, duration).show()
fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration)
fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = activity!!.showToast(message, duration)
\ No newline at end of file
package chat.rocket.android.util.extensions
import android.support.annotation.LayoutRes
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
fun View.setVisible(visible: Boolean) {
visibility = if (visible) {
View.VISIBLE
} else {
View.GONE
}
}
fun ViewGroup.inflate(@LayoutRes resource: Int): View {
return LayoutInflater.from(context).inflate(resource, this, false)
}
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"
android:fillColor="#FF0000"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<vector android:height="24dp" android:viewportHeight="800.0"
android:viewportWidth="800.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/actionMenuColor" android:pathData="M511.8,390H520V84C567,67.8 596.3,42 596.3,16H203.7c0,26 29.3,51.8 76.3,68V390h8.2c-92.9,16 -157.5,61 -157.5,108H377v288h48V498h244.3C669.3,451 604.7,406 511.8,390z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M6,17h3l2,-4L11,7L5,7v6h3zM14,17h3l2,-4L19,7h-6v6h3z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"
android:fillColor="@color/actionMenuColor"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/darkGray" />
<size
android:width="4dp"
android:height="4dp" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/AppTheme"
tools:context=".chatroom.ui.PinnedMessagesActivity">
<include
android:id="@+id/layout_app_bar"
layout="@layout/app_bar_chat_room" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/layout_app_bar" />
</RelativeLayout>
\ No newline at end of file
...@@ -17,12 +17,18 @@ ...@@ -17,12 +17,18 @@
app:indicatorName="BallPulseIndicator" app:indicatorName="BallPulseIndicator"
tools:visibility="visible" /> tools:visibility="visible" />
<android.support.v7.widget.RecyclerView <FrameLayout
android:id="@+id/recycler_view" android:id="@+id/message_list_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer" android:layout_above="@+id/layout_message_composer">
android:scrollbars="vertical" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
</FrameLayout>
<View <View
android:id="@+id/view_dim" android:id="@+id/view_dim"
...@@ -37,8 +43,8 @@ ...@@ -37,8 +43,8 @@
layout="@layout/message_attachment_options" layout="@layout/message_attachment_options"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_above="@+id/layout_message_composer" android:layout_above="@+id/layout_message_composer"
android:layout_margin="5dp"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
...@@ -49,4 +55,4 @@ ...@@ -49,4 +55,4 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true" /> android:layout_alignParentBottom="true" />
</RelativeLayout> </RelativeLayout>
\ 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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view_pinned"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="48dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:visibility="gone"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="@+id/recycler_view_pinned"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</android.support.constraint.ConstraintLayout>
\ 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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary">
<TextView
android:id="@+id/text_view_action_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/white"
android:textSize="14sp"
android:typeface="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_view_action_cancel_quote"
app:layout_constraintTop_toBottomOf="@+id/text_view_action_title"
tools:text="action text" />
<ImageView
android:id="@+id/image_view_action_cancel_quote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_close_white_24dp" />
<TextView
android:id="@+id/text_view_action_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:textColor="@color/colorAccent"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/text_view_action_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_view_action_cancel_quote"
app:layout_constraintTop_toTopOf="parent"
tools:text="Edit message" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_pinned_messages"
android:title="@string/title_pinned_messages"
app:showAsAction="never" />
</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:id="@+id/common_actions">
<item
android:id="@+id/action_menu_msg_reply"
android:icon="@drawable/ic_reply_black_24px"
android:title="@string/action_msg_reply" />
<item
android:id="@+id/action_menu_msg_quote"
android:icon="@drawable/ic_quote_black_24px"
android:title="@string/action_msg_quote" />
<item
android:id="@+id/action_menu_msg_edit"
android:icon="@drawable/ic_edit_black_24px"
android:title="@string/action_msg_edit" />
<item
android:id="@+id/action_menu_msg_copy"
android:icon="@drawable/ic_content_copy_black_24px"
android:title="@string/action_msg_copy" />
<!--<item-->
<!--android:id="@+id/action_menu_msg_share"-->
<!--android:icon="@drawable/ic_share_black_24px"-->
<!--android:title="@string/action_msg_share" />-->
<item
android:id="@+id/action_menu_msg_pin_unpin"
android:icon="@drawable/ic_pin_black_24dp"
android:title="@string/action_msg_pin" />
<!--<item-->
<!--android:id="@+id/action_menu_msg_star"-->
<!--android:icon="@drawable/ic_star_black_24px"-->
<!--android:title="@string/action_msg_star" />-->
</group>
<group android:id="@+id/dangerous_actions">
<item
android:id="@+id/action_menu_msg_delete"
android:icon="@drawable/ic_delete_black_24px"
android:title="@string/action_msg_delete" />
</group>
</menu>
\ No newline at end of file
...@@ -57,5 +57,26 @@ ...@@ -57,5 +57,26 @@
<string name="message_user_joined_channel">Entrou no sala.</string> <string name="message_user_joined_channel">Entrou no sala.</string>
<string name="message_welcome">Bem-vindo, %s</string> <string name="message_welcome">Bem-vindo, %s</string>
<string name="message_removed">Mensagem removida</string> <string name="message_removed">Mensagem removida</string>
<string name="message_pinned">Pinou uma mensagem:</string>
<!-- Message actions -->
<string name="action_msg_reply">Responder</string>
<string name="action_msg_edit">Editar</string>
<string name="action_msg_copy">Copiar</string>
<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_star">Favoritar Mensagem</string>
<string name="action_msg_share">Compartilhar</string>
<string name="action_title_editing">Editando Mensagem</string>
<!-- Permission messages -->
<string name="permission_editing_not_allowed">Edição não permitida</string>
<string name="permission_deleting_not_allowed">Remoção não permitida</string>
<string name="permission_pinning_not_allowed">Fixar não permitido</string>
<!-- Pinned Messages -->
<string name="title_pinned_messages">Mensagens Pinadas</string>
</resources> </resources>
\ No newline at end of file
...@@ -24,5 +24,11 @@ ...@@ -24,5 +24,11 @@
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="red">#FFFF0000</color>
</resources> <color name="darkGray">#a0a0a0</color>
\ No newline at end of file <color name="actionMenuColor">#727272</color>
<color name="linkTextColor">#FF074481</color>
<color name="linkBackgroundColor">#30074481</color>
</resources>
...@@ -9,4 +9,6 @@ ...@@ -9,4 +9,6 @@
<dimen name="fab_elevation">6dp</dimen> <dimen name="fab_elevation">6dp</dimen>
<dimen name="message_time_text_size">12sp</dimen>
</resources> </resources>
\ No newline at end of file
...@@ -59,5 +59,26 @@ ...@@ -59,5 +59,26 @@
<string name="message_user_joined_channel">Has joined the channel.</string> <string name="message_user_joined_channel">Has joined the channel.</string>
<string name="message_welcome">Welcome %s</string> <string name="message_welcome">Welcome %s</string>
<string name="message_removed">Message removed</string> <string name="message_removed">Message removed</string>
<string name="message_pinned">Pinned a message:</string>
<!-- Message actions -->
<string name="action_msg_reply">Reply</string>
<string name="action_msg_edit">Edit</string>
<string name="action_msg_copy">Copy</string>
<string name="action_msg_quote">Quote</string>
<string name="action_msg_delete">Delete</string>
<string name="action_msg_pin">Pin Message</string>
<string name="action_msg_unpin">Unpin Message</string>
<string name="action_msg_star">Star Message</string>
<string name="action_msg_share">Share</string>
<string name="action_title_editing">Editing Message</string>
<!-- Permission messages -->
<string name="permission_editing_not_allowed">Editing is not allowed</string>
<string name="permission_deleting_not_allowed">Deleting is not allowed</string>
<string name="permission_pinning_not_allowed">Pinning is not allowed</string>
<!-- Pinned Messages -->
<string name="title_pinned_messages">Pinned Messages</string>
</resources> </resources>
\ No newline at end of file
package chat.rocket.android;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
\ No newline at end of file
package chat.rocket.android
import chat.rocket.android.server.infraestructure.MemoryMessagesRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Test
import org.hamcrest.CoreMatchers.`is` as isEqualTo
class MemoryMessagesRepositoryTest {
val repository = MemoryMessagesRepository()
val msg = Message(
id = "messageId",
roomId = "GENERAL",
message = "Beam me up, Scotty.",
timestamp = 1511443964815,
attachments = null,
sender = null,
avatar = null,
channels = null,
editedAt = null,
editedBy = null,
groupable = true,
mentions = null,
parseUrls = false,
senderAlias = null,
type = MessageType.MessageRemoved(),
updatedAt = 1511443964815,
urls = null
)
val msg2 = Message(
id = "messageId2",
roomId = "sandbox",
message = "Highly Illogical",
timestamp = 1511443964818,
attachments = null,
sender = null,
avatar = null,
channels = null,
editedAt = null,
editedBy = null,
groupable = true,
mentions = null,
parseUrls = false,
senderAlias = null,
type = MessageType.MessageRemoved(),
updatedAt = 1511443964818,
urls = null
)
@Before
fun setup() {
repository.clear()
}
@Test
fun `save() should save a single message`() {
assertThat(repository.getAll().size, isEqualTo(0))
repository.save(msg)
val allMessages = repository.getAll()
assertThat(allMessages.size, isEqualTo(1))
allMessages[0].apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
}
@Test
fun `saveAll() should all saved messages`() {
assertThat(repository.getAll().size, isEqualTo(0))
repository.saveAll(listOf(msg, msg2))
val allMessages = repository.getAll()
assertThat(allMessages.size, isEqualTo(2))
allMessages[0].apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
allMessages[1].apply {
assertThat(id, isEqualTo("messageId2"))
assertThat(message, isEqualTo("Highly Illogical"))
assertThat(roomId, isEqualTo("sandbox"))
}
}
@Test
fun `getById() should return a single message`() {
repository.saveAll(listOf(msg, msg2))
var singleMsg = repository.getById("messageId")
assertThat(singleMsg, notNullValue())
singleMsg!!.apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
singleMsg = repository.getById("messageId2")
assertThat(singleMsg, notNullValue())
singleMsg!!.apply {
assertThat(id, isEqualTo("messageId2"))
assertThat(message, isEqualTo("Highly Illogical"))
assertThat(roomId, isEqualTo("sandbox"))
}
}
@Test
fun `getByRoomId() should return all messages for room id or an empty list`() {
repository.saveAll(listOf(msg, msg2))
var roomMessages = repository.getByRoomId("faAad32fkasods2")
assertThat(roomMessages.isEmpty(), isEqualTo(true))
roomMessages = repository.getByRoomId("sandbox")
assertThat(roomMessages.size, isEqualTo(1))
roomMessages[0].apply {
assertThat(id, isEqualTo("messageId2"))
assertThat(message, isEqualTo("Highly Illogical"))
assertThat(roomId, isEqualTo("sandbox"))
}
roomMessages = repository.getByRoomId("GENERAL")
assertThat(roomMessages.size, isEqualTo(1))
roomMessages[0].apply {
assertThat(id, isEqualTo("messageId"))
assertThat(message, isEqualTo("Beam me up, Scotty."))
assertThat(roomId, isEqualTo("GENERAL"))
}
}
}
\ No newline at end of file
...@@ -27,8 +27,10 @@ ext { ...@@ -27,8 +27,10 @@ ext {
frescoImageViewer : '0.5.0', frescoImageViewer : '0.5.0',
androidSvg : '1.2.1', androidSvg : '1.2.1',
aVLoadingIndicatorView : '2.1.3', aVLoadingIndicatorView : '2.1.3',
markwon : '1.0.3',
textDrawable : '1.0.2', // Remove this library after https://github.com/RocketChat/Rocket.Chat/pull/9492 is merged textDrawable : '1.0.2', // Remove this library after https://github.com/RocketChat/Rocket.Chat/pull/9492 is merged
moshiLazyAdapters : '2.1', // Even declared on the SDK we need to add this library here because java.lang.NoClassDefFoundError: Failed resolution of: Lcom/serjltt/moshi/adapters/FallbackEnum; moshiLazyAdapters : '2.1', // Even declared on the SDK we need to add this library here because java.lang.NoClassDefFoundError: Failed resolution of: Lcom/serjltt/moshi/adapters/FallbackEnum;
sheetMenu : '1.3.3',
// For testing // For testing
junit : '4.12', junit : '4.12',
...@@ -89,8 +91,13 @@ ext { ...@@ -89,8 +91,13 @@ ext {
textDrawable : "com.github.rocketchat:textdrawable:${versions.textDrawable}", textDrawable : "com.github.rocketchat:textdrawable:${versions.textDrawable}",
markwon : "ru.noties:markwon:${versions.markwon}",
markwonImageLoader : "ru.noties:markwon-image-loader:${versions.markwon}",
moshiLazyAdapters : "com.serjltt.moshi:moshi-lazy-adapters:${versions.moshiLazyAdapters}", moshiLazyAdapters : "com.serjltt.moshi:moshi-lazy-adapters:${versions.moshiLazyAdapters}",
sheetMenu : "com.github.whalemare:sheetmenu:${versions.sheetMenu}",
// For testing // For testing
junit : "junit:junit:$versions.junit", junit : "junit:junit:$versions.junit",
expressoCore : "com.android.support.test.espresso:espresso-core:${versions.expresso}", expressoCore : "com.android.support.test.espresso:espresso-core:${versions.expresso}",
......
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