Commit f0448751 authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Merge branch 'develop-2.x' into improvement/svg-in-favor-of-text-drawable

parents a70c775e befa16fc
# 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
Retrolambda needs java8 to be installed on your system
- 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.
- 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
git clone https://github.com/RocketChat/Rocket.Chat.Android.git
cd Rocket.Chat.Android
echo "sdk.dir="$ANDROID_HOME > local.properties
./gradlew assembleDebug
cd ..
cd Rocket.Chat.Android/app/libs
ls
```
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
......@@ -27,4 +47,4 @@ Please report via [GitHub issue](https://github.com/RocketChat/Rocket.Chat.Andro
## 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.
......@@ -12,8 +12,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 1007
versionName "2.0.0-dev5.1"
versionCode 1008
versionName "2.0.0-dev6"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......@@ -94,6 +94,11 @@ dependencies {
implementation libraries.frescoImageViewer
implementation libraries.androidSvg
implementation libraries.markwon
implementation libraries.markwonImageLoader
implementation libraries.sheetMenu
implementation libraries.aVLoadingIndicatorView
implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
......@@ -119,4 +124,4 @@ task compileSdk(type:Exec) {
}
preBuild.dependsOn compileSdk
apply plugin: 'com.google.gms.google-services'
\ No newline at end of file
apply plugin: 'com.google.gms.google-services'
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 @@
</activity>
<activity
android:name=".webview.WebViewActivity"
android:name=".main.ui.MainActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".main.ui.MainActivity"
android:name=".webview.WebViewActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".chatroom.ui.ChatRoomActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".chatroom.ui.PinnedMessagesActivity"
android:theme="@style/AppTheme" />
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
......@@ -78,7 +82,6 @@
<meta-data
android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
</application>
</manifest>
\ No newline at end of file
......@@ -7,12 +7,13 @@ import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
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.RocketChatTwoFactorException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken
import javax.inject.Inject
......@@ -93,7 +94,12 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
try {
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()
navigator.toChatList()
} catch (exception: RocketChatException) {
......
......@@ -6,19 +6,22 @@ import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
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.ScrollView
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.login.presentation.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
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 kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject
......@@ -167,8 +170,9 @@ class LoginFragment : Fragment(), LoginView {
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))
......@@ -216,7 +220,7 @@ class LoginFragment : Fragment(), LoginView {
}
// 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() {
social_accounts_container.postDelayed({
......@@ -232,4 +236,4 @@ class LoginFragment : Fragment(), LoginView {
scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}, 1250)
}
}
\ No newline at end of file
}
......@@ -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.ui.AuthenticationActivity
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
class AuthenticationNavigator(internal val activity: AuthenticationActivity, internal val context: Context) {
......
......@@ -6,7 +6,7 @@ import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.*
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.core.RocketChatClient
import chat.rocket.core.internal.rest.settings
......@@ -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,
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,
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) {
if (!UrlHelper.isValidUrl(server)) {
......@@ -55,8 +56,8 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
......
......@@ -6,12 +6,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.server.presentation.ServerPresenter
import chat.rocket.android.authentication.server.presentation.ServerView
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.util.*
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_server.*
import javax.inject.Inject
......@@ -56,7 +55,9 @@ class ServerFragment : Fragment(), ServerView {
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))
......
......@@ -7,11 +7,12 @@ import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
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.util.ifNull
import chat.rocket.core.RocketChatClient
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.signup
import javax.inject.Inject
......@@ -49,8 +50,12 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
view.showLoading()
try {
client.signup(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?
// TODO This function returns a user so should we save it?
client.signup(email, name, username, password)
// TODO This function returns a user token so should we save it?
client.login(username, password)
val me = client.me()
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
......
......@@ -14,8 +14,9 @@ import chat.rocket.android.authentication.signup.presentation.SignupView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.showToast
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
import javax.inject.Inject
......@@ -100,9 +101,9 @@ class SignupFragment : Fragment(), SignupView {
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))
......
......@@ -8,7 +8,7 @@ import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
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.RocketChatException
import chat.rocket.common.util.ifNull
......@@ -43,8 +43,12 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
view.showLoading()
try {
// The token is saved via the client TokenProvider
val token = client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
multiServerRepository.save(server, TokenModel(token.userId, token.authToken))
val token =
client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
multiServerRepository.save(
server,
TokenModel(token.userId, token.authToken)
)
registerPushToken()
navigator.toChatList()
} catch (exception: RocketChatException) {
......
......@@ -9,13 +9,13 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter
import chat.rocket.android.authentication.twofactor.presentation.TwoFAView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.showToast
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_two_fa.*
import javax.inject.Inject
......@@ -83,7 +83,9 @@ class TwoFAFragment : Fragment(), TwoFAView {
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))
......@@ -108,4 +110,4 @@ class TwoFAFragment : Fragment(), TwoFAView {
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
import chat.rocket.android.R
import chat.rocket.android.authentication.presentation.AuthenticationPresenter
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.AndroidInjector
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
package chat.rocket.android.chatroom.domain
import android.content.Context
import android.net.Uri
import chat.rocket.android.util.extensions.getFileName
import chat.rocket.android.util.extensions.getMimeType
import chat.rocket.android.util.extensions.getRealPathFromURI
import okio.Okio
import java.io.File
import javax.inject.Inject
class UriInteractor @Inject constructor(private val context: Context) {
/**
* Gets the file name from an [Uri].
*/
fun getFileName(uri: Uri): String? = uri.getFileName(context)
/**
* Gets the MimeType of an [Uri]
*/
fun getMimeType(uri: Uri): String = uri.getMimeType(context)
/**
* Gets the real path of an [Uri]
*/
fun getRealPath(uri: Uri): String? = uri.getRealPathFromURI(context)
/**
* Save the contents of an [Uri] to a temp file named after uri.getFileName()
*/
fun tempFile(uri: Uri): File? {
try {
val outputDir = context.cacheDir // context being the Activity pointer
val outputFile = File(outputDir, uri.getFileName(context))
val from = context.contentResolver.openInputStream(uri)
Okio.source(from).use { a ->
Okio.buffer(Okio.sink(outputFile)).use{ b ->
b.writeAll(a)
b.close()
}
a.close()
}
return outputFile
} catch (ex: Exception) {
ex.printStackTrace()
return null
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.presentation
import android.net.Uri
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
......@@ -10,9 +11,8 @@ interface ChatRoomView : LoadingView, MessageView {
* Shows the chat room messages.
*
* @param dataSet The data set to show.
* @param serverUrl The server URL.
*/
fun showMessages(dataSet: List<MessageViewModel>, serverUrl: String)
fun showMessages(dataSet: List<MessageViewModel>)
/**
* Send a message to a chat room.
......@@ -21,6 +21,18 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun sendMessage(text: String)
/**
* Uploads a file to a chat room.
*
* @param uri The file URI to send.
*/
fun uploadFile(uri: Uri)
/**
* Shows a invalid file message.
*/
fun showInvalidFileMessage()
/**
* Shows a (recent) message sent to a chat room.
*
......@@ -28,6 +40,13 @@ interface ChatRoomView : LoadingView, MessageView {
*/
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.
*
......@@ -35,6 +54,27 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun dispatchUpdateMessage(index: Int, message: MessageViewModel)
/**
* 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)
......
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,8 +6,8 @@ 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.addFragment
import chat.rocket.android.util.textContent
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
......@@ -15,6 +15,7 @@ import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject
fun Context.chatRoomIntent(chatRoomId: String, chatRoomName: String, chatRoomType: String, isChatRoomReadOnly: Boolean): Intent {
return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
......@@ -67,6 +68,8 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
}
private fun setupToolbar(chatRoomName: String) {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName
toolbar.setNavigationOnClickListener {
finishActivity()
......
package chat.rocket.android.chatroom.ui
import android.support.v7.widget.RecyclerView
import android.text.method.LinkMovementMethod
import android.view.MenuItem
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.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.MessageViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.common.util.ifNull
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.*
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
class ChatRoomAdapter(private val serverUrl: String) : 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>()
init {
setHasStableIds(true)
}
val dataSet = ArrayList<MessageViewModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(parent.inflate(R.layout.item_message), serverUrl)
ViewHolder(parent.inflate(R.layout.item_message), roomType, roomName, presenter)
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
override fun getItemId(position: Int): Long = dataSet[position].id.hashCode().toLong()
fun addDataSet(dataSet: List<MessageViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(previousDataSetSize, dataSet)
......@@ -50,25 +55,75 @@ class ChatRoomAdapter(private val serverUrl: String) : RecyclerView.Adapter<Chat
notifyItemInserted(0)
}
fun updateItem(index: Int, message: MessageViewModel) {
dataSet[index] = message
notifyItemChanged(index)
fun updateItem(message: MessageViewModel) {
val index = dataSet.indexOfFirst { it.id == message.id }
if (index > -1) {
dataSet[index] = message
notifyItemChanged(index)
}
}
override fun getItemId(position: Int): Long {
return dataSet[position].id.hashCode().toLong()
fun removeItem(messageId: String) {
val index = dataSet.indexOfFirst { it.id == messageId }
if (index > -1) {
dataSet.removeAt(index)
notifyItemRemoved(index)
}
}
class ViewHolder(itemView: View, val serverUrl: String) : RecyclerView.ViewHolder(itemView) {
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) = with(itemView) {
bindUserAvatar(message, image_avatar, image_unknown_avatar)
text_user_name.text = message.sender
text_message_time.text = message.time
text_content.text = message.content
messageViewModel = message
image_avatar.setImageURI(message.avatarUri)
text_sender.text = 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)
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)
}
}
}
bindAttachment(message, message_attachment, image_attachment, audio_video_attachment,
file_name)
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 bindAttachment(message: MessageViewModel,
......@@ -113,14 +168,5 @@ class ChatRoomAdapter(private val serverUrl: String) : RecyclerView.Adapter<Chat
file_name.text = message.attachmentTitle
}
}
private fun bindUserAvatar(message: MessageViewModel, drawee: SimpleDraweeView, imageUnknownAvatar: ImageView) = message.getAvatarUrl(serverUrl).let {
drawee.setImageURI(it.toString())
drawee.setVisible(true)
imageUnknownAvatar.setVisible(false)
}.ifNull {
drawee.setVisible(false)
imageUnknownAvatar.setVisible(true)
}
}
}
\ 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.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
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.model.Message
import chat.rocket.core.model.Value
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
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> {
return messageList.map { MessageViewModel(context, tokenRepository.get(), it, settings) }
suspend fun mapToViewModelList(messageList: List<Message>, settings: Map<String, Value<Any>>): List<MessageViewModel> {
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
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SaveChatRoomsInteractor
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.core.RocketChatClient
import chat.rocket.core.internal.model.Subscription
......@@ -38,9 +38,15 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
fun loadChatRooms() {
launchUI(strategy) {
view.showLoading()
view.updateChatRooms(loadRooms())
subscribeRoomUpdates()
view.hideLoading()
try {
view.updateChatRooms(loadRooms())
subscribeRoomUpdates()
} catch (e: RocketChatException) {
Timber.e(e)
view.showMessage(e.message!!)
} finally {
view.hideLoading()
}
}
}
......
......@@ -10,9 +10,9 @@ import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView
......
......@@ -9,13 +9,13 @@ import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView
import android.view.*
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection
......@@ -99,7 +99,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
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))
......@@ -155,4 +157,4 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
return newRooms[newItemPosition]
}
}
}
\ No newline at end of file
}
package chat.rocket.android.core.behaviours
import android.support.annotation.StringRes
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 showGenericErrorMessage()
......
......@@ -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.ui.AuthenticationActivity
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.PinnedMessagesActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
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.main.ui.MainActivity
import chat.rocket.android.profile.di.ProfileFragmentProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -35,4 +37,8 @@ abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(modules = [ChatRoomFragmentProvider::class])
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
import android.content.Context
import android.content.SharedPreferences
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.app.utils.CustomImageFormatConfigurator
import chat.rocket.android.authentication.infraestructure.MemoryTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.helper.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.server.domain.ChatRoomsRepository
import chat.rocket.android.server.domain.CurrentServerRepository
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.server.domain.*
import chat.rocket.android.server.infraestructure.*
import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger
import chat.rocket.common.util.PlatformLogger
......@@ -38,7 +34,11 @@ import kotlinx.coroutines.experimental.Job
import okhttp3.Interceptor
import okhttp3.OkHttpClient
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 java.util.concurrent.Executors
import javax.inject.Singleton
@Module
......@@ -188,7 +188,7 @@ class AppModule {
@Provides
@Singleton
fun provideMoshi(): Moshi {
return Moshi.Builder().add(AppJsonAdapterFactory.INSTANCE).build()
return Moshi.Builder().add(AppJsonAdapterFactory.INSTANCE).build()
}
@Provides
......@@ -196,4 +196,38 @@ class AppModule {
fun provideMultiServerTokenRepository(repository: LocalRepository, moshi: Moshi): MultiServerTokenRepository {
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
......@@ -5,7 +5,7 @@ import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.widget.TextView
import chat.rocket.android.util.ifEmpty
import chat.rocket.android.util.extensions.ifEmpty
object TextHelper {
......
......@@ -6,6 +6,7 @@ interface LocalRepository {
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
const val TOKEN_KEY = "token_"
const val SETTINGS_KEY = "settings_"
const val USERNAME_KEY = "my_username"
}
fun save(key: String, value: String?)
......
......@@ -20,5 +20,6 @@ class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : L
clear(LocalRepository.KEY_PUSH_TOKEN)
clear(LocalRepository.TOKEN_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
import chat.rocket.android.R
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
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.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......
......@@ -4,7 +4,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.GetCurrentServerInteractor
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.util.ifNull
import chat.rocket.core.RocketChatClient
......@@ -27,13 +27,18 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
val myself = client.me()
myselfId = myself.id
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) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
......@@ -51,8 +56,8 @@ class ProfilePresenter @Inject constructor (private val view: ProfileView,
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
......
......@@ -11,10 +11,7 @@ import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.presentation.ProfilePresenter
import chat.rocket.android.profile.presentation.ProfileView
import chat.rocket.android.util.getObservable
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.app_bar.*
......@@ -79,7 +76,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
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))
......@@ -126,7 +125,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
}
private fun listenToChanges() {
Observables.combineLatest(text_name.getObservable(), text_username.getObservable(), text_email.getObservable()).subscribe({ t ->
Observables.combineLatest(text_name.asObservable(), text_username.asObservable(), text_email.asObservable()).subscribe({ t ->
if (t.first.toString() != currentName || t.second.toString() != currentUsername || t.third.toString() != currentEmail) {
startActionMode()
} else {
......@@ -148,4 +147,4 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_username.isEnabled = value
text_email.isEnabled = value
}
}
\ No newline at end of file
}
package chat.rocket.android.server.domain
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
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
fun get(url: String) = repository.get(url)
suspend fun getByName(url: String, name: String): List<ChatRoom> {
val chatRooms = async {
val allChatRooms = repository.get(url)
if (name.isEmpty()) {
return@async allChatRooms
}
return@async allChatRooms.filter {
it.name.contains(name, true)
}
/**
* Get a list of chat rooms that contains the name parameter.
*
* @param url The server url.
* @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.
*/
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
import chat.rocket.core.model.Value
typealias PublicSettings = Map<String, Value<Any>>
interface SettingsRepository {
fun save(url: String, settings: Map<String, Value<Any>>)
fun get(url: String): Map<String, Value<Any>>?
fun save(url: String, settings: PublicSettings)
fun get(url: String): PublicSettings?
}
const val ACCOUNT_FACEBOOK = "Accounts_OAuth_Facebook"
......@@ -32,6 +34,11 @@ const val HIDE_USER_LEAVE = "Message_HideType_ul"
const val HIDE_TYPE_AU = "Message_HideType_au"
const val HIDE_TYPE_RU = "Message_HideType_ru"
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.
*
......@@ -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>>.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 {
val value = this[ACCOUNT_REGISTRATION]
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
import android.support.annotation.LayoutRes
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.TextView
import com.jakewharton.rxbinding2.widget.RxTextView
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
fun String.ifEmpty(value: String): String {
if (isEmpty()) {
return value
}
return this
}
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)
}
var TextView.textContent: String
get() = text.toString()
set(value) {
text = value
}
var TextView.hintContent: String
get() = hint.toString()
set(value) {
hint = value
}
fun EditText.getObservable(): Observable<CharSequence> {
return RxTextView.textChanges(this)
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
}
\ No newline at end of file
package chat.rocket.android.util.extensions
import android.view.View
import android.view.ViewAnimationUtils
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
fun View.rotateBy(value: Float, duration: Long = 200) {
animate()
.rotationBy(value)
.setDuration(duration)
.start()
}
fun View.fadeInOrOut(startValue: Float, finishValue: Float, duration: Long = 200) {
animate()
.alpha(startValue)
.setDuration(duration)
.setInterpolator(DecelerateInterpolator())
.withEndAction({
animate()
.alpha(finishValue)
.setDuration(duration)
.setInterpolator(AccelerateInterpolator()).start()
}).start()
if (startValue > finishValue) {
setVisible(false)
} else {
setVisible(true)
}
}
fun View.circularRevealOrUnreveal(centerX: Int, centerY: Int, startRadius: Float, endRadius: Float, duration: Long = 600) {
val anim = ViewAnimationUtils.createCircularReveal(this, centerX, centerY, startRadius, endRadius)
anim.duration = duration
if (startRadius < endRadius) {
setVisible(true)
} else {
setVisible(false)
}
anim.start()
}
\ 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 kotlinx.coroutines.experimental.CoroutineScope
......
package chat.rocket.android.util.extensions
import android.widget.EditText
import com.jakewharton.rxbinding2.widget.RxTextView
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
fun EditText.asObservable(debounceTimeout: Long = 100): Observable<CharSequence> {
return RxTextView.textChanges(this)
.debounce(debounceTimeout, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(AndroidSchedulers.mainThread())
}
\ No newline at end of file
package chat.rocket.android.util.extensions
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.widget.TextView
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import android.provider.MediaStore
import ru.noties.markwon.Markwon
fun String.ifEmpty(value: String): String {
if (isEmpty()) {
return value
}
return this
}
fun CharSequence.ifEmpty(value: String): CharSequence {
if (isEmpty()) {
return value
}
return this
}
var TextView.textContent: String
get() = text.toString()
set(value) {
text = value
}
var TextView.hintContent: String
get() = hint.toString()
set(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(context: Context): String? {
val cursor = context.contentResolver.query(this, null, null, null, null, null)
var fileName: String? = null
cursor.use { cursor ->
if (cursor != null && cursor.moveToFirst()) {
fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
return fileName
}
fun Uri.getFileSize(context: Context): String? {
val cursor = context.contentResolver.query(this, null, null, null, null, null)
var fileSize: String? = null
cursor.use { cursor ->
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
if (cursor != null && cursor.moveToFirst()) {
if (!cursor.isNull(sizeIndex)) {
fileSize = cursor.getString(sizeIndex)
}
}
}
return fileSize
}
fun Uri.getMimeType(context: Context): String {
return if (scheme == ContentResolver.SCHEME_CONTENT) {
context.contentResolver.getType(this)
} else {
val fileExtension = MimeTypeMap.getFileExtensionFromUrl(toString())
MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.toLowerCase())
}
}
fun Uri.getRealPathFromURI(context: Context): String? {
val cursor = context.contentResolver.query(this, arrayOf(MediaStore.Images.Media.DATA), null, null, null)
cursor.use { cursor ->
val columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
cursor.moveToFirst()
return cursor.getString(columnIndex)
}
}
\ No newline at end of file
package chat.rocket.android.util
package chat.rocket.android.util.extensions
import android.app.Activity
import android.content.Context
import android.support.annotation.LayoutRes
import android.support.annotation.StringRes
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
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) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
supportFragmentManager.beginTransaction()
......@@ -26,4 +42,12 @@ fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int, newInstan
fun Activity.hideKeyboard() {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
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
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF2F343D"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
\ 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 xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="17.0"
android:viewportWidth="18.0">
<path
android:fillColor="#FF1D74F5"
android:fillType="nonZero"
android:pathData="M4.221,11.799L10.444,5.576C10.97,5.05 11.833,5.067 12.383,5.617C12.928,6.163 12.946,7.034 12.423,7.557L6.195,13.785C5.226,14.754 3.613,14.717 2.599,13.704C1.582,12.687 1.546,11.08 2.52,10.106L9.295,3.331C10.713,1.913 13.07,1.966 14.552,3.448C16.035,4.931 16.087,7.287 14.668,8.706L7.898,15.476L8.747,16.324L15.516,9.555C17.383,7.688 17.316,4.588 15.364,2.636C13.414,0.686 10.313,0.616 8.446,2.483L1.671,9.258C0.248,10.681 0.302,13.03 1.788,14.515C3.27,15.997 5.626,16.051 7.044,14.633L13.272,8.405C14.242,7.435 14.209,5.82 13.195,4.805C12.175,3.786 10.569,3.754 9.595,4.728L3.373,10.95L4.221,11.799Z" />
</vector>
\ No newline at end of file
<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:viewportHeight="20.0"
android:viewportWidth="20.0">
<path
android:fillColor="#FF2F343D"
android:fillType="evenOdd"
android:pathData="M12.4,7.6m-1.2,0a1.2,1.2 0,1 1,2.4 0a1.2,1.2 0,1 1,-2.4 0" />
<path
android:fillColor="#FF2F343D"
android:fillType="evenOdd"
android:pathData="M7.6,7.6m-1.2,0a1.2,1.2 0,1 1,2.4 0a1.2,1.2 0,1 1,-2.4 0" />
<path
android:pathData="M10,10m-8.4,0a8.4,8.4 0,1 1,16.8 0a8.4,8.4 0,1 1,-16.8 0"
android:strokeColor="#FF2F343D"
android:strokeWidth="1.5" />
<path
android:pathData="M6.606,12.794L6.606,12.794C8.48,14.669 11.52,14.669 13.394,12.794"
android:strokeColor="#FF2F343D"
android:strokeWidth="1.5" />
</vector>
\ 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="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:viewportHeight="20.0"
android:viewportWidth="22.0">
<path
android:fillColor="#FF1D74F5"
android:fillType="nonZero"
android:pathData="M0.869,1.32C0.731,0.987 0.79,0.565 1.045,0.276C1.281,0.01 1.634,-0.079 1.967,0.076L20.645,9.134C20.939,9.29 21.136,9.645 21.136,10C21.155,10.377 20.939,10.71 20.645,10.866L1.967,19.924C1.634,20.079 1.281,19.99 1.045,19.724C0.79,19.435 0.712,19.036 0.869,18.68L4.263,10L0.869,1.32ZM18.193,10L3.262,2.741L5.832,9.29L9.953,9.334C10.404,9.312 10.777,9.734 10.777,10.266C10.796,10.777 10.423,11.199 9.953,11.199L5.636,11.199L3.262,17.259L18.193,10Z" />
</vector>
\ 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="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"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="@color/colorLightTheme" />
<corners
android:radius="6dp" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="@color/colorBackgroundContentFileAttachment" />
<corners
android:radius="6dp" />
<stroke
android:width="2dp"
android:color="@color/colorDrawableTintGrey" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="@color/colorBackgroundFileAttachment" />
<corners
android:radius="6dp" />
</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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
......@@ -12,14 +11,4 @@
android:layout_height="40dp"
app:roundedCornerRadius="3dp" />
<!-- TODO define the correct bg color for this-->
<ImageView
android:id="@+id/image_unknown_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_help_black_24dp"
android:tint="@color/colorAccent"
android:visibility="gone"
tools:ignore="contentDescription" />
</LinearLayout>
\ No newline at end of file
......@@ -2,24 +2,11 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/layout_message_composer"
android:scrollbars="vertical" />
<include
android:id="@+id/layout_message_composer"
layout="@layout/message_composer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
......@@ -30,4 +17,42 @@
app:indicatorName="BallPulseIndicator"
tools:visibility="visible" />
</RelativeLayout>
\ No newline at end of file
<FrameLayout
android:id="@+id/message_list_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer">
<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
android:id="@+id/view_dim"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/layout_message_composer"
android:background="@color/colorDim"
android:visibility="gone" />
<include
android:id="@+id/layout_message_attachment_options"
layout="@layout/message_attachment_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer"
android:layout_margin="5dp"
android:visibility="gone"
tools:visibility="visible" />
<include
android:id="@+id/layout_message_composer"
layout="@layout/message_composer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
</RelativeLayout>
<?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
......@@ -27,7 +27,7 @@
app:layout_constraintLeft_toRightOf="@+id/layout_avatar">
<TextView
android:id="@+id/text_user_name"
android:id="@+id/text_sender"
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
......
<?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"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/text_file_attachment"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@drawable/style_message_file_upload_content_bg"
android:drawablePadding="10dp"
android:drawableStart="@drawable/ic_files_24dp"
android:ellipsize="end"
android:gravity="center"
android:maxLength="20"
android:maxLines="1"
android:padding="5dp"
android:visibility="gone"
tools:text="brazilian_anthem.mp4"
tools:visibility="visible" />
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_attachment"
android:layout_width="match_parent"
android:layout_height="150dp"
android:visibility="gone"
fresco:actualImageScaleType="fitStart"
fresco:placeholderImage="@drawable/image_dummy"
tools:visibility="visible"
android:visibility="gone"/>
tools:visibility="visible" />
<FrameLayout
android:id="@+id/audio_video_attachment"
......@@ -23,17 +39,18 @@
android:background="@color/black"
android:visibility="gone"
tools:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/exo_controls_play"/>
android:src="@drawable/exo_controls_play" />
</FrameLayout>
<TextView
android:id="@+id/file_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Filename.png"/>
tools:text="Filename.png" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/style_attachment_options"
android:orientation="vertical">
<Button
android:id="@+id/button_files"
style="?android:attr/borderlessButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="20dp"
android:drawableStart="@drawable/ic_files_24dp"
android:gravity="start|center"
android:text="@string/action_files" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
......@@ -22,36 +22,51 @@
app:layout_constraintTop_toBottomOf="@+id/divider" />
<LinearLayout
android:id="@+id/top_container"
android:id="@+id/input_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:orientation="horizontal"
android:paddingBottom="10dp"
app:layout_constraintTop_toBottomOf="@+id/divider">
<ImageButton
android:id="@+id/button_add_reaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_show_attachment_options"
android:src="@drawable/ic_reaction_24dp" />
<EditText
android:id="@+id/text_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:hint="@string/msg_message"
android:maxLines="4" />
<TextView
android:id="@+id/text_send"
style="@style/TextAppearance.AppCompat.Medium"
<ImageButton
android:id="@+id/button_show_attachment_options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_show_attachment_options"
android:src="@drawable/ic_add_24dp" />
<ImageButton
android:id="@+id/button_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:text="@string/action_send"
android:textColor="@color/colorAccent" />
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/msg_content_description_send_message"
android:src="@drawable/ic_send_24dp"
android:visibility="gone" />
</LinearLayout>
</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
......@@ -11,16 +11,15 @@
<!-- Actions -->
<string name="action_connect">Conectar</string>
<string name="action_send">Enviar</string>
<string name="action_terms_of_service">Termos de Serviço</string>
<string name="action_privacy_policy">Política de Privacidade</string>
<string name="action_search">Pesquisar</string>
<string name="action_update">Atualizar</string>
<string name="action_logout">Sair</string>
<string name="action_files">Arquivos</string>
<!-- Regular information messages -->
<string name="msg_no_internet_connection">Sem conexão à internet</string>
<string name="msg_invalid_server_url">URL de servidor inválida</string>
<string name="msg_generic_error">Desculpe, ocorreu um erro, tente novamente</string>
<string name="msg_no_data_to_display">Nenhum dado para exibir</string>
<string name="msg_profile_update_successfully">Perfil atualizado com sucesso</string>
......@@ -33,10 +32,12 @@
<string name="msg_new_user">Novo usuário? %1$s</string>
<string name="msg_new_user_agreement">Ao proceder você concorda com nossos %1$s e %2$s</string>
<string name="msg_2fa_code">Código 2FA</string>
<string name="msg_invalid_2fa_code">Código 2FA inválido</string>
<string name="msg_yesterday">ontem</string>
<string name="msg_message">Messagem</string>
<string name="msg_this_room_is_read_only">Este chat é apenas de leitura</string>
<string name="msg_invalid_2fa_code">Código 2FA inválido</string>
<string name="msg_invalid_file">Arquivo inválido</string>
<string name="msg_invalid_server_url">URL de servidor inválida</string>
<string name="msg_content_description_log_in_using_facebook">Fazer login através do Facebook</string>
<string name="msg_content_description_log_in_using_github">Fazer login através do Github</string>
<string name="msg_content_description_log_in_using_google">Fazer login através do Google</string>
......@@ -44,6 +45,8 @@
<string name="msg_content_description_log_in_using_meteor">Fazer login através do Meteor</string>
<string name="msg_content_description_log_in_using_twitter">Fazer login através do Twitter</string>
<string name="msg_content_description_log_in_using_gitlab">Fazer login através do Gitlab</string>
<string name="msg_content_description_send_message">Enviar mensagem</string>
<string name="msg_content_description_show_attachment_options">Mostrar opções de anexo</string>
<string name="msg_you">Você</string>
<string name="msg_unknown">Desconhecido</string>
......@@ -55,5 +58,26 @@
<string name="message_user_joined_channel">Entrou no sala.</string>
<string name="message_welcome">Bem-vindo, %s</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>
\ No newline at end of file
......@@ -15,7 +15,20 @@
<color name="colorDividerMessageComposer">#D8D8D8</color>
<color name="colorLightTheme">#FBFBFB</color>
<color name="colorDim">#99000000</color>
<color name="colorBackgroundFileAttachment">#E2FFC8</color>
<color name="colorBackgroundContentFileAttachment">#FFD8F5C0</color>
<color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color>
<color name="red">#FFFF0000</color>
<color name="darkGray">#a0a0a0</color>
<color name="actionMenuColor">#727272</color>
<color name="linkTextColor">#FF074481</color>
<color name="linkBackgroundColor">#30074481</color>
</resources>
\ No newline at end of file
</resources>
......@@ -9,4 +9,6 @@
<dimen name="fab_elevation">6dp</dimen>
<dimen name="message_time_text_size">12sp</dimen>
</resources>
\ No newline at end of file
......@@ -12,16 +12,15 @@
<!-- Actions -->
<string name="action_connect">Connect</string>
<string name="action_send">Send</string>
<string name="action_terms_of_service">Terms of Service</string>
<string name="action_privacy_policy">Privacy Policy</string>
<string name="action_search">Search</string>
<string name="action_update">Update</string>
<string name="action_logout">Log Out</string>
<string name="action_files">Files</string>
<!-- Regular information messages -->
<string name="msg_no_internet_connection">No internet connection</string>
<string name="msg_invalid_server_url">Invalid server URL</string>
<string name="msg_generic_error">Sorry, an error has occurred, please try again</string>
<string name="msg_no_data_to_display">No data to display</string>
<string name="msg_profile_update_successfully">Profile update successfully</string>
......@@ -34,11 +33,13 @@
<string name="msg_new_user">New user? %1$s</string>
<string name="msg_new_user_agreement">By proceeding you are agreeing to our\n%1$s and %2$s</string>
<string name="msg_2fa_code">2FA Code</string>
<string name="msg_invalid_2fa_code">Invalid 2FA Code</string>
<string name="msg_more_than_ninety_nine_unread_messages" translatable="false">99+</string>
<string name="msg_yesterday">Yesterday</string>
<string name="msg_message">Message</string>
<string name="msg_this_room_is_read_only">This room is read only</string>
<string name="msg_invalid_2fa_code">Invalid 2FA Code</string>
<string name="msg_invalid_file">Invalid file</string>
<string name="msg_invalid_server_url">Invalid server URL</string>
<string name="msg_content_description_log_in_using_facebook">Login using Facebook</string>
<string name="msg_content_description_log_in_using_github">Login using Github</string>
<string name="msg_content_description_log_in_using_google">Login using Google</string>
......@@ -46,6 +47,8 @@
<string name="msg_content_description_log_in_using_meteor">Login using Meteor</string>
<string name="msg_content_description_log_in_using_twitter">Login using Twitter</string>
<string name="msg_content_description_log_in_using_gitlab">Login using Gitlab</string>
<string name="msg_content_description_send_message">Send message</string>
<string name="msg_content_description_show_attachment_options">Show attachment options</string>
<string name="msg_you">You</string>
<string name="msg_unknown">Unknown</string>
......@@ -57,5 +60,26 @@
<string name="message_user_joined_channel">Has joined the channel.</string>
<string name="message_welcome">Welcome %s</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>
\ 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
......@@ -26,6 +26,8 @@ ext {
kotshi : '0.3.0',
frescoImageViewer : '0.5.0',
androidSvg : 'master-SNAPSHOT',
markwon : '1.0.3',
sheetMenu : '1.3.3',
aVLoadingIndicatorView : '2.1.3',
// For testing
......@@ -82,6 +84,11 @@ ext {
frescoImageViewer : "com.github.stfalcon:frescoimageviewer:${versions.frescoImageViewer}",
androidSvg : "com.github.BigBadaboom:androidsvg:${versions.androidSvg}",
markwon : "ru.noties:markwon:${versions.markwon}",
markwonImageLoader : "ru.noties:markwon-image-loader:${versions.markwon}",
sheetMenu : "com.github.whalemare:sheetmenu:${versions.sheetMenu}",
aVLoadingIndicatorView : "com.wang.avi:library:${versions.aVLoadingIndicatorView}",
// For testing
......
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