Unverified Commit 99f75dd6 authored by Filipe de Lima Brito's avatar Filipe de Lima Brito Committed by GitHub

Merge pull request #1955 from RocketChat/beta

[RELEASE] Merge BETA into MASTER
parents 1f918d27 07655139
---
name: Bug report
about: Create a bug report to help us improve
---
## Describe the bug
<!-- A clear and concise description of what the bug is. -->
## To Reproduce
<!--Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error -->
## Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
## Logs
<!-- Please add logs in case of any crash or applicable error. -->
## Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->
## Devices and Versions
<!-- Version can be found by opening the side menu and then clicking on "Settings" and then "About" -->
Your Rocket.Chat.Android version: (e.g. 2.1.0)
Your Rocket.Chat Server version: (e.g. 0.63.1-develop)
<!-- Found a bug? List all devices that reproduced it and all that doesn't -->
Mobile device model and OS version: (e.g. "Nexus 7 - Android 6.0.1")
## Additional context
<!-- Add any other context about the problem here. -->
---
name: Chore
about: Issues related to docs, workflow, dependency and others
---
## Describe the chore
<!-- A clear and concise description of what you want to do. -->
## Devices and Versions
<!-- Version can be found by opening the side menu and then clicking on "Settings" and then "About" -->
Your Rocket.Chat.Android version: (e.g. 2.1.0)
Your Rocket.Chat Server version: (e.g. 0.63.1-develop)
<!-- Found a bug? List all devices that reproduced it and all that doesn't -->
Mobile device model and OS version: (e.g. "Nexus 7 - Android 6.0.1")
---
name: Feature request
about: Suggest an idea for this project
---
## Describe the feature you'd like
<!-- A clear and concise description of what you want to happen. -->
## Devices and Versions
<!-- Version can be found by opening the side menu and then clicking on "Settings" and then "About" -->
Your Rocket.Chat.Android version: (e.g. 2.1.0)
Your Rocket.Chat Server version: (e.g. 0.63.1-develop)
<!-- Found a bug? List all devices that reproduced it and all that doesn't -->
Mobile device model and OS version: (e.g. "Nexus 7 - Android 6.0.1")
## Screenshots
<!-- Add screenshots to provide context or UI mockup. -->
## Additional context
<!-- Add any other context about the problem here. -->
...@@ -4,4 +4,6 @@ ...@@ -4,4 +4,6 @@
<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below --> <!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #ISSUE_NUMBER Closes #ISSUE_NUMBER
<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can --> #### Changes: [Add here what changes were made in this issue and if possible provide links.]
\ No newline at end of file
#### Screenshots or GIF for the change:
...@@ -2,29 +2,33 @@ ...@@ -2,29 +2,33 @@
# Rocket.Chat Android native application # Rocket.Chat Android native application
[![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) [![CircleCI](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop.svg?style=shield)](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a81156a8682e4649994270d3670c3c83)](https://www.codacy.com/app/matheusjardimb/Rocket.Chat.Android)
## Get it from the stores
[![](https://user-images.githubusercontent.com/551004/48210434-74c07100-e35e-11e8-8eee-3ba84ffa74d7.png)](https://play.google.com/store/apps/details?id=chat.rocket.android) [![](https://user-images.githubusercontent.com/551004/48210349-50649480-e35e-11e8-97d9-74a4331faf3a.png)](https://f-droid.org/en/packages/chat.rocket.android/)
## Description ## Description
This repository contains all the code related to the Android native application of [Rocket.Chat](https://github.com/RocketChat/Rocket.Chat/#about-rocketchat). To send new pull-requests, always use the branch `develop` as base and open an issue with the description of what you want/need to accomplish, if the issue wasn't created yet. This repository contains all the code related to the Android native application of [Rocket.Chat](https://github.com/RocketChat/Rocket.Chat/#about-rocketchat). To send new pull-requests, always use the branch `develop` as base and open an issue with the description of what you want/need to accomplish, if the issue wasn't created yet.
## How to build ## How to build
- You need to download the latest [Android Studio Preview](https://developer.android.com/studio/preview/) version since the stable IDE version does not support the [JetPack](https://developer.android.com/jetpack/) that is being used on this application. - 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.
- 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. - 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.
### SDK Instructions ### SDK Instructions
- This version requires the [Kotlin SDK](https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK) for Rocket.Chat. Clone the Kotlin SDK in by running `git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git`. - This version requires the [Kotlin SDK](https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK) for Rocket.Chat. Clone the Kotlin SDK in by running `git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git`.
- First, a build is required for the SDK, so that required jar files are generated. Make sure that the android repository and the kotlin sdk have the same immediate parent directory. Change the current directory to `Rocket.Chat.Android/app` and run the `build-sdk.sh` which will result in creating of the required jar file `core*.jar` and `common*.jar` in `Rocket.Chat.Android/app/libs`,by the following steps in your terminal window: - First, a build is required for the SDK, so that required jar files are generated. Make sure that the Android repository and the Kotlin SDK have the same immediate parent directory. Change the current directory to `Rocket.Chat.Android/app` and run the `build-sdk.sh` which will result in creating of the required jar file `core*.jar` and `common*.jar` in `Rocket.Chat.Android/app/libs`, by the following steps in your terminal window:
``` ```
cd Rocket.Chat.Android/app cd Rocket.Chat.Android/app
./build-sdk.sh ./build-sdk.sh
``` ```
**Note:** *You need to have Java 8 as default Java for the system (project won't build when using a Java 9+ version).*
## How to run ## How to run
### Command Line ### Command Line
...@@ -35,7 +39,7 @@ cd Rocket.Chat.Android/app ...@@ -35,7 +39,7 @@ cd Rocket.Chat.Android/app
### Android Studio ### 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. - After importing the project in Android Studio, go to `Run > Run app` and then select your device, or create a new virtual device by following the wizard.
## Bug report & Feature request ## Bug report & Feature request
...@@ -43,4 +47,4 @@ Are you having a technical issue trying to compile the app, or setting up Push N ...@@ -43,4 +47,4 @@ Are you having a technical issue trying to compile the app, or setting up Push N
## Coding Style ## Coding Style
Please follow our [coding style](https://github.com/RocketChat/java-code-styles/blob/master/CODING_STYLE.md) when contributing. Please follow the official [Kotlin coding convections](https://kotlinlang.org/docs/reference/coding-conventions.html) when contributing.
...@@ -16,8 +16,8 @@ android { ...@@ -16,8 +16,8 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion versions.minSdk minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 2050 versionCode 2057
versionName "3.1.1" versionName "3.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
...@@ -140,6 +140,7 @@ dependencies { ...@@ -140,6 +140,7 @@ dependencies {
implementation libraries.frescoAnimatedWebP implementation libraries.frescoAnimatedWebP
implementation libraries.glide implementation libraries.glide
implementation libraries.glideTransformations
kapt libraries.kotshiCompiler kapt libraries.kotshiCompiler
implementation libraries.kotshiApi implementation libraries.kotshiApi
......
This diff is collapsed.
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="rocket_chat_images"
path="Android/data/chat.rocket.android.dev/files/Pictures" />
</paths>
\ No newline at end of file
package chat.rocket.android.helper package chat.rocket.android.helper
import timber.log.Timber import timber.log.Timber
import android.util.Log
// Production logger... Just ignore it
class CrashlyticsTree : Timber.Tree() { class CrashlyticsTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) { override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) {
Log.println(priority, tag, message) throwable?.printStackTrace()
if (throwable != null) {
Log.e(tag,throwable.toString())
}
} }
} }
...@@ -105,6 +105,15 @@ ...@@ -105,6 +105,15 @@
<meta-data <meta-data
android:name="io.fabric.ApiKey" android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" /> android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
</application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="chat.rocket.android.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest> </manifest>
...@@ -61,7 +61,14 @@ interface Analytics { ...@@ -61,7 +61,14 @@ interface Analytics {
fun logServerSwitch(serverUrl: String, serverCount: Int) {} fun logServerSwitch(serverUrl: String, serverCount: Int) {}
/** /**
* Logs the admin opening. * Logs the admin opening event.
*/ */
fun logOpenAdmin() {} fun logOpenAdmin() {}
/**
* Logs the reset password event.
*
* @param resetPasswordSucceeded True if successful reset password, false otherwise.
*/
fun logResetPassword(resetPasswordSucceeded: Boolean) {}
} }
...@@ -70,4 +70,10 @@ class AnalyticsManager @Inject constructor( ...@@ -70,4 +70,10 @@ class AnalyticsManager @Inject constructor(
analytics.forEach { it.logOpenAdmin() } analytics.forEach { it.logOpenAdmin() }
} }
} }
fun logResetPassword(resetPasswordSucceeded: Boolean) {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logResetPassword(resetPasswordSucceeded) }
}
}
} }
...@@ -16,9 +16,9 @@ sealed class ScreenViewEvent(val screenName: String) { ...@@ -16,9 +16,9 @@ sealed class ScreenViewEvent(val screenName: String) {
object ChatRoom : ScreenViewEvent("ChatRoomFragment") object ChatRoom : ScreenViewEvent("ChatRoomFragment")
object ChatRooms : ScreenViewEvent("ChatRoomsFragment") object ChatRooms : ScreenViewEvent("ChatRoomsFragment")
object CreateChannel : ScreenViewEvent("CreateChannelFragment") object CreateChannel : ScreenViewEvent("CreateChannelFragment")
object UserDetails : ScreenViewEvent("UserDetailsFragment")
object FavoriteMessages : ScreenViewEvent("FavoriteMessagesFragment") object FavoriteMessages : ScreenViewEvent("FavoriteMessagesFragment")
object Files : ScreenViewEvent("FilesFragment") object Files : ScreenViewEvent("FilesFragment")
object MemberBottomSheet : ScreenViewEvent("MemberBottomSheetFragment")
object Members : ScreenViewEvent("MembersFragment") object Members : ScreenViewEvent("MembersFragment")
object Mentions : ScreenViewEvent("MentionsFragment") object Mentions : ScreenViewEvent("MentionsFragment")
object MessageInfo : ScreenViewEvent("MessageInfoFragment") object MessageInfo : ScreenViewEvent("MessageInfoFragment")
......
...@@ -23,10 +23,17 @@ class OnBoardingPresenter @Inject constructor( ...@@ -23,10 +23,17 @@ class OnBoardingPresenter @Inject constructor(
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
val settingsInteractor: GetSettingsInteractor, val settingsInteractor: GetSettingsInteractor,
val factory: RocketChatClientFactory val factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, settingsInteractor) { ) : CheckServerPresenter(
strategy = strategy,
factory = factory,
settingsInteractor = settingsInteractor,
refreshSettingsInteractor = refreshSettingsInteractor
) {
fun toSignInToYourServer() = navigator.toSignInToYourServer() fun toSignInToYourServer() = navigator.toSignInToYourServer()
fun toCreateANewServer(createServerUrl: String) = navigator.toWebPage(createServerUrl)
fun connectToCommunityServer(communityServerUrl: String) { fun connectToCommunityServer(communityServerUrl: String) {
connectToServer(communityServerUrl) { connectToServer(communityServerUrl) {
if (totalSocialAccountsEnabled == 0 && !isNewAccountCreationEnabled) { if (totalSocialAccountsEnabled == 0 && !isNewAccountCreationEnabled) {
...@@ -63,8 +70,6 @@ class OnBoardingPresenter @Inject constructor( ...@@ -63,8 +70,6 @@ class OnBoardingPresenter @Inject constructor(
} }
} }
fun toCreateANewServer(createServerUrl: String) = navigator.toWebPage(createServerUrl)
private fun connectToServer(serverUrl: String, block: () -> Unit) { private fun connectToServer(serverUrl: String, block: () -> Unit) {
launchUI(strategy) { launchUI(strategy) {
// Check if we already have an account for this server... // Check if we already have an account for this server...
...@@ -77,9 +82,9 @@ class OnBoardingPresenter @Inject constructor( ...@@ -77,9 +82,9 @@ class OnBoardingPresenter @Inject constructor(
try { try {
withContext(DefaultDispatcher) { withContext(DefaultDispatcher) {
setupConnectionInfo(serverUrl) setupConnectionInfo(serverUrl)
refreshSettingsInteractor.refresh(serverUrl)
// preparing next fragment before showing it // preparing next fragment before showing it
refreshServerAccounts()
checkEnabledAccounts(serverUrl) checkEnabledAccounts(serverUrl)
checkIfLoginFormIsEnabled() checkIfLoginFormIsEnabled()
checkIfCreateNewAccountIsEnabled() checkIfCreateNewAccountIsEnabled()
......
...@@ -25,7 +25,13 @@ class ServerPresenter @Inject constructor( ...@@ -25,7 +25,13 @@ class ServerPresenter @Inject constructor(
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
val settingsInteractor: GetSettingsInteractor, val settingsInteractor: GetSettingsInteractor,
val factory: RocketChatClientFactory val factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, settingsInteractor, view) { ) : CheckServerPresenter(
strategy = strategy,
factory = factory,
settingsInteractor = settingsInteractor,
versionCheckView = view,
refreshSettingsInteractor = refreshSettingsInteractor
) {
fun checkServer(server: String) { fun checkServer(server: String) {
if (!server.isValidUrl()) { if (!server.isValidUrl()) {
...@@ -93,9 +99,8 @@ class ServerPresenter @Inject constructor( ...@@ -93,9 +99,8 @@ class ServerPresenter @Inject constructor(
view.showLoading() view.showLoading()
try { try {
withContext(DefaultDispatcher) { withContext(DefaultDispatcher) {
refreshSettingsInteractor.refresh(serverUrl)
// preparing next fragment before showing it // preparing next fragment before showing it
refreshServerAccounts()
checkEnabledAccounts(serverUrl) checkEnabledAccounts(serverUrl)
checkIfLoginFormIsEnabled() checkIfLoginFormIsEnabled()
checkIfCreateNewAccountIsEnabled() checkIfCreateNewAccountIsEnabled()
...@@ -112,5 +117,4 @@ class ServerPresenter @Inject constructor( ...@@ -112,5 +117,4 @@ class ServerPresenter @Inject constructor(
} }
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.authentication.server.ui package chat.rocket.android.authentication.server.ui
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
...@@ -11,6 +12,7 @@ import android.widget.ScrollView ...@@ -11,6 +12,7 @@ import android.widget.ScrollView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.text.color
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
...@@ -51,6 +53,8 @@ class ServerFragment : Fragment(), ServerView { ...@@ -51,6 +53,8 @@ class ServerFragment : Fragment(), ServerView {
lateinit var analyticsManager: AnalyticsManager lateinit var analyticsManager: AnalyticsManager
private var deepLinkInfo: LoginDeepLinkInfo? = null private var deepLinkInfo: LoginDeepLinkInfo? = null
private var protocol = "https://" private var protocol = "https://"
private var isDomainAppended = false
private var appendedText = ""
private lateinit var serverUrlDisposable: Disposable private lateinit var serverUrlDisposable: Disposable
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) { if (KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) {
...@@ -131,7 +135,7 @@ class ServerFragment : Fragment(), ServerView { ...@@ -131,7 +135,7 @@ class ServerFragment : Fragment(), ServerView {
} }
private fun setupOnClickListener() = private fun setupOnClickListener() =
ui { _ -> ui {
button_connect.setOnClickListener { button_connect.setOnClickListener {
presenter.checkServer("$protocol${text_server_url.textContent.sanitize()}") presenter.checkServer("$protocol${text_server_url.textContent.sanitize()}")
} }
...@@ -244,16 +248,41 @@ class ServerFragment : Fragment(), ServerView { ...@@ -244,16 +248,41 @@ class ServerFragment : Fragment(), ServerView {
private fun subscribeEditText() { private fun subscribeEditText() {
serverUrlDisposable = text_server_url.asObservable() serverUrlDisposable = text_server_url.asObservable()
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
.subscribe { .subscribe { processUserInput(it.toString()) }
if ("$protocol${it.toString()}".isValidUrl()) { }
private fun unsubscribeEditText() = serverUrlDisposable.dispose()
private fun processUserInput(text: String) {
if (text.last().toString() == "." && !isDomainAppended) {
addDomain()
} else if (isDomainAppended && text != appendedText) {
removeDomain()
}
if ("$protocol$text".isValidUrl()) {
enableButtonConnect() enableButtonConnect()
} else { } else {
disableButtonConnect() disableButtonConnect()
} }
} }
private fun addDomain() {
val cursorPosition = text_server_url.length()
text_server_url.append(SpannableStringBuilder()
.color(R.color.colorAuthenticationSecondaryText) { append("rocket.chat") })
text_server_url.setSelection(cursorPosition)
appendedText = text_server_url.text.toString()
isDomainAppended = true
} }
private fun unsubscribeEditText() = serverUrlDisposable.dispose() private fun removeDomain() {
text_server_url.setText(
text_server_url.text.toString().substring(0, text_server_url.selectionEnd)
)
text_server_url.setSelection(text_server_url.length())
isDomainAppended = false
}
private fun enableUserInput() { private fun enableUserInput() {
enableButtonConnect() enableButtonConnect()
......
...@@ -5,7 +5,8 @@ import chat.rocket.android.dagger.scope.PerFragment ...@@ -5,7 +5,8 @@ import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
@Module abstract class TwoFAFragmentProvider { @Module
abstract class TwoFAFragmentProvider {
@ContributesAndroidInjector(modules = [TwoFAFragmentModule::class]) @ContributesAndroidInjector(modules = [TwoFAFragmentModule::class])
@PerFragment @PerFragment
......
...@@ -19,7 +19,6 @@ import dagger.android.AndroidInjector ...@@ -19,7 +19,6 @@ import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar.* import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.coroutines.experimental.Job
import javax.inject.Inject import javax.inject.Inject
class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector { class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
...@@ -27,7 +26,6 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -27,7 +26,6 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject @Inject
lateinit var presenter: AuthenticationPresenter lateinit var presenter: AuthenticationPresenter
val job = Job()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this) AndroidInjection.inject(this)
......
package chat.rocket.android.chatdetails.adapter
import chat.rocket.android.chatdetails.domain.Option
import chat.rocket.android.chatrooms.adapter.ItemHolder
data class OptionItemHolder(override val data: Option): ItemHolder<Option>
\ No newline at end of file
package chat.rocket.android.chatdetails.adapter
import android.content.Context
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.chatdetails.domain.Option
import chat.rocket.android.chatrooms.adapter.ViewHolder
import kotlinx.android.synthetic.main.item_detail_option.view.*
class OptionViewHolder(
itemView: View
): ViewHolder<OptionItemHolder>(itemView) {
override fun bindViews(data: OptionItemHolder) {
val option = data.data
bindName(option, itemView.name)
bindIcon(option, itemView.icon)
itemView.setOnClickListener { option.listener() }
}
private fun bindIcon(option: Option, view: ImageView) {
val drawable = DrawableHelper.getDrawableFromId(option.icon, itemView.context)
drawable.let { image ->
val mutateDrawable = DrawableHelper.wrapDrawable(image).mutate()
DrawableHelper.tintDrawable(mutateDrawable, itemView.context, R.color.colorPrimaryText)
view.setImageDrawable(mutateDrawable)
}
}
private fun bindName(option: Option, view: TextView) {
view.text = option.name
}
}
\ No newline at end of file
package chat.rocket.android.chatdetails.di
import chat.rocket.android.chatdetails.presentation.ChatDetailsView
import chat.rocket.android.chatdetails.ui.ChatDetailsFragment
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.DatabaseManager
import dagger.Module
import dagger.Provides
@Module
class ChatDetailsFragmentModule {
@Provides
@PerFragment
fun chatDetailsView(frag: ChatDetailsFragment): ChatDetailsView {
return frag
}
@Provides
@PerFragment
fun provideChatRoomDao(manager: DatabaseManager): ChatRoomDao = manager.chatRoomDao()
}
\ No newline at end of file
package chat.rocket.android.chatdetails.di
import chat.rocket.android.chatdetails.ui.ChatDetailsFragment
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class ChatDetailsFragmentProvider {
@ContributesAndroidInjector(modules = [ChatDetailsFragmentModule::class])
@PerFragment
abstract fun providesChatDetailsFragment(): ChatDetailsFragment
}
\ No newline at end of file
package chat.rocket.android.chatdetails.domain
data class ChatDetails(
val name: String?,
val fullName: String?,
val type: String?,
val topic: String?,
val announcement: String?,
val description: String?
)
\ No newline at end of file
package chat.rocket.android.chatdetails.domain
data class Option(
val name: String,
val icon: Int,
val listener: () -> Unit
)
\ No newline at end of file
package chat.rocket.android.chatdetails.presentation
import chat.rocket.android.chatdetails.domain.ChatDetails
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getInfo
import chat.rocket.core.model.Room
import timber.log.Timber
import javax.inject.Inject
class ChatDetailsPresenter @Inject constructor(
private val view: ChatDetailsView,
private val navigator: ChatRoomNavigator,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: ConnectionManagerFactory
) {
private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer)
private val client = manager.client
fun getDetails(chatRoomId: String, chatRoomType: String) {
launchUI(strategy) {
try {
val room = retryIO("getInfo($chatRoomId, null, $chatRoomType") {
client.getInfo(chatRoomId, null, roomTypeOf(chatRoomType))
}
view.displayDetails(roomToChatDetails(room))
} catch(exception: Exception) {
Timber.e(exception)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
}
fun toFiles(chatRoomId: String) {
navigator.toFileList(chatRoomId)
}
fun toMembers(chatRoomId: String) {
navigator.toMembersList(chatRoomId)
}
fun toMentions(chatRoomId: String) {
navigator.toMentions(chatRoomId)
}
fun toPinned(chatRoomId: String) {
navigator.toPinnedMessageList(chatRoomId)
}
fun toFavorites(chatRoomId: String) {
navigator.toFavoriteMessageList(chatRoomId)
}
private fun roomToChatDetails(room: Room): ChatDetails {
return with(room) {
ChatDetails(
name = name,
fullName = fullName,
type = type.toString(),
topic = topic,
description = description,
announcement = announcement
)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatdetails.presentation
import chat.rocket.android.chatdetails.domain.ChatDetails
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface ChatDetailsView: MessageView {
fun displayDetails(room: ChatDetails)
}
\ No newline at end of file
package chat.rocket.android.chatdetails.ui
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.chatdetails.adapter.OptionItemHolder
import chat.rocket.android.chatdetails.adapter.OptionViewHolder
import chat.rocket.android.chatdetails.domain.Option
import chat.rocket.android.util.extensions.inflate
class ChatDetailsAdapter: RecyclerView.Adapter<OptionViewHolder>() {
private val options: MutableList<Option> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionViewHolder = OptionViewHolder(parent.inflate(R.layout.item_detail_option))
override fun onBindViewHolder(holder: OptionViewHolder, position: Int) = holder.bindViews(OptionItemHolder(options[position]))
override fun getItemCount(): Int = options.size
fun addOption(name: String, icon: Int, listener: () -> Unit) {
options.add(Option(name, icon, listener))
notifyDataSetChanged()
}
}
\ No newline at end of file
package chat.rocket.android.chatdetails.ui
import DrawableHelper
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import chat.rocket.android.R
import chat.rocket.android.chatdetails.domain.ChatDetails
import chat.rocket.android.chatdetails.presentation.ChatDetailsPresenter
import chat.rocket.android.chatdetails.presentation.ChatDetailsView
import chat.rocket.android.chatdetails.viewmodel.ChatDetailsViewModel
import chat.rocket.android.chatdetails.viewmodel.ChatDetailsViewModelFactory
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_details.*
import javax.inject.Inject
fun newInstance(
chatRoomId: String,
chatRoomType: String,
isSubscribed: Boolean,
disableMenu: Boolean
): ChatDetailsFragment {
return ChatDetailsFragment().apply {
arguments = Bundle(4).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
putBoolean(BUNDLE_IS_SUBSCRIBED, isSubscribed)
putBoolean(BUNDLE_DISABLE_MENU, disableMenu)
}
}
}
internal const val TAG_CHAT_DETAILS_FRAGMENT = "ChatDetailsFragment"
private const val BUNDLE_CHAT_ROOM_ID = "BUNDLE_CHAT_ROOM_ID"
private const val BUNDLE_CHAT_ROOM_TYPE = "BUNDLE_CHAT_ROOM_TYPE"
private const val BUNDLE_IS_SUBSCRIBED = "BUNDLE_IS_SUBSCRIBED"
private const val BUNDLE_DISABLE_MENU = "BUNDLE_DISABLE_MENU"
class ChatDetailsFragment : Fragment(), ChatDetailsView {
@Inject
lateinit var presenter: ChatDetailsPresenter
@Inject
lateinit var factory: ChatDetailsViewModelFactory
private var adapter: ChatDetailsAdapter? = null
private lateinit var viewModel: ChatDetailsViewModel
private var chatRoomId: String? = null
private var chatRoomType: String? = null
private var isSubscribed: Boolean = true
private var disableMenu: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
isSubscribed = bundle.getBoolean(BUNDLE_IS_SUBSCRIBED)
disableMenu = bundle.getBoolean(BUNDLE_DISABLE_MENU)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_chat_details)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this, factory).get(ChatDetailsViewModel::class.java)
setupOptions()
setupToolbar()
getDetails()
}
override fun displayDetails(room: ChatDetails) {
ui {
val text = room.name
name.text = text
bindImage(chatRoomType!!)
content_topic.text =
if (room.topic.isNullOrEmpty()) getString(R.string.msg_no_topic) else room.topic
content_announcement.text =
if (room.announcement.isNullOrEmpty()) getString(R.string.msg_no_announcement) else room.announcement
content_description.text =
if (room.description.isNullOrEmpty()) getString(R.string.msg_no_description) else room.description
}
}
override fun showGenericErrorMessage() = showMessage(R.string.msg_generic_error)
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
ui {
showToast(message)
}
}
private fun addOptions(adapter: ChatDetailsAdapter?) {
adapter?.let {
if (!disableMenu) {
it.addOption(getString(R.string.title_files), R.drawable.ic_files_24dp) {
presenter.toFiles(chatRoomId!!)
}
}
if (chatRoomType != RoomType.DIRECT_MESSAGE && !disableMenu) {
it.addOption(getString(R.string.msg_mentions), R.drawable.ic_at_black_20dp) {
presenter.toMentions(chatRoomId!!)
}
it.addOption(
getString(R.string.title_members),
R.drawable.ic_people_outline_black_24dp
) {
presenter.toMembers(chatRoomId!!)
}
}
it.addOption(
getString(R.string.title_favorite_messages),
R.drawable.ic_star_border_white_24dp
) {
presenter.toFavorites(chatRoomId!!)
}
it.addOption(
getString(R.string.title_pinned_messages),
R.drawable.ic_action_message_pin_24dp
) {
presenter.toPinned(chatRoomId!!)
}
}
}
private fun bindImage(chatRoomType: String) {
val drawable = when (roomTypeOf(chatRoomType)) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_hashtag_black_12dp, context!!)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_12_dp, context!!)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, context!!, R.color.colorPrimary)
DrawableHelper.compoundDrawable(name, mutableDrawable)
}
}
private fun getDetails() {
if (isSubscribed)
viewModel.getDetails(chatRoomId!!).observe(viewLifecycleOwner, Observer { details ->
displayDetails(details)
})
else
presenter.getDetails(chatRoomId!!, chatRoomType!!)
}
private fun setupOptions() {
ui {
if (options.adapter == null) {
adapter = ChatDetailsAdapter()
options.adapter = adapter
with(options) {
layoutManager = LinearLayoutManager(it)
addItemDecoration(
DividerItemDecoration(
it,
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start),
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)
)
)
itemAnimator = DefaultItemAnimator()
isNestedScrollingEnabled = false
}
}
addOptions(adapter)
}
}
private fun setupToolbar() {
with((activity as ChatRoomActivity)) {
hideToolbarChatRoomIcon()
showToolbarTitle(getString(R.string.title_channel_details))
}
}
}
\ No newline at end of file
package chat.rocket.android.chatdetails.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import chat.rocket.android.chatdetails.domain.ChatDetails
import chat.rocket.android.db.ChatRoomDao
class ChatDetailsViewModel(private val chatRoomDao: ChatRoomDao): ViewModel() {
fun getDetails(chatRoomId: String): LiveData<ChatDetails> {
return Transformations.switchMap(chatRoomDao.get(chatRoomId)) { room ->
val entity = room.chatRoom
val data: MutableLiveData<ChatDetails> = MutableLiveData()
data.value = ChatDetails(
entity.name,
entity.fullname,
entity.type,
entity.topic,
entity.announcement,
entity.description
)
return@switchMap data
}
}
}
\ No newline at end of file
package chat.rocket.android.chatdetails.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import chat.rocket.android.db.ChatRoomDao
import javax.inject.Inject
class ChatDetailsViewModelFactory @Inject constructor(private val chatRoomDao: ChatRoomDao) : ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>) =
ChatDetailsViewModel(chatRoomDao) as T
}
\ No newline at end of file
...@@ -146,7 +146,8 @@ class AttachmentViewHolder( ...@@ -146,7 +146,8 @@ class AttachmentViewHolder(
private fun bindAudioOrVideo(data: AttachmentUiModel) { private fun bindAudioOrVideo(data: AttachmentUiModel) {
with(itemView) { with(itemView) {
text_file_name.content = data.title file_name.isVisible = data.hasTitle
file_name.text = data.title
file_text.isVisible = data.hasText file_text.isVisible = data.hasText
file_text.text = data.text file_text.text = data.text
......
...@@ -61,6 +61,10 @@ abstract class BaseViewHolder<T : BaseUiModel<*>>( ...@@ -61,6 +61,10 @@ abstract class BaseViewHolder<T : BaseUiModel<*>>(
reactionListener?.onReactionAdded(messageId, emoji) reactionListener?.onReactionAdded(messageId, emoji)
} }
} }
override fun onReactionLongClicked(shortname: String, isCustom: Boolean, url: String?, usernames: List<String>) {
reactionListener?.onReactionLongClicked(shortname, isCustom,url, usernames)
}
} }
val context = itemView.context val context = itemView.context
......
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import androidx.recyclerview.widget.RecyclerView
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.* import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.chatroom.uimodel.AttachmentUiModel
import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.chatroom.uimodel.MessageReplyUiModel
import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.chatroom.uimodel.UrlPreviewUiModel
import chat.rocket.android.chatroom.uimodel.toViewType
import chat.rocket.android.emoji.EmojiReactionListener import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.openTabbedUrl import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.actions.Action import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction import chat.rocket.core.model.attachment.actions.ButtonAction
import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage import chat.rocket.core.model.isSystemMessage
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException import java.security.InvalidParameterException
...@@ -22,7 +28,8 @@ class ChatRoomAdapter( ...@@ -22,7 +28,8 @@ class ChatRoomAdapter(
private val roomName: String? = null, private val roomName: String? = null,
private val actionSelectListener: OnActionSelected? = null, private val actionSelectListener: OnActionSelected? = null,
private val enableActions: Boolean = true, private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null private val reactionListener: EmojiReactionListener? = null,
private val navigator: ChatRoomNavigator? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() { ) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseUiModel<*>>() private val dataSet = ArrayList<BaseUiModel<*>>()
...@@ -34,7 +41,11 @@ class ChatRoomAdapter( ...@@ -34,7 +41,11 @@ class ChatRoomAdapter(
return when (viewType.toViewType()) { return when (viewType.toViewType()) {
BaseUiModel.ViewType.MESSAGE -> { BaseUiModel.ViewType.MESSAGE -> {
val view = parent.inflate(R.layout.item_message) val view = parent.inflate(R.layout.item_message)
MessageViewHolder(view, actionsListener, reactionListener) MessageViewHolder(
view,
actionsListener,
reactionListener
) { userId -> navigator?.toUserDetails(userId) }
} }
BaseUiModel.ViewType.URL_PREVIEW -> { BaseUiModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview) val view = parent.inflate(R.layout.message_url_preview)
...@@ -42,11 +53,20 @@ class ChatRoomAdapter( ...@@ -42,11 +53,20 @@ class ChatRoomAdapter(
} }
BaseUiModel.ViewType.ATTACHMENT -> { BaseUiModel.ViewType.ATTACHMENT -> {
val view = parent.inflate(R.layout.item_message_attachment) val view = parent.inflate(R.layout.item_message_attachment)
AttachmentViewHolder(view, actionsListener, reactionListener, actionAttachmentOnClickListener) AttachmentViewHolder(
view,
actionsListener,
reactionListener,
actionAttachmentOnClickListener
)
} }
BaseUiModel.ViewType.MESSAGE_REPLY -> { BaseUiModel.ViewType.MESSAGE_REPLY -> {
val view = parent.inflate(R.layout.item_message_reply) val view = parent.inflate(R.layout.item_message_reply)
MessageReplyViewHolder(view, actionsListener, reactionListener) { roomName, permalink -> MessageReplyViewHolder(
view,
actionsListener,
reactionListener
) { roomName, permalink ->
actionSelectListener?.openDirectMessage(roomName, permalink) actionSelectListener?.openDirectMessage(roomName, permalink)
} }
} }
...@@ -117,7 +137,8 @@ class ChatRoomAdapter( ...@@ -117,7 +137,8 @@ class ChatRoomAdapter(
fun prependData(dataSet: List<BaseUiModel<*>>) { fun prependData(dataSet: List<BaseUiModel<*>>) {
//---At first we will update all already saved elements with received updated ones //---At first we will update all already saved elements with received updated ones
val filteredDataSet = dataSet.filter { newItem -> val filteredDataSet = dataSet.filter { newItem ->
val matchedIndex = this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } val matchedIndex =
this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType }
if (matchedIndex > -1) { if (matchedIndex > -1) {
this.dataSet[matchedIndex] = newItem this.dataSet[matchedIndex] = newItem
notifyItemChanged(matchedIndex) notifyItemChanged(matchedIndex)
...@@ -251,6 +272,9 @@ class ChatRoomAdapter( ...@@ -251,6 +272,9 @@ class ChatRoomAdapter(
R.id.action_message_permalink -> { R.id.action_message_permalink -> {
actionSelectListener?.copyPermalink(id) actionSelectListener?.copyPermalink(id)
} }
R.id.action_message_report -> {
actionSelectListener?.reportMessage(id)
}
else -> { else -> {
TODO("Not implemented") TODO("Not implemented")
} }
...@@ -260,16 +284,34 @@ class ChatRoomAdapter( ...@@ -260,16 +284,34 @@ class ChatRoomAdapter(
} }
interface OnActionSelected { interface OnActionSelected {
fun showMessageInfo(id: String) fun showMessageInfo(id: String)
fun citeMessage(roomName: String, roomType: String, messageId: String, mentionAuthor: Boolean)
fun citeMessage(
roomName: String,
roomType: String,
messageId: String,
mentionAuthor: Boolean
)
fun copyMessage(id: String) fun copyMessage(id: String)
fun editMessage(roomId: String, messageId: String, text: String) fun editMessage(roomId: String, messageId: String, text: String)
fun toogleStar(id: String, star: Boolean) fun toogleStar(id: String, star: Boolean)
fun tooglePin(id: String, pin: Boolean) fun tooglePin(id: String, pin: Boolean)
fun deleteMessage(roomId: String, id: String) fun deleteMessage(roomId: String, id: String)
fun showReactions(id: String) fun showReactions(id: String)
fun openDirectMessage(roomName: String, message: String) fun openDirectMessage(roomName: String, message: String)
fun sendMessage(chatRoomId: String, text: String) fun sendMessage(chatRoomId: String, text: String)
fun copyPermalink(id: String) fun copyPermalink(id: String)
fun reportMessage(id: String)
} }
} }
...@@ -75,7 +75,7 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() ...@@ -75,7 +75,7 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
class ReactionViewHolder( class ReactionViewHolder(
view: View, view: View,
private val listener: EmojiReactionListener? private val listener: EmojiReactionListener?
) : RecyclerView.ViewHolder(view), View.OnClickListener { ) : RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
@Inject @Inject
lateinit var localRepository: LocalRepository lateinit var localRepository: LocalRepository
...@@ -121,6 +121,8 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() ...@@ -121,6 +121,8 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
view_flipper_reaction.setOnClickListener(this@ReactionViewHolder) view_flipper_reaction.setOnClickListener(this@ReactionViewHolder)
text_count.setOnClickListener(this@ReactionViewHolder) text_count.setOnClickListener(this@ReactionViewHolder)
view_flipper_reaction.setOnLongClickListener(this@ReactionViewHolder)
text_count.setOnLongClickListener(this@ReactionViewHolder)
} }
} }
...@@ -132,6 +134,11 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() ...@@ -132,6 +134,11 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
} }
} }
} }
override fun onLongClick(v: View?): Boolean {
listener?.onReactionLongClicked(reaction.shortname, reaction.isCustom, reaction.url, reaction.usernames)
return true
}
} }
class AddReactionViewHolder( class AddReactionViewHolder(
......
...@@ -18,7 +18,8 @@ import kotlinx.android.synthetic.main.item_message.view.* ...@@ -18,7 +18,8 @@ import kotlinx.android.synthetic.main.item_message.view.*
class MessageViewHolder( class MessageViewHolder(
itemView: View, itemView: View,
listener: ActionsListener, listener: ActionsListener,
reactionListener: EmojiReactionListener? = null reactionListener: EmojiReactionListener? = null,
private val avatarListener: (String) -> Unit
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener), Drawable.Callback { ) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener), Drawable.Callback {
init { init {
...@@ -70,6 +71,12 @@ class MessageViewHolder( ...@@ -70,6 +71,12 @@ class MessageViewHolder(
) )
read_receipt_view.isVisible = true read_receipt_view.isVisible = true
} }
image_avatar.setOnClickListener {
data.message.sender?.id?.let { userId ->
avatarListener(userId)
}
}
} }
} }
......
package chat.rocket.android.chatroom.di package chat.rocket.android.chatroom.di
import android.app.Application
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.ChatRoomView import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.chatroom.ui.ChatRoomFragment import chat.rocket.android.chatroom.ui.ChatRoomFragment
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.chatrooms.adapter.RoomUiModelMapper
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.db.ChatRoomDao import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.DatabaseManagerFactory import chat.rocket.android.db.UserDao
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentUserInteractor
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
import javax.inject.Named import javax.inject.Named
@Module @Module
class ChatRoomFragmentModule { class ChatRoomFragmentModule {
@Provides
@PerFragment
fun provideJob() = Job()
@Provides @Provides
@PerFragment @PerFragment
fun chatRoomView(frag: ChatRoomFragment): ChatRoomView { fun chatRoomView(frag: ChatRoomFragment): ChatRoomView {
...@@ -35,11 +34,37 @@ class ChatRoomFragmentModule { ...@@ -35,11 +34,37 @@ class ChatRoomFragmentModule {
@Provides @Provides
@PerFragment @PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy { fun provideChatRoomDao(manager: DatabaseManager): ChatRoomDao = manager.chatRoomDao()
return CancelStrategy(owner, jobs)
@Provides
@PerFragment
fun provideUserDao(manager: DatabaseManager): UserDao = manager.userDao()
@Provides
@PerFragment
fun provideGetCurrentUserInteractor(
tokenRepository: TokenRepository,
@Named("currentServer") serverUrl: String,
userDao: UserDao
): GetCurrentUserInteractor {
return GetCurrentUserInteractor(tokenRepository, serverUrl, userDao)
} }
@Provides @Provides
@PerFragment @PerFragment
fun provideChatRoomDao(manager: DatabaseManager): ChatRoomDao = manager.chatRoomDao() fun provideRoomMapper(
context: Application,
repository: SettingsRepository,
userInteractor: GetCurrentUserInteractor,
@Named("currentServer") serverUrl: String,
permissionsInteractor: PermissionsInteractor
): RoomUiModelMapper {
return RoomUiModelMapper(
context,
repository.get(serverUrl),
userInteractor,
serverUrl,
permissionsInteractor
)
}
} }
package chat.rocket.android.chatroom.di package chat.rocket.android.chatroom.di
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.chatroom.ui.ChatRoomActivity import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class ChatRoomModule { class ChatRoomModule {
@Provides @Provides
@PerActivity @PerActivity
fun provideChatRoomNavigator(activity: ChatRoomActivity) = ChatRoomNavigator(activity) fun provideChatRoomNavigator(activity: ChatRoomActivity) = ChatRoomNavigator(activity)
@Provides
@PerActivity
fun provideJob() = Job()
@Provides
@PerActivity
fun provideLifecycleOwner(activity: ChatRoomActivity): LifecycleOwner = activity
@Provides
@PerActivity
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
...@@ -6,6 +6,7 @@ import dagger.Module ...@@ -6,6 +6,7 @@ import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
@Module abstract class MessageServiceProvider { @Module abstract class MessageServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class]) @ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideMessageService(): MessageService abstract fun provideMessageService(): MessageService
} }
\ No newline at end of file
package chat.rocket.android.chatroom.presentation package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatdetails.ui.TAG_CHAT_DETAILS_FRAGMENT
import chat.rocket.android.chatinformation.ui.messageInformationIntent import chat.rocket.android.chatinformation.ui.messageInformationIntent
import chat.rocket.android.chatroom.ui.ChatRoomActivity import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.chatRoomIntent import chat.rocket.android.chatroom.ui.chatRoomIntent
...@@ -10,16 +11,70 @@ import chat.rocket.android.members.ui.TAG_MEMBERS_FRAGMENT ...@@ -10,16 +11,70 @@ import chat.rocket.android.members.ui.TAG_MEMBERS_FRAGMENT
import chat.rocket.android.mentions.ui.TAG_MENTIONS_FRAGMENT import chat.rocket.android.mentions.ui.TAG_MENTIONS_FRAGMENT
import chat.rocket.android.pinnedmessages.ui.TAG_PINNED_MESSAGES_FRAGMENT import chat.rocket.android.pinnedmessages.ui.TAG_PINNED_MESSAGES_FRAGMENT
import chat.rocket.android.server.ui.changeServerIntent import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.userdetails.ui.TAG_USER_DETAILS_FRAGMENT
import chat.rocket.android.util.extensions.addFragmentBackStack import chat.rocket.android.util.extensions.addFragmentBackStack
class ChatRoomNavigator(internal val activity: ChatRoomActivity) { class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
fun toUserDetails(userId: String) {
activity.addFragmentBackStack(TAG_USER_DETAILS_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.userdetails.ui.newInstance(userId)
}
}
fun toChatRoom(
chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isReadOnly: Boolean,
chatRoomLastSeen: Long,
isSubscribed: Boolean,
isCreator: Boolean,
isFavorite: Boolean
) {
activity.startActivity(
activity.chatRoomIntent(
chatRoomId,
chatRoomName,
chatRoomType,
isReadOnly,
chatRoomLastSeen,
isSubscribed,
isCreator,
isFavorite
)
)
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
fun toChatDetails(
chatRoomId: String,
chatRoomType: String,
isChatRoomSubscribed: Boolean,
isMenuDisabled: Boolean
) {
activity.addFragmentBackStack(TAG_CHAT_DETAILS_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.chatdetails.ui.newInstance(
chatRoomId,
chatRoomType,
isChatRoomSubscribed,
isMenuDisabled
)
}
}
fun toMembersList(chatRoomId: String) { fun toMembersList(chatRoomId: String) {
activity.addFragmentBackStack(TAG_MEMBERS_FRAGMENT, R.id.fragment_container) { activity.addFragmentBackStack(TAG_MEMBERS_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.members.ui.newInstance(chatRoomId) chat.rocket.android.members.ui.newInstance(chatRoomId)
} }
} }
fun toMemberDetails(userId: String) {
activity.addFragmentBackStack(TAG_USER_DETAILS_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.userdetails.ui.newInstance(userId)
}
}
fun toMentions(chatRoomId: String) { fun toMentions(chatRoomId: String) {
activity.addFragmentBackStack(TAG_MENTIONS_FRAGMENT, R.id.fragment_container) { activity.addFragmentBackStack(TAG_MENTIONS_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.mentions.ui.newInstance(chatRoomId) chat.rocket.android.mentions.ui.newInstance(chatRoomId)
......
...@@ -5,6 +5,7 @@ import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel ...@@ -5,6 +5,7 @@ import chat.rocket.android.chatroom.uimodel.suggestion.ChatRoomSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel import chat.rocket.android.chatroom.uimodel.suggestion.CommandSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel import chat.rocket.android.chatroom.uimodel.suggestion.EmojiSuggestionUiModel
import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel import chat.rocket.android.chatroom.uimodel.suggestion.PeopleSuggestionUiModel
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.socket.model.State import chat.rocket.core.internal.realtime.socket.model.State
...@@ -131,12 +132,7 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -131,12 +132,7 @@ interface ChatRoomView : LoadingView, MessageView {
fun populateEmojiSuggestions(emojis: List<EmojiSuggestionUiModel>) fun populateEmojiSuggestions(emojis: List<EmojiSuggestionUiModel>)
/** fun onJoined(roomUiModel: RoomUiModel)
* This user has joined the chat callback.
*
* @param userCanPost Whether the user can post a message or not.
*/
fun onJoined(userCanPost: Boolean)
fun showReactionsPopup(messageId: String) fun showReactionsPopup(messageId: String)
...@@ -147,9 +143,6 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -147,9 +143,6 @@ interface ChatRoomView : LoadingView, MessageView {
*/ */
fun populateCommandSuggestions(commands: List<CommandSuggestionUiModel>) fun populateCommandSuggestions(commands: List<CommandSuggestionUiModel>)
/** fun onRoomUpdated(roomUiModel: RoomUiModel)
* Communicate whether it's a broadcast channel and if current user can post to it.
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
} }
...@@ -8,63 +8,25 @@ import androidx.appcompat.widget.SearchView ...@@ -8,63 +8,25 @@ import androidx.appcompat.widget.SearchView
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.util.extension.onQueryTextListener import chat.rocket.android.util.extension.onQueryTextListener
import chat.rocket.common.model.RoomType
internal fun ChatRoomFragment.setupMenu(menu: Menu) { internal fun ChatRoomFragment.setupMenu(menu: Menu) {
setupSearchMessageMenuItem(menu, requireContext()) setupSearchMessageMenuItem(menu, requireContext())
setupFavoriteMenuItem(menu) setupFavoriteMenuItem(menu)
setupDetailsMenuItem(menu)
menu.add(
Menu.NONE,
MENU_ACTION_PINNED_MESSAGES,
Menu.NONE,
R.string.title_pinned_messages
)
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_MESSAGES,
Menu.NONE,
R.string.title_favorite_messages
)
if (chatRoomType != RoomType.DIRECT_MESSAGE && !disableMenu) {
menu.add(
Menu.NONE,
MENU_ACTION_MEMBER,
Menu.NONE,
R.string.title_members_list
)
menu.add(
Menu.NONE,
MENU_ACTION_MENTIONS,
Menu.NONE,
R.string.msg_mentions
)
}
if (!disableMenu) {
menu.add(
Menu.NONE,
MENU_ACTION_FILES,
Menu.NONE,
R.string.title_files
)
}
} }
internal fun ChatRoomFragment.setOnMenuItemClickListener(item: MenuItem) { internal fun ChatRoomFragment.setOnMenuItemClickListener(item: MenuItem) {
when (item.itemId) { when (item.itemId) {
MENU_ACTION_FAVORITE_UNFAVORITE_CHAT -> presenter.toggleFavoriteChatRoom( MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT -> presenter.toggleFavoriteChatRoom(
chatRoomId, chatRoomId,
isFavorite isFavorite
) )
MENU_ACTION_MEMBER -> presenter.toMembersList(chatRoomId) MENU_ACTION_SHOW_DETAILS -> presenter.toChatDetails(
MENU_ACTION_MENTIONS -> presenter.toMentions(chatRoomId) chatRoomId,
MENU_ACTION_PINNED_MESSAGES -> presenter.toPinnedMessageList(chatRoomId) chatRoomType,
MENU_ACTION_FAVORITE_MESSAGES -> presenter.toFavoriteMessageList(chatRoomId) isSubscribed,
MENU_ACTION_FILES -> presenter.toFileList(chatRoomId) disableMenu
)
} }
} }
...@@ -125,7 +87,7 @@ private fun ChatRoomFragment.setupFavoriteMenuItem(menu: Menu) { ...@@ -125,7 +87,7 @@ private fun ChatRoomFragment.setupFavoriteMenuItem(menu: Menu) {
if (isFavorite) { if (isFavorite) {
menu.add( menu.add(
Menu.NONE, Menu.NONE,
MENU_ACTION_FAVORITE_UNFAVORITE_CHAT, MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT,
Menu.NONE, Menu.NONE,
R.string.title_unfavorite_chat R.string.title_unfavorite_chat
).setIcon(R.drawable.ic_star_yellow_24dp) ).setIcon(R.drawable.ic_star_yellow_24dp)
...@@ -133,10 +95,20 @@ private fun ChatRoomFragment.setupFavoriteMenuItem(menu: Menu) { ...@@ -133,10 +95,20 @@ private fun ChatRoomFragment.setupFavoriteMenuItem(menu: Menu) {
} else { } else {
menu.add( menu.add(
Menu.NONE, Menu.NONE,
MENU_ACTION_FAVORITE_UNFAVORITE_CHAT, MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT,
Menu.NONE, Menu.NONE,
R.string.title_favorite_chat R.string.title_favorite_chat
).setIcon(R.drawable.ic_star_border_white_24dp) ).setIcon(R.drawable.ic_star_border_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} }
} }
private fun ChatRoomFragment.setupDetailsMenuItem(menu: Menu) {
menu.add(
Menu.NONE,
MENU_ACTION_SHOW_DETAILS,
Menu.NONE,
R.string.title_channel_details
).setIcon(R.drawable.ic_info_outline_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
\ No newline at end of file
...@@ -21,7 +21,8 @@ data class MessageUiModel( ...@@ -21,7 +21,8 @@ data class MessageUiModel(
var isFirstUnread: Boolean, var isFirstUnread: Boolean,
override var isTemporary: Boolean = false, override var isTemporary: Boolean = false,
override var menuItemsToHide: MutableList<Int> = mutableListOf(), override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var permalink: String override var permalink: String,
val subscriptionId: String
) : BaseMessageUiModel<Message> { ) : BaseMessageUiModel<Message> {
override val viewType: Int override val viewType: Int
get() = BaseUiModel.ViewType.MESSAGE.viewType get() = BaseUiModel.ViewType.MESSAGE.viewType
......
...@@ -6,5 +6,6 @@ data class ReactionUiModel( ...@@ -6,5 +6,6 @@ data class ReactionUiModel(
val unicode: CharSequence, val unicode: CharSequence,
val count: Int, val count: Int,
val usernames: List<String> = emptyList(), val usernames: List<String> = emptyList(),
var url: String? = null var url: String? = null,
val isCustom: Boolean = false
) )
...@@ -31,6 +31,7 @@ import chat.rocket.android.server.domain.messageReadReceiptEnabled ...@@ -31,6 +31,7 @@ import chat.rocket.android.server.domain.messageReadReceiptEnabled
import chat.rocket.android.server.domain.messageReadReceiptStoreUsers import chat.rocket.android.server.domain.messageReadReceiptStoreUsers
import chat.rocket.android.server.domain.useRealName import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extension.isImage
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.ifNotNullNorEmpty import chat.rocket.android.util.extensions.ifNotNullNorEmpty
import chat.rocket.android.util.extensions.isNotNullNorEmpty import chat.rocket.android.util.extensions.isNotNullNorEmpty
...@@ -125,8 +126,13 @@ class UiModelMapper @Inject constructor( ...@@ -125,8 +126,13 @@ class UiModelMapper @Inject constructor(
getChatRoomAsync(message.roomId)?.let { chatRoom -> getChatRoomAsync(message.roomId)?.let { chatRoom ->
message.urls?.forEach { url -> message.urls?.forEach { url ->
if (url.url.isImage()) {
val attachment = Attachment(imageUrl = url.url)
mapAttachment(message, attachment, chatRoom)?.let { list.add(it) }
} else {
mapUrl(message, url, chatRoom)?.let { list.add(it) } mapUrl(message, url, chatRoom)?.let { list.add(it) }
} }
}
message.attachments?.mapNotNull { attachment -> message.attachments?.mapNotNull { attachment ->
mapAttachment(message, attachment, chatRoom) mapAttachment(message, attachment, chatRoom)
...@@ -162,7 +168,7 @@ class UiModelMapper @Inject constructor( ...@@ -162,7 +168,7 @@ class UiModelMapper @Inject constructor(
// TODO: move this to new interactor or FetchChatRoomsInteractor? // TODO: move this to new interactor or FetchChatRoomsInteractor?
private suspend fun getChatRoomAsync(roomId: String): ChatRoom? = withContext(CommonPool) { private suspend fun getChatRoomAsync(roomId: String): ChatRoom? = withContext(CommonPool) {
return@withContext dbManager.chatRoomDao().get(roomId)?.let { return@withContext dbManager.getRoom(id = roomId)?.let {
with(it.chatRoom) { with(it.chatRoom) {
ChatRoom( ChatRoom(
id = id, id = id,
...@@ -454,7 +460,8 @@ class UiModelMapper @Inject constructor( ...@@ -454,7 +460,8 @@ class UiModelMapper @Inject constructor(
messageId = message.id, avatar = avatar!!, time = time, senderName = sender, messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, currentDayMarkerText = dayMarkerText, content = content, isPinned = message.pinned, currentDayMarkerText = dayMarkerText,
showDayMarker = false, reactions = getReactions(message), isFirstUnread = false, showDayMarker = false, reactions = getReactions(message), isFirstUnread = false,
preview = preview, isTemporary = !synced, unread = unread, permalink = permalink) preview = preview, isTemporary = !synced, unread = unread, permalink = permalink,
subscriptionId = chatRoom.subscriptionId)
} }
private fun mapMessagePreview(message: Message): Message { private fun mapMessagePreview(message: Message): Message {
...@@ -469,7 +476,7 @@ class UiModelMapper @Inject constructor( ...@@ -469,7 +476,7 @@ class UiModelMapper @Inject constructor(
val list = mutableListOf<ReactionUiModel>() val list = mutableListOf<ReactionUiModel>()
val customEmojis = EmojiRepository.getCustomEmojis() val customEmojis = EmojiRepository.getCustomEmojis()
it.getShortNames().forEach { shortname -> it.getShortNames().forEach { shortname ->
val usernames = it.getUsernames(shortname) ?: emptyList() val usernames = it.getUsernames(shortname).orEmpty()
val count = usernames.size val count = usernames.size
val custom = customEmojis.firstOrNull { emoji -> emoji.shortname == shortname } val custom = customEmojis.firstOrNull { emoji -> emoji.shortname == shortname }
list.add( list.add(
...@@ -478,7 +485,8 @@ class UiModelMapper @Inject constructor( ...@@ -478,7 +485,8 @@ class UiModelMapper @Inject constructor(
unicode = EmojiParser.parse(context, shortname), unicode = EmojiParser.parse(context, shortname),
count = count, count = count,
usernames = usernames, usernames = usernames,
url = custom?.url) url = custom?.url,
isCustom = custom != null)
) )
} }
list list
......
...@@ -2,15 +2,11 @@ package chat.rocket.android.chatrooms.adapter ...@@ -2,15 +2,11 @@ package chat.rocket.android.chatrooms.adapter
import android.app.Application import android.app.Application
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.color
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.db.model.ChatRoom import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.checkIfMyself
import chat.rocket.android.server.domain.GetCurrentUserInteractor import chat.rocket.android.server.domain.GetCurrentUserInteractor
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.PublicSettings import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.showLastMessage import chat.rocket.android.server.domain.showLastMessage
import chat.rocket.android.server.domain.useRealName import chat.rocket.android.server.domain.useRealName
...@@ -30,20 +26,16 @@ class RoomUiModelMapper( ...@@ -30,20 +26,16 @@ class RoomUiModelMapper(
private val context: Application, private val context: Application,
private val settings: PublicSettings, private val settings: PublicSettings,
private val userInteractor: GetCurrentUserInteractor, private val userInteractor: GetCurrentUserInteractor,
private val serverUrl: String private val serverUrl: String,
private val permissions: PermissionsInteractor
) { ) {
private val nameUnreadColor = ContextCompat.getColor(context, R.color.colorPrimaryText) private val currentUser by lazy { userInteractor.get() }
private val nameColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
private val dateUnreadColor = ContextCompat.getColor(context, R.color.colorAccent)
private val dateColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
private val messageUnreadColor = ContextCompat.getColor(context, android.R.color.primary_text_light)
private val messageColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
private val currentUser by lazy { fun map(
userInteractor.get() rooms: List<ChatRoom>,
} grouped: Boolean = false,
showLastMessage: Boolean = true
fun map(rooms: List<ChatRoom>, grouped: Boolean = false, showLastMessage: Boolean = true): List<ItemHolder<*>> { ): List<ItemHolder<*>> {
val list = ArrayList<ItemHolder<*>>(rooms.size + 4) val list = ArrayList<ItemHolder<*>>(rooms.size + 4)
var lastType: String? = null var lastType: String? = null
rooms.forEach { room -> rooms.forEach { room ->
...@@ -72,7 +64,7 @@ class RoomUiModelMapper( ...@@ -72,7 +64,7 @@ class RoomUiModelMapper(
private fun mapUser(user: User): RoomUiModel { private fun mapUser(user: User): RoomUiModel {
return with(user) { return with(user) {
val name = mapName(user.username!!, user.name, false) val name = mapName(user.username!!, user.name)
val status = user.status val status = user.status
val avatar = serverUrl.avatarUrl(user.username!!) val avatar = serverUrl.avatarUrl(user.username!!)
val username = user.username!! val username = user.username!!
...@@ -88,39 +80,56 @@ class RoomUiModelMapper( ...@@ -88,39 +80,56 @@ class RoomUiModelMapper(
} }
} }
private fun mapRoom(room: Room, showLastMessage:Boolean = true): RoomUiModel { private fun mapRoom(room: Room, showLastMessage: Boolean = true): RoomUiModel {
return with(room) { return with(room) {
RoomUiModel( RoomUiModel(
id = id, id = id,
name = name!!, name = name!!,
type = type, type = type,
avatar = serverUrl.avatarUrl(name!!, isGroupOrChannel = true), avatar = serverUrl.avatarUrl(name!!, isGroupOrChannel = true),
lastMessage = if(showLastMessage) { mapLastMessage(lastMessage?.sender?.id, lastMessage?.sender?.username, lastMessage = if (showLastMessage) {
mapLastMessage(
lastMessage?.sender?.id, lastMessage?.sender?.username,
lastMessage?.sender?.name, lastMessage?.message, lastMessage?.sender?.name, lastMessage?.message,
isDirectMessage = type is RoomType.DirectMessage)} else { null } isDirectMessage = type is RoomType.DirectMessage
)
} else {
null
},
muted = muted.orEmpty(),
writable = isChannelWritable(muted)
) )
} }
} }
fun map(chatRoom: ChatRoom, showLastMessage:Boolean = true): RoomUiModel { fun map(chatRoom: ChatRoom, showLastMessage: Boolean = true): RoomUiModel {
return with(chatRoom.chatRoom) { return with(chatRoom.chatRoom) {
val isUnread = alert || unread > 0 val isUnread = alert || unread > 0
val type = roomTypeOf(type) val type = roomTypeOf(type)
val status = chatRoom.status?.let { userStatusOf(it) } val status = chatRoom.status?.let { userStatusOf(it) }
val roomName = mapName(name, fullname, isUnread) val roomName = mapName(name, fullname)
val timestamp = mapDate(lastMessageTimestamp ?: updatedAt, isUnread) val timestamp = mapDate(lastMessageTimestamp ?: updatedAt)
val avatar = if (type is RoomType.DirectMessage) { val avatar = if (type is RoomType.DirectMessage) {
serverUrl.avatarUrl(name) serverUrl.avatarUrl(name)
} else { } else {
serverUrl.avatarUrl(name, isGroupOrChannel = true) serverUrl.avatarUrl(name, isGroupOrChannel = true)
} }
val unread = mapUnread(unread) val unread = mapUnread(unread)
val lastMessage = if(showLastMessage) { mapLastMessage(lastMessageUserId, chatRoom.lastMessageUserName, val lastMessage = if (showLastMessage) {
chatRoom.lastMessageUserFullName, lastMessageText, isUnread, mapLastMessage(
type is RoomType.DirectMessage) } else { null } lastMessageUserId,
chatRoom.lastMessageUserName,
chatRoom.lastMessageUserFullName,
lastMessageText,
type is RoomType.DirectMessage
)
} else {
null
}
val hasMentions = mapMentions(userMentions, groupMentions)
val open = open val open = open
val lastMessageMarkdown =
val lastMessageMarkdown = lastMessage?.let { Markwon.markdown(context, it.toString()).toString() } lastMessage?.let { Markwon.markdown(context, it.toString()).toString() }
RoomUiModel( RoomUiModel(
id = id, id = id,
...@@ -130,14 +139,22 @@ class RoomUiModelMapper( ...@@ -130,14 +139,22 @@ class RoomUiModelMapper(
open = open, open = open,
date = timestamp, date = timestamp,
unread = unread, unread = unread,
mentions = hasMentions,
alert = isUnread, alert = isUnread,
lastMessage = lastMessageMarkdown, lastMessage = lastMessageMarkdown,
status = status, status = status,
username = if (type is RoomType.DirectMessage) name else null username = if (type is RoomType.DirectMessage) name else null,
muted = muted.orEmpty(),
writable = isChannelWritable(muted)
) )
} }
} }
private fun isChannelWritable(muted: List<String>?): Boolean {
val canWriteToReadOnlyChannels = permissions.canPostToReadOnlyChannels()
return canWriteToReadOnlyChannels || !muted.orEmpty().contains(currentUser?.username)
}
private fun roomType(type: String): String { private fun roomType(type: String): String {
val resources = context.resources val resources = context.resources
return when (type) { return when (type) {
...@@ -149,47 +166,37 @@ class RoomUiModelMapper( ...@@ -149,47 +166,37 @@ class RoomUiModelMapper(
} }
} }
private fun mapLastMessage(userId: String?, name: String?, fullName: String?, text: String?, private fun mapLastMessage(
unread: Boolean = false, userId: String?,
isDirectMessage: Boolean = false): CharSequence? { name: String?,
fullName: String?,
text: String?,
isDirectMessage: Boolean = false
): CharSequence? {
return if (!settings.showLastMessage()) { return if (!settings.showLastMessage()) {
null null
} else if (name != null && text != null) { } else if (name != null && text != null) {
val user = if (currentUser != null && currentUser!!.id == userId) { val user = if (currentUser != null && currentUser!!.id == userId) {
"${context.getString(R.string.msg_you)}: " "${context.getString(R.string.msg_you)}: "
} else { } else {
if (isDirectMessage) "" else "${mapName(name, fullName, unread)}: " if (isDirectMessage) "" else "${mapName(name, fullName)}: "
}
val color = if (unread) messageUnreadColor else messageColor
SpannableStringBuilder()
.color(color) {
bold { append(user) }
append(text)
} }
SpannableStringBuilder().append(user).append(text)
} else { } else {
context.getText(R.string.msg_no_messages_yet) context.getText(R.string.msg_no_messages_yet)
} }
} }
private fun mapName(name: String, fullName: String?, unread: Boolean): CharSequence { private fun mapName(name: String, fullName: String?): CharSequence {
val roomName = if (settings.useSpecialCharsOnRoom() || settings.useRealName()) { return if (settings.useSpecialCharsOnRoom() || settings.useRealName()) {
fullName ?: name fullName ?: name
} else { } else {
name name
} }
val color = if (unread) nameUnreadColor else nameColor
return SpannableStringBuilder()
.color(color) {
append(roomName)
}
} }
private fun mapUnread(unread: Long): String? { private fun mapUnread(unread: Long): String? {
return when(unread) { return when (unread) {
0L -> null 0L -> null
in 1..99 -> unread.toString() in 1..99 -> unread.toString()
else -> context.getString(R.string.msg_more_than_ninety_nine_unread_messages) else -> context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
...@@ -197,12 +204,14 @@ class RoomUiModelMapper( ...@@ -197,12 +204,14 @@ class RoomUiModelMapper(
} }
} }
private fun mapDate(date: Long?, unread: Boolean): CharSequence? { private fun mapMentions(userMentions: Long?, groupMentions: Long?): Boolean {
return date?.localDateTime()?.date(context)?.let { if (userMentions != null && groupMentions != null) {
val color = if (unread) dateUnreadColor else dateColor if (userMentions > 0 || groupMentions > 0) {
SpannableStringBuilder().color(color) { return true
append(it)
} }
} }
return false
} }
private fun mapDate(date: Long?): CharSequence? = date?.localDateTime()?.date(context)
} }
...@@ -4,6 +4,7 @@ import android.content.res.Resources ...@@ -4,6 +4,7 @@ import android.content.res.Resources
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.View import android.view.View
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
...@@ -11,19 +12,16 @@ import chat.rocket.common.model.RoomType ...@@ -11,19 +12,16 @@ import chat.rocket.common.model.RoomType
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
import kotlinx.android.synthetic.main.item_chat.view.* import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.* import kotlinx.android.synthetic.main.unread_messages_badge.view.*
import ru.noties.markwon.Markwon
class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit) : ViewHolder<RoomItemHolder>(itemView) {
class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit) :
ViewHolder<RoomItemHolder>(itemView) {
private val resources: Resources = itemView.resources private val resources: Resources = itemView.resources
private val channelUnread: Drawable = resources.getDrawable(R.drawable.ic_hashtag_black_12dp) private val channelIcon: Drawable = resources.getDrawable(R.drawable.ic_hashtag_12dp)
private val channel: Drawable = resources.getDrawable(R.drawable.ic_hashtag_12dp) private val groupIcon: Drawable = resources.getDrawable(R.drawable.ic_lock_12_dp)
private val groupUnread: Drawable = resources.getDrawable(R.drawable.ic_lock_black_12_dp) private val onlineIcon: Drawable = resources.getDrawable(R.drawable.ic_status_online_12dp)
private val group: Drawable = resources.getDrawable(R.drawable.ic_lock_12_dp) private val awayIcon: Drawable = resources.getDrawable(R.drawable.ic_status_away_12dp)
private val online: Drawable = resources.getDrawable(R.drawable.ic_status_online_12dp) private val busyIcon: Drawable = resources.getDrawable(R.drawable.ic_status_busy_12dp)
private val away: Drawable = resources.getDrawable(R.drawable.ic_status_away_12dp) private val offlineIcon: Drawable = resources.getDrawable(R.drawable.ic_status_invisible_12dp)
private val busy: Drawable = resources.getDrawable(R.drawable.ic_status_busy_12dp)
private val offline: Drawable = resources.getDrawable(R.drawable.ic_status_invisible_12dp)
override fun bindViews(data: RoomItemHolder) { override fun bindViews(data: RoomItemHolder) {
val room = data.data val room = data.data
...@@ -31,53 +29,59 @@ class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit ...@@ -31,53 +29,59 @@ class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit
image_avatar.setImageURI(room.avatar) image_avatar.setImageURI(room.avatar)
text_chat_name.text = room.name text_chat_name.text = room.name
if (room.status != null && room.type is RoomType.DirectMessage) {
image_chat_icon.setImageDrawable(getStatusDrawable(room.status))
} else {
image_chat_icon.setImageDrawable(getRoomDrawable(room.type))
}
if (room.lastMessage != null) { if (room.lastMessage != null) {
text_last_message.isVisible = true
text_last_message.text = room.lastMessage text_last_message.text = room.lastMessage
text_last_message.isVisible = true
} else { } else {
text_last_message.isGone = true text_last_message.isGone = true
} }
if (room.date != null) { if (room.date != null) {
text_last_message_date_time.isVisible = true text_timestamp.text = room.date
text_last_message_date_time.text = room.date text_timestamp.isVisible = true
} else { } else {
text_last_message_date_time.isGone = true text_timestamp.isInvisible = true
} }
if (room.unread != null) { if (room.alert) {
if (room.unread == null) text_total_unread_messages.text = "!"
if (room.unread != null) text_total_unread_messages.text = room.unread
if (room.mentions) text_total_unread_messages.text = "@${room.unread}"
text_chat_name.setTextAppearance(context, R.style.ChatList_ChatName_Unread_TextView)
text_timestamp.setTextAppearance(context, R.style.ChatList_Timestamp_Unread_TextView)
text_last_message.setTextAppearance(context, R.style.ChatList_LastMessage_Unread_TextView)
text_total_unread_messages.isVisible = true text_total_unread_messages.isVisible = true
text_total_unread_messages.text = room.unread
} else { } else {
text_total_unread_messages.isGone = true text_chat_name.setTextAppearance(context, R.style.ChatList_ChatName_TextView)
text_timestamp.setTextAppearance(context, R.style.ChatList_Timestamp_TextView)
text_last_message.setTextAppearance(context, R.style.ChatList_LastMessage_TextView)
text_total_unread_messages.isInvisible = true
} }
if (room.status != null && room.type is RoomType.DirectMessage) { setOnClickListener { listener(room) }
image_chat_icon.setImageDrawable(getStatusDrawable(room.status))
} else {
image_chat_icon.setImageDrawable(getRoomDrawable(room.type, room.alert))
}
setOnClickListener {
listener(room)
}
} }
} }
private fun getRoomDrawable(type: RoomType, alert: Boolean): Drawable? { private fun getRoomDrawable(type: RoomType): Drawable? {
return when(type) { return when (type) {
is RoomType.Channel -> if (alert) channelUnread else channel is RoomType.Channel -> channelIcon
is RoomType.PrivateGroup -> if (alert) groupUnread else group is RoomType.PrivateGroup -> groupIcon
else -> null else -> null
} }
} }
private fun getStatusDrawable(status: UserStatus): Drawable { private fun getStatusDrawable(status: UserStatus): Drawable {
return when(status) { return when (status) {
is UserStatus.Online -> online is UserStatus.Online -> onlineIcon
is UserStatus.Away -> away is UserStatus.Away -> awayIcon
is UserStatus.Busy -> busy is UserStatus.Busy -> busyIcon
else -> offline else -> offlineIcon
} }
} }
} }
\ No newline at end of file
...@@ -6,7 +6,8 @@ import chat.rocket.android.R ...@@ -6,7 +6,8 @@ import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) : RecyclerView.Adapter<ViewHolder<*>>() { class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) :
RecyclerView.Adapter<ViewHolder<*>>() {
init { init {
setHasStableIds(true) setHasStableIds(true)
...@@ -40,16 +41,16 @@ class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) : RecyclerView.A ...@@ -40,16 +41,16 @@ class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) : RecyclerView.A
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
val item = values[position] val item = values[position]
return when(item) { return when (item) {
is HeaderItemHolder -> item.data.hashCode().toLong()
is RoomItemHolder -> item.data.id.hashCode().toLong() is RoomItemHolder -> item.data.id.hashCode().toLong()
is HeaderItemHolder -> item.data.hashCode().toLong()
is LoadingItemHolder -> "loading".hashCode().toLong() is LoadingItemHolder -> "loading".hashCode().toLong()
else -> throw IllegalStateException("View type must be either Room, Header or Loading") else -> throw IllegalStateException("View type must be either Room, Header or Loading")
} }
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return when(values[position]) { return when (values[position]) {
is RoomItemHolder -> VIEW_TYPE_ROOM is RoomItemHolder -> VIEW_TYPE_ROOM
is HeaderItemHolder -> VIEW_TYPE_HEADER is HeaderItemHolder -> VIEW_TYPE_HEADER
is LoadingItemHolder -> VIEW_TYPE_LOADING is LoadingItemHolder -> VIEW_TYPE_LOADING
......
...@@ -12,7 +12,12 @@ data class RoomUiModel( ...@@ -12,7 +12,12 @@ data class RoomUiModel(
val date: CharSequence? = null, val date: CharSequence? = null,
val unread: String? = null, val unread: String? = null,
val alert: Boolean = false, val alert: Boolean = false,
val mentions: Boolean = false,
val lastMessage: CharSequence? = null, val lastMessage: CharSequence? = null,
val status: UserStatus? = null, val status: UserStatus? = null,
val username: String? = null val username: String? = null,
val broadcast: Boolean = false,
val canModerate: Boolean = false,
val writable: Boolean = true,
val muted: List<String> = emptyList()
) )
...@@ -12,6 +12,7 @@ import chat.rocket.android.db.DatabaseManager ...@@ -12,6 +12,7 @@ import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.UserDao import chat.rocket.android.db.UserDao
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentUserInteractor import chat.rocket.android.server.domain.GetCurrentUserInteractor
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.PublicSettings import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository import chat.rocket.android.server.domain.TokenRepository
...@@ -88,9 +89,10 @@ class ChatRoomsFragmentModule { ...@@ -88,9 +89,10 @@ class ChatRoomsFragmentModule {
context: Application, context: Application,
repository: SettingsRepository, repository: SettingsRepository,
userInteractor: GetCurrentUserInteractor, userInteractor: GetCurrentUserInteractor,
@Named("currentServer") serverUrl: String @Named("currentServer") serverUrl: String,
permissionsInteractor: PermissionsInteractor
): RoomUiModelMapper { ): RoomUiModelMapper {
return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl) return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl, permissionsInteractor)
} }
@Provides @Provides
......
...@@ -3,9 +3,12 @@ package chat.rocket.android.chatrooms.infrastructure ...@@ -3,9 +3,12 @@ package chat.rocket.android.chatrooms.infrastructure
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import chat.rocket.android.db.ChatRoomDao import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.model.ChatRoom import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.util.retryDB
import javax.inject.Inject import javax.inject.Inject
class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao){ class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao) {
// TODO - check how to use retryDB here - suspend
fun getChatRooms(order: Order): LiveData<List<ChatRoom>> { fun getChatRooms(order: Order): LiveData<List<ChatRoom>> {
return when(order) { return when(order) {
Order.ACTIVITY -> dao.getAll() Order.ACTIVITY -> dao.getAll()
...@@ -15,9 +18,10 @@ class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao){ ...@@ -15,9 +18,10 @@ class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao){
} }
} }
fun search(query: String) = dao.searchSync(query) suspend fun search(query: String) =
retryDB("roomSearch($query)") { dao.searchSync(query) }
fun count() = dao.count() suspend fun count() = retryDB("roomsCount") { dao.count() }
enum class Order { enum class Order {
ACTIVITY, ACTIVITY,
......
...@@ -13,6 +13,7 @@ import chat.rocket.android.server.domain.useRealName ...@@ -13,6 +13,7 @@ import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.domain.useSpecialCharsOnRoom import chat.rocket.android.server.domain.useSpecialCharsOnRoom
import chat.rocket.android.server.infraestructure.ConnectionManager import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.util.extension.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
...@@ -41,11 +42,31 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -41,11 +42,31 @@ class ChatRoomsPresenter @Inject constructor(
private val client = manager.client private val client = manager.client
private val settings = settingsRepository.get(currentServer) private val settings = settingsRepository.get(currentServer)
fun loadChatRoom(roomId: String) {
launchUI(strategy) {
view.showLoadingRoom("")
try {
val room = dbManager.getRoom(roomId)
if (room != null) {
loadChatRoom(room.chatRoom, true)
} else {
Timber.d("Error loading channel")
view.showGenericErrorMessage()
}
} catch (ex: Exception) {
Timber.d(ex, "Error loading channel")
view.showGenericErrorMessage()
} finally {
view.hideLoadingRoom()
}
}
}
fun loadChatRoom(chatRoom: RoomUiModel) { fun loadChatRoom(chatRoom: RoomUiModel) {
launchUI(strategy) { launchUI(strategy) {
view.showLoadingRoom(chatRoom.name) view.showLoadingRoom(chatRoom.name)
try { try {
val room = dbManager.getRoom(chatRoom.id) val room = retryDB("getRoom(${chatRoom.id}") { dbManager.getRoom(chatRoom.id) }
if (room != null) { if (room != null) {
loadChatRoom(room.chatRoom, true) loadChatRoom(room.chatRoom, true)
} else { } else {
...@@ -56,7 +77,8 @@ class ChatRoomsPresenter @Inject constructor( ...@@ -56,7 +77,8 @@ class ChatRoomsPresenter @Inject constructor(
type = type.toString(), type = type.toString(),
name = username ?: name.toString(), name = username ?: name.toString(),
fullname = name.toString(), fullname = name.toString(),
open = open open = open,
muted = muted
) )
loadChatRoom(entity, false) loadChatRoom(entity, false)
} }
......
package chat.rocket.android.chatrooms.ui
import DateTimeHelper
import DrawableHelper
import android.content.Context
import android.graphics.Color
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.checkIfMyself
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.showLastMessage
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.domain.useSpecialCharsOnRoom
import chat.rocket.android.util.extensions.*
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class ChatRoomsAdapter(
private val context: Context,
private val settings: PublicSettings,
private val localRepository: LocalRepository,
private val listener: (ChatRoom) -> Unit
) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() {
var dataSet: MutableList<ChatRoom> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = ViewHolder(parent.inflate(R.layout.item_chat))
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(dataSet[position])
override fun getItemCount(): Int = dataSet.size
fun updateRooms(newRooms: List<ChatRoom>) {
dataSet.clear()
dataSet.addAll(newRooms)
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(chatRoom: ChatRoom) = with(itemView) {
bindAvatar(chatRoom, image_avatar)
bindName(chatRoom, text_chat_name)
bindIcon(chatRoom, image_chat_icon)
if (settings.showLastMessage()) {
text_last_message.isVisible = true
text_last_message_date_time.isVisible = true
bindLastMessageDateTime(chatRoom, text_last_message_date_time)
bindLastMessage(chatRoom, text_last_message)
} else {
text_last_message.isVisible = false
text_last_message_date_time.isVisible = false
}
bindUnreadMessages(chatRoom, text_total_unread_messages)
if (chatRoom.alert || chatRoom.unread > 0) {
text_chat_name.setTextColor(ContextCompat.getColor(context,
R.color.colorPrimaryText))
text_last_message_date_time.setTextColor(ContextCompat.getColor(context,
R.color.colorAccent))
text_last_message.setTextColor(ContextCompat.getColor(context,
android.R.color.primary_text_light))
} else {
text_chat_name.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
text_last_message_date_time.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
text_last_message.setTextColor(ContextCompat.getColor(context,
R.color.colorSecondaryText))
}
setOnClickListener { listener(chatRoom) }
}
private fun bindAvatar(chatRoom: ChatRoom, drawee: SimpleDraweeView) {
if (chatRoom.type is RoomType.DirectMessage) {
drawee.setImageURI(chatRoom.client.url.avatarUrl(chatRoom.name))
} else {
drawee.setImageURI(chatRoom.client.url.avatarUrl(chatRoom.name, true))
}
}
private fun bindIcon(chatRoom: ChatRoom, imageView: ImageView) {
val drawable = when (chatRoom.type) {
is RoomType.Channel -> DrawableHelper.getDrawableFromId(
R.drawable.ic_hashtag_black_12dp,
context
)
is RoomType.PrivateGroup -> DrawableHelper.getDrawableFromId(
R.drawable.ic_lock_black_12_dp,
context
)
is RoomType.DirectMessage -> DrawableHelper.getUserStatusDrawable(
chatRoom.status,
context
)
else -> null
}
drawable?.let {
val mutateDrawable = DrawableHelper.wrapDrawable(it).mutate()
if (chatRoom.type !is RoomType.DirectMessage) {
val color = when (chatRoom.alert || chatRoom.unread > 0) {
true -> R.color.colorPrimaryText
false -> R.color.colorSecondaryText
}
DrawableHelper.tintDrawable(mutateDrawable, context, color)
}
imageView.setImageDrawable(mutateDrawable)
}
}
private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.textContent = chatRoomName(chatRoom)
}
private fun chatRoomName(chatRoom: ChatRoom): String {
return if (settings.useSpecialCharsOnRoom() || settings.useRealName()) {
chatRoom.fullName ?: chatRoom.name
} else {
chatRoom.name
}
}
private fun bindLastMessageDateTime(chatRoom: ChatRoom, textView: TextView) {
val lastMessage = chatRoom.lastMessage
if (lastMessage != null) {
val localDateTime = DateTimeHelper.getLocalDateTime(lastMessage.timestamp)
textView.content = DateTimeHelper.getDate(localDateTime, context)
} else {
textView.content = ""
}
}
private fun bindLastMessage(chatRoom: ChatRoom, textView: TextView) {
val lastMessage = chatRoom.lastMessage
val lastMessageSender = lastMessage?.sender
if (lastMessage != null && lastMessageSender != null) {
val message = lastMessage.message
val senderUsername = if (settings.useRealName()) {
lastMessageSender.name ?: lastMessageSender.username
} else {
lastMessageSender.username
}
when (senderUsername) {
chatRoom.name -> {
textView.content = message
}
else -> {
val user = if (localRepository.checkIfMyself(lastMessageSender.username!!)) {
"${context.getString(R.string.msg_you)}: "
} else {
"$senderUsername: "
}
val spannable = SpannableStringBuilder(user)
val len = spannable.length
spannable.setSpan(ForegroundColorSpan(Color.BLACK), 0, len - 1, 0)
spannable.append(message)
textView.content = spannable
}
}
} else {
textView.content = context.getText(R.string.msg_no_messages_yet)
}
}
private fun bindUnreadMessages(chatRoom: ChatRoom, textView: TextView) {
val totalUnreadMessage = chatRoom.unread
when {
totalUnreadMessage in 1..99 -> {
textView.textContent = totalUnreadMessage.toString()
textView.isVisible = true
}
totalUnreadMessage > 99 -> {
textView.textContent = context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
textView.isVisible = true
}
else -> textView.isVisible = false
}
}
}
}
\ No newline at end of file
...@@ -36,6 +36,8 @@ import chat.rocket.android.helper.SharedPreferenceHelper ...@@ -36,6 +36,8 @@ import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.util.extension.onQueryTextListener import chat.rocket.android.util.extension.onQueryTextListener
import chat.rocket.android.util.extensions.fadeIn import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.ifNotNullNorEmpty
import chat.rocket.android.util.extensions.ifNotNullNotEmpty
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui import chat.rocket.android.util.extensions.ui
...@@ -83,9 +85,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -83,9 +85,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
val bundle = arguments val bundle = arguments
if (bundle != null) { if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID) chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomId?.let { chatRoomId.ifNotNullNotEmpty { roomId ->
// TODO - bring back support to load a room from id. presenter.loadChatRoom(roomId)
//presenter.goToChatRoomWithId(it)
chatRoomId = null chatRoomId = null
} }
} }
...@@ -238,7 +239,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -238,7 +239,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
AlertDialog.Builder(context) AlertDialog.Builder(context)
.setTitle(R.string.dialog_sort_title) .setTitle(R.string.dialog_sort_title)
.setView(dialogLayout) .setView(dialogLayout)
.setPositiveButton(R.string.dialog_button_done) { dialog, _ -> .setPositiveButton(R.string.msg_sort) { dialog, _ ->
invalidateQueryOnSearch() invalidateQueryOnSearch()
updateSort() updateSort()
dialog.dismiss() dialog.dismiss()
......
package chat.rocket.android.chatrooms.ui
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import java.util.*
class SimpleSectionedRecyclerViewAdapter(private val context: Context, private val sectionResourceId: Int, private val textResourceId: Int,
val baseAdapter: ChatRoomsAdapter) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var isValid = true
private val sectionsHeaders = SparseArray<Section>()
init {
baseAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
isValid = baseAdapter.itemCount > 0
notifyDataSetChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
isValid = baseAdapter.itemCount > 0
notifyItemRangeChanged(positionStart, itemCount)
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
isValid = baseAdapter.itemCount > 0
notifyItemRangeInserted(positionStart, itemCount)
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
isValid = baseAdapter.itemCount > 0
notifyItemRangeRemoved(positionStart, itemCount)
}
})
}
class SectionViewHolder(view: View, textResourceId: Int) : RecyclerView.ViewHolder(view) {
var title: TextView = view.findViewById<View>(textResourceId) as TextView
}
override fun onCreateViewHolder(parent: ViewGroup, typeView: Int): RecyclerView.ViewHolder {
return if (typeView == SECTION_TYPE) {
val view = LayoutInflater.from(context).inflate(sectionResourceId, parent, false)
SectionViewHolder(view, textResourceId)
} else {
baseAdapter.onCreateViewHolder(parent, typeView - 1)
}
}
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
if (isSectionHeaderPosition(position)) {
(viewHolder as SectionViewHolder).title.text = sectionsHeaders.get(position).title
} else {
baseAdapter.onBindViewHolder(viewHolder as ChatRoomsAdapter.ViewHolder, sectionedPositionToPosition(position))
}
}
override fun getItemViewType(position: Int): Int {
return if (isSectionHeaderPosition(position))
SECTION_TYPE
else
baseAdapter.getItemViewType(sectionedPositionToPosition(position)) + 1
}
class Section(internal var firstPosition: Int, var title: CharSequence) {
internal var sectionedPosition: Int = 0
}
fun setSections(sections: Array<Section>) {
sectionsHeaders.clear()
Arrays.sort(sections) { section1, section2 ->
when {
section1.firstPosition == section2.firstPosition -> 0
section1.firstPosition < section2.firstPosition -> -1
else -> 1
}
}
for ((offset, section) in sections.withIndex()) {
section.sectionedPosition = section.firstPosition + offset
sectionsHeaders.append(section.sectionedPosition, section)
}
notifyDataSetChanged()
}
fun clearSections(){
sectionsHeaders.clear()
notifyDataSetChanged()
}
fun positionToSectionedPosition(position: Int): Int {
var offset = 0
for (i in 0 until sectionsHeaders.size()) {
if (sectionsHeaders.valueAt(i).firstPosition > position) {
break
}
++offset
}
return position + offset
}
private fun sectionedPositionToPosition(sectionedPosition: Int): Int {
if (isSectionHeaderPosition(sectionedPosition)) {
return RecyclerView.NO_POSITION
}
var offset = 0
for (i in 0 until sectionsHeaders.size()) {
if (sectionsHeaders.valueAt(i).sectionedPosition > sectionedPosition) {
break
}
--offset
}
return sectionedPosition + offset
}
private fun isSectionHeaderPosition(position: Int): Boolean {
return sectionsHeaders.get(position) != null
}
override fun getItemId(position: Int): Long {
return when (isSectionHeaderPosition(position)) {
true -> (Integer.MAX_VALUE - sectionsHeaders.indexOfKey(position)).toLong()
false -> baseAdapter.getItemId(sectionedPositionToPosition(position))
}
}
override fun getItemCount(): Int {
return if (isValid) baseAdapter.itemCount + sectionsHeaders.size() else 0
}
companion object {
private const val SECTION_TYPE = 0
}
}
...@@ -27,7 +27,7 @@ import kotlinx.coroutines.experimental.launch ...@@ -27,7 +27,7 @@ import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
import timber.log.Timber import timber.log.Timber
import java.security.InvalidParameterException import java.lang.IllegalArgumentException
import kotlin.coroutines.experimental.coroutineContext import kotlin.coroutines.experimental.coroutineContext
...@@ -171,6 +171,6 @@ fun Query.asSortingOrder(): ChatRoomsRepository.Order { ...@@ -171,6 +171,6 @@ fun Query.asSortingOrder(): ChatRoomsRepository.Order {
ChatRoomsRepository.Order.ACTIVITY ChatRoomsRepository.Order.ACTIVITY
} }
} }
else -> throw InvalidParameterException("Should be ByName or ByActivity") else -> throw IllegalArgumentException("Should be ByName or ByActivity")
} }
} }
...@@ -11,6 +11,7 @@ import chat.rocket.android.authentication.server.di.ServerFragmentProvider ...@@ -11,6 +11,7 @@ import chat.rocket.android.authentication.server.di.ServerFragmentProvider
import chat.rocket.android.authentication.signup.di.SignupFragmentProvider import chat.rocket.android.authentication.signup.di.SignupFragmentProvider
import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatdetails.di.ChatDetailsFragmentProvider
import chat.rocket.android.chatinformation.di.MessageInfoFragmentProvider import chat.rocket.android.chatinformation.di.MessageInfoFragmentProvider
import chat.rocket.android.chatinformation.ui.MessageInfoActivity import chat.rocket.android.chatinformation.ui.MessageInfoActivity
import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider
...@@ -35,6 +36,7 @@ import chat.rocket.android.server.ui.ChangeServerActivity ...@@ -35,6 +36,7 @@ import chat.rocket.android.server.ui.ChangeServerActivity
import chat.rocket.android.settings.di.SettingsFragmentProvider import chat.rocket.android.settings.di.SettingsFragmentProvider
import chat.rocket.android.settings.password.di.PasswordFragmentProvider import chat.rocket.android.settings.password.di.PasswordFragmentProvider
import chat.rocket.android.settings.password.ui.PasswordActivity import chat.rocket.android.settings.password.ui.PasswordActivity
import chat.rocket.android.userdetails.di.UserDetailsFragmentProvider
import chat.rocket.android.webview.adminpanel.di.AdminPanelWebViewFragmentProvider import chat.rocket.android.webview.adminpanel.di.AdminPanelWebViewFragmentProvider
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
...@@ -73,9 +75,10 @@ abstract class ActivityBuilder { ...@@ -73,9 +75,10 @@ abstract class ActivityBuilder {
@PerActivity @PerActivity
@ContributesAndroidInjector( @ContributesAndroidInjector(
modules = [ modules = [ChatRoomModule::class,
ChatRoomModule::class,
ChatRoomFragmentProvider::class, ChatRoomFragmentProvider::class,
UserDetailsFragmentProvider::class,
ChatDetailsFragmentProvider::class,
MembersFragmentProvider::class, MembersFragmentProvider::class,
MentionsFragmentProvider::class, MentionsFragmentProvider::class,
PinnedMessagesFragmentProvider::class, PinnedMessagesFragmentProvider::class,
...@@ -96,6 +99,8 @@ abstract class ActivityBuilder { ...@@ -96,6 +99,8 @@ abstract class ActivityBuilder {
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [MessageInfoFragmentProvider::class]) @ContributesAndroidInjector(modules = [MessageInfoFragmentProvider::class])
abstract fun bindMessageInfoActiviy(): MessageInfoActivity abstract fun bindMessageInfoActiviy(): MessageInfoActivity
@PerActivity
@ContributesAndroidInjector(modules = [DrawModule::class]) @ContributesAndroidInjector(modules = [DrawModule::class])
abstract fun bindDrawingActivity(): DrawingActivity abstract fun bindDrawingActivity(): DrawingActivity
} }
...@@ -64,6 +64,7 @@ import chat.rocket.common.internal.ISO8601Date ...@@ -64,6 +64,7 @@ import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger import chat.rocket.common.util.Logger
import chat.rocket.common.util.NoOpLogger
import chat.rocket.common.util.PlatformLogger import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.internal.AttachmentAdapterFactory import chat.rocket.core.internal.AttachmentAdapterFactory
import chat.rocket.core.internal.ReactionsAdapter import chat.rocket.core.internal.ReactionsAdapter
...@@ -231,7 +232,7 @@ class AppModule { ...@@ -231,7 +232,7 @@ class AppModule {
return Moshi.Builder() return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY) .add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE) .add(AppJsonAdapterFactory.INSTANCE)
.add(AttachmentAdapterFactory(Logger(logger, url))) .add(AttachmentAdapterFactory(NoOpLogger))
.add( .add(
java.lang.Long::class.java, java.lang.Long::class.java,
ISO8601Date::class.java, ISO8601Date::class.java,
......
...@@ -14,6 +14,7 @@ import chat.rocket.common.internal.ISO8601Date ...@@ -14,6 +14,7 @@ import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger import chat.rocket.common.util.Logger
import chat.rocket.common.util.NoOpLogger
import chat.rocket.common.util.PlatformLogger import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.internal.AttachmentAdapterFactory import chat.rocket.core.internal.AttachmentAdapterFactory
import chat.rocket.core.internal.ReactionsAdapter import chat.rocket.core.internal.ReactionsAdapter
...@@ -47,7 +48,7 @@ class LocalModule { ...@@ -47,7 +48,7 @@ class LocalModule {
return Moshi.Builder() return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY) .add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE) .add(AppJsonAdapterFactory.INSTANCE)
.add(AttachmentAdapterFactory(Logger(logger, url))) .add(AttachmentAdapterFactory(NoOpLogger))
.add( .add(
java.lang.Long::class.java, java.lang.Long::class.java,
ISO8601Date::class.java, ISO8601Date::class.java,
...@@ -67,8 +68,6 @@ class LocalModule { ...@@ -67,8 +68,6 @@ class LocalModule {
return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE) return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
} }
@Provides @Provides
@Singleton @Singleton
fun provideLocalRepository(sharedPreferences: SharedPreferences, moshi: Moshi): LocalRepository { fun provideLocalRepository(sharedPreferences: SharedPreferences, moshi: Moshi): LocalRepository {
......
...@@ -9,7 +9,6 @@ import androidx.room.Transaction ...@@ -9,7 +9,6 @@ import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import chat.rocket.android.db.model.ChatRoom import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.db.model.ChatRoomEntity import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.common.model.RoomType
@Dao @Dao
abstract class ChatRoomDao : BaseDao<ChatRoomEntity> { abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
...@@ -18,7 +17,14 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> { ...@@ -18,7 +17,14 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
$BASE_QUERY $BASE_QUERY
WHERE chatrooms.id = :id WHERE chatrooms.id = :id
""") """)
abstract fun get(id: String): ChatRoom? abstract fun get(id: String): LiveData<ChatRoom>
@Transaction
@Query("""
$BASE_QUERY
WHERE chatrooms.id = :id
""")
abstract fun getSync(id: String): ChatRoom?
@Transaction @Transaction
@Query("$BASE_QUERY $FILTER_NOT_OPENED") @Query("$BASE_QUERY $FILTER_NOT_OPENED")
......
...@@ -19,6 +19,7 @@ import chat.rocket.android.util.extensions.exhaustive ...@@ -19,6 +19,7 @@ import chat.rocket.android.util.extensions.exhaustive
import chat.rocket.android.util.extensions.removeTrailingSlash import chat.rocket.android.util.extensions.removeTrailingSlash
import chat.rocket.android.util.extensions.toEntity import chat.rocket.android.util.extensions.toEntity
import chat.rocket.android.util.extensions.userId import chat.rocket.android.util.extensions.userId
import chat.rocket.android.util.retryDB
import chat.rocket.common.model.BaseRoom import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.SimpleUser import chat.rocket.common.model.SimpleUser
...@@ -41,8 +42,7 @@ import timber.log.Timber ...@@ -41,8 +42,7 @@ import timber.log.Timber
import java.util.HashSet import java.util.HashSet
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class DatabaseManager(val context: Application, class DatabaseManager(val context: Application, val serverUrl: String) {
val serverUrl: String) {
private val database: RCDatabase = androidx.room.Room.databaseBuilder(context, private val database: RCDatabase = androidx.room.Room.databaseBuilder(context,
RCDatabase::class.java, serverUrl.databaseName()) RCDatabase::class.java, serverUrl.databaseName())
...@@ -92,12 +92,26 @@ class DatabaseManager(val context: Application, ...@@ -92,12 +92,26 @@ class DatabaseManager(val context: Application,
} }
} }
fun logout() { suspend fun logout() {
database.clearAllTables() retryDB("clearAllTables") { database.clearAllTables() }
} }
suspend fun getRoom(id: String) = withContext(dbManagerContext) { suspend fun getRoom(id: String) = withContext(dbManagerContext) {
chatRoomDao().get(id) retryDB("getRoom($id)") {
chatRoomDao().getSync(id)
}
}
suspend fun insertOrReplaceRoom(chatRoomEntity: ChatRoomEntity) {
retryDB("insertOrReplace($chatRoomEntity)") {
chatRoomDao().insertOrReplace(chatRoomEntity)
}
}
suspend fun getUser(id: String) = withContext(dbManagerContext) {
retryDB("getUser($id)") {
userDao().getUser(id)
}
} }
fun processUsersBatch(users: List<User>) { fun processUsersBatch(users: List<User>) {
...@@ -124,11 +138,11 @@ class DatabaseManager(val context: Application, ...@@ -124,11 +138,11 @@ class DatabaseManager(val context: Application,
val toInsert = ArrayList<ChatRoomEntity>(batch.size / 2) val toInsert = ArrayList<ChatRoomEntity>(batch.size / 2)
val toUpdate = ArrayList<ChatRoomEntity>(batch.size) val toUpdate = ArrayList<ChatRoomEntity>(batch.size)
batch.forEach { batch.forEach {
when(it.type) { when (it.type) {
is Type.Removed -> toRemove.add(removeChatRoom(it.data)) is Type.Removed -> toRemove.add(removeChatRoom(it.data))
is Type.Inserted -> insertChatRoom(it.data)?.let { toInsert.add(it) } is Type.Inserted -> insertChatRoom(it.data)?.let { toInsert.add(it) }
is Type.Updated -> { is Type.Updated -> {
when(it.data) { when (it.data) {
is Subscription -> updateSubs[(it.data as Subscription).roomId] = it.data as Subscription is Subscription -> updateSubs[(it.data as Subscription).roomId] = it.data as Subscription
is Room -> updateRooms[(it.data as Room).id] = it.data as Room is Room -> updateRooms[(it.data as Room).id] = it.data as Room
} }
...@@ -152,7 +166,7 @@ class DatabaseManager(val context: Application, ...@@ -152,7 +166,7 @@ class DatabaseManager(val context: Application,
fun updateSelfUser(myself: Myself) { fun updateSelfUser(myself: Myself) {
launch(dbManagerContext) { launch(dbManagerContext) {
val user = userDao().getUser(myself.id) val user = retryDB("getUser(${myself.id})") { userDao().getUser(myself.id) }
val entity = user?.copy( val entity = user?.copy(
name = myself.name ?: user.name, name = myself.name ?: user.name,
username = myself.username ?: user.username, username = myself.username ?: user.username,
...@@ -168,7 +182,7 @@ class DatabaseManager(val context: Application, ...@@ -168,7 +182,7 @@ class DatabaseManager(val context: Application,
fun processRooms(rooms: List<ChatRoom>) { fun processRooms(rooms: List<ChatRoom>) {
launch(dbManagerContext) { launch(dbManagerContext) {
val entities = rooms.map { mapChatRoom(it) } val entities = rooms.map { mapChatRoom(it) }
sendOperation(Operation.InsertRooms(entities)) sendOperation(Operation.CleanInsertRooms(entities))
} }
} }
...@@ -329,14 +343,14 @@ class DatabaseManager(val context: Application, ...@@ -329,14 +343,14 @@ class DatabaseManager(val context: Application,
} }
private fun removeChatRoom(data: BaseRoom): String { private fun removeChatRoom(data: BaseRoom): String {
return when(data) { return when (data) {
is Subscription -> data.roomId is Subscription -> data.roomId
else -> data.id else -> data.id
} }
} }
private suspend fun updateRoom(data: Room): ChatRoomEntity? { private suspend fun updateRoom(data: Room): ChatRoomEntity? {
return chatRoomDao().get(data.id)?.let { current -> return retryDB("getChatRoom(${data.id})") { chatRoomDao().getSync(data.id) }?.let { current ->
with(data) { with(data) {
val chatRoom = current.chatRoom val chatRoom = current.chatRoom
...@@ -349,9 +363,13 @@ class DatabaseManager(val context: Application, ...@@ -349,9 +363,13 @@ class DatabaseManager(val context: Application,
ownerId = user?.id ?: chatRoom.ownerId, ownerId = user?.id ?: chatRoom.ownerId,
readonly = readonly, readonly = readonly,
updatedAt = updatedAt ?: chatRoom.updatedAt, updatedAt = updatedAt ?: chatRoom.updatedAt,
topic = topic,
announcement = announcement,
description = description,
lastMessageText = mapLastMessageText(lastMessage), lastMessageText = mapLastMessageText(lastMessage),
lastMessageUserId = lastMessage?.sender?.id, lastMessageUserId = lastMessage?.sender?.id,
lastMessageTimestamp = lastMessage?.timestamp lastMessageTimestamp = lastMessage?.timestamp,
muted = muted ?: chatRoom.muted
) )
} }
} }
...@@ -373,7 +391,7 @@ class DatabaseManager(val context: Application, ...@@ -373,7 +391,7 @@ class DatabaseManager(val context: Application,
context.getString(R.string.msg_sent_attachment) context.getString(R.string.msg_sent_attachment)
private suspend fun updateSubscription(data: Subscription): ChatRoomEntity? { private suspend fun updateSubscription(data: Subscription): ChatRoomEntity? {
return chatRoomDao().get(data.roomId)?.let { current -> return retryDB("getRoom(${data.roomId}") { chatRoomDao().getSync(data.roomId) }?.let { current ->
with(data) { with(data) {
val userId = if (type is RoomType.DirectMessage) { val userId = if (type is RoomType.DirectMessage) {
...@@ -389,12 +407,16 @@ class DatabaseManager(val context: Application, ...@@ -389,12 +407,16 @@ class DatabaseManager(val context: Application,
id = roomId, id = roomId,
subscriptionId = id, subscriptionId = id,
type = type.toString(), type = type.toString(),
name = name ?: throw NullPointerException(), // this should be filtered on the SDK name = name
?: throw NullPointerException(), // this should be filtered on the SDK
fullname = fullName ?: chatRoom.fullname, fullname = fullName ?: chatRoom.fullname,
userId = userId ?: chatRoom.userId, userId = userId ?: chatRoom.userId,
readonly = readonly ?: chatRoom.readonly, readonly = readonly ?: chatRoom.readonly,
isDefault = isDefault, isDefault = isDefault,
favorite = isFavorite, favorite = isFavorite,
topic = chatRoom.topic,
announcement = chatRoom.announcement,
description = chatRoom.description,
open = open, open = open,
alert = alert, alert = alert,
unread = unread, unread = unread,
...@@ -402,14 +424,15 @@ class DatabaseManager(val context: Application, ...@@ -402,14 +424,15 @@ class DatabaseManager(val context: Application,
groupMentions = groupMentions ?: chatRoom.groupMentions, groupMentions = groupMentions ?: chatRoom.groupMentions,
updatedAt = updatedAt ?: chatRoom.updatedAt, updatedAt = updatedAt ?: chatRoom.updatedAt,
timestamp = timestamp ?: chatRoom.timestamp, timestamp = timestamp ?: chatRoom.timestamp,
lastSeen = lastSeen ?: chatRoom.lastSeen lastSeen = lastSeen ?: chatRoom.lastSeen,
muted = chatRoom.muted
) )
} }
} }
} }
private suspend fun insertChatRoom(data: BaseRoom): ChatRoomEntity? { private suspend fun insertChatRoom(data: BaseRoom): ChatRoomEntity? {
return when(data) { return when (data) {
is Room -> insertRoom(data) is Room -> insertRoom(data)
is Subscription -> insertSubscription(data) is Subscription -> insertSubscription(data)
else -> null else -> null
...@@ -451,13 +474,17 @@ class DatabaseManager(val context: Application, ...@@ -451,13 +474,17 @@ class DatabaseManager(val context: Application,
id = room.id, id = room.id,
subscriptionId = subscription.id, subscriptionId = subscription.id,
type = room.type.toString(), type = room.type.toString(),
name = room.name ?: subscription.name ?: throw NullPointerException(), // this should be filtered on the SDK name = room.name ?: subscription.name
?: throw NullPointerException(), // this should be filtered on the SDK
fullname = subscription.fullName ?: room.fullName, fullname = subscription.fullName ?: room.fullName,
userId = userId, userId = userId,
ownerId = room.user?.id, ownerId = room.user?.id,
readonly = subscription.readonly, readonly = subscription.readonly,
isDefault = subscription.isDefault, isDefault = subscription.isDefault,
favorite = subscription.isFavorite, favorite = subscription.isFavorite,
topic = room.topic,
announcement = room.announcement,
description = room.description,
open = subscription.open, open = subscription.open,
alert = subscription.alert, alert = subscription.alert,
unread = subscription.unread, unread = subscription.unread,
...@@ -495,6 +522,9 @@ class DatabaseManager(val context: Application, ...@@ -495,6 +522,9 @@ class DatabaseManager(val context: Application,
readonly = readonly, readonly = readonly,
isDefault = default, isDefault = default,
favorite = favorite, favorite = favorite,
topic = topic,
announcement = announcement,
description = description,
open = open, open = open,
alert = alert, alert = alert,
unread = unread, unread = unread,
...@@ -506,7 +536,8 @@ class DatabaseManager(val context: Application, ...@@ -506,7 +536,8 @@ class DatabaseManager(val context: Application,
lastMessageText = mapLastMessageText(lastMessage), lastMessageText = mapLastMessageText(lastMessage),
lastMessageUserId = lastMessage?.sender?.id, lastMessageUserId = lastMessage?.sender?.id,
lastMessageTimestamp = lastMessage?.timestamp, lastMessageTimestamp = lastMessage?.timestamp,
broadcast = broadcast broadcast = broadcast,
muted = room.muted
) )
} }
} }
...@@ -535,9 +566,11 @@ class DatabaseManager(val context: Application, ...@@ -535,9 +566,11 @@ class DatabaseManager(val context: Application,
} }
} }
private fun findUser(userId: String): String? = userDao().findUser(userId) private suspend fun findUser(userId: String): String? =
retryDB("findUser($userId)") { userDao().findUser(userId) }
private fun doOperation(operation: Operation) { private suspend fun doOperation(operation: Operation) {
retryDB(description = "doOperation($operation)") {
when (operation) { when (operation) {
is Operation.ClearStatus -> userDao().clearStatus() is Operation.ClearStatus -> userDao().clearStatus()
is Operation.UpdateRooms -> { is Operation.UpdateRooms -> {
...@@ -569,6 +602,7 @@ class DatabaseManager(val context: Application, ...@@ -569,6 +602,7 @@ class DatabaseManager(val context: Application,
} }
}.exhaustive }.exhaustive
} }
}
} }
sealed class Operation { sealed class Operation {
...@@ -579,6 +613,7 @@ sealed class Operation { ...@@ -579,6 +613,7 @@ sealed class Operation {
val toUpdate: List<ChatRoomEntity>, val toUpdate: List<ChatRoomEntity>,
val toRemove: List<String> val toRemove: List<String>
) : Operation() ) : Operation()
data class InsertRooms(val chatRooms: List<ChatRoomEntity>) : Operation() data class InsertRooms(val chatRooms: List<ChatRoomEntity>) : Operation()
data class CleanInsertRooms(val chatRooms: List<ChatRoomEntity>) : Operation() data class CleanInsertRooms(val chatRooms: List<ChatRoomEntity>) : Operation()
...@@ -609,7 +644,7 @@ private fun String.databaseName(): String { ...@@ -609,7 +644,7 @@ private fun String.databaseName(): String {
val tmp = this.removePrefix("https://") val tmp = this.removePrefix("https://")
.removePrefix("http://") .removePrefix("http://")
.removeTrailingSlash() .removeTrailingSlash()
.replace("/","-") .replace("/", "-")
.replace(".", "_") .replace(".", "_")
return "$tmp.db" return "$tmp.db"
......
...@@ -2,6 +2,7 @@ package chat.rocket.android.db ...@@ -2,6 +2,7 @@ package chat.rocket.android.db
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import chat.rocket.android.db.model.AttachmentActionEntity import chat.rocket.android.db.model.AttachmentActionEntity
import chat.rocket.android.db.model.AttachmentEntity import chat.rocket.android.db.model.AttachmentEntity
import chat.rocket.android.db.model.AttachmentFieldEntity import chat.rocket.android.db.model.AttachmentFieldEntity
...@@ -14,6 +15,7 @@ import chat.rocket.android.db.model.MessagesSync ...@@ -14,6 +15,7 @@ import chat.rocket.android.db.model.MessagesSync
import chat.rocket.android.db.model.ReactionEntity import chat.rocket.android.db.model.ReactionEntity
import chat.rocket.android.db.model.UrlEntity import chat.rocket.android.db.model.UrlEntity
import chat.rocket.android.db.model.UserEntity import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.emoji.internal.db.StringListConverter
@Database( @Database(
entities = [ entities = [
...@@ -23,9 +25,10 @@ import chat.rocket.android.db.model.UserEntity ...@@ -23,9 +25,10 @@ import chat.rocket.android.db.model.UserEntity
AttachmentFieldEntity::class, AttachmentActionEntity::class, UrlEntity::class, AttachmentFieldEntity::class, AttachmentActionEntity::class, UrlEntity::class,
ReactionEntity::class, MessagesSync::class ReactionEntity::class, MessagesSync::class
], ],
version = 10, version = 11,
exportSchema = true exportSchema = true
) )
@TypeConverters(StringListConverter::class)
abstract class RCDatabase : RoomDatabase() { abstract class RCDatabase : RoomDatabase() {
abstract fun userDao(): UserDao abstract fun userDao(): UserDao
abstract fun chatRoomDao(): ChatRoomDao abstract fun chatRoomDao(): ChatRoomDao
......
...@@ -29,7 +29,7 @@ abstract class UserDao : BaseDao<UserEntity> { ...@@ -29,7 +29,7 @@ abstract class UserDao : BaseDao<UserEntity> {
abstract fun findUser(id: String): String? abstract fun findUser(id: String): String?
@Query("SELECT * FROM users WHERE ID = :id") @Query("SELECT * FROM users WHERE ID = :id")
abstract fun getUser(id:String): UserEntity? abstract fun getUser(id: String): UserEntity?
@Transaction @Transaction
open fun upsert(user: BaseUserEntity) { open fun upsert(user: BaseUserEntity) {
......
...@@ -5,6 +5,8 @@ import androidx.room.Entity ...@@ -5,6 +5,8 @@ import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import chat.rocket.android.emoji.internal.db.StringListConverter
@Entity(tableName = "chatrooms", @Entity(tableName = "chatrooms",
indices = [ indices = [
...@@ -20,6 +22,7 @@ import androidx.room.PrimaryKey ...@@ -20,6 +22,7 @@ import androidx.room.PrimaryKey
ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["lastMessageUserId"]) ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["lastMessageUserId"])
] ]
) )
@TypeConverters(StringListConverter::class)
data class ChatRoomEntity( data class ChatRoomEntity(
@PrimaryKey var id: String, @PrimaryKey var id: String,
var subscriptionId: String, var subscriptionId: String,
...@@ -31,6 +34,9 @@ data class ChatRoomEntity( ...@@ -31,6 +34,9 @@ data class ChatRoomEntity(
var readonly: Boolean? = false, var readonly: Boolean? = false,
var isDefault: Boolean? = false, var isDefault: Boolean? = false,
var favorite: Boolean? = false, var favorite: Boolean? = false,
var topic: String? = null,
var announcement: String? = null,
var description: String? = null,
var open: Boolean = true, var open: Boolean = true,
var alert: Boolean = false, var alert: Boolean = false,
var unread: Long = 0, var unread: Long = 0,
...@@ -42,7 +48,8 @@ data class ChatRoomEntity( ...@@ -42,7 +48,8 @@ data class ChatRoomEntity(
var lastMessageText: String? = null, var lastMessageText: String? = null,
var lastMessageUserId: String? = null, var lastMessageUserId: String? = null,
var lastMessageTimestamp: Long? = null, var lastMessageTimestamp: Long? = null,
var broadcast: Boolean? = false var broadcast: Boolean? = false,
var muted: List<String>? = null
) )
data class ChatRoom( data class ChatRoom(
......
package chat.rocket.android.favoritemessages.di package chat.rocket.android.favoritemessages.di
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesView import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesView
import chat.rocket.android.favoritemessages.ui.FavoriteMessagesFragment import chat.rocket.android.favoritemessages.ui.FavoriteMessagesFragment
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
import javax.inject.Named
@Module @Module
class FavoriteMessagesFragmentModule { class FavoriteMessagesFragmentModule {
...@@ -21,20 +14,4 @@ class FavoriteMessagesFragmentModule { ...@@ -21,20 +14,4 @@ class FavoriteMessagesFragmentModule {
fun provideFavoriteMessagesView(frag: FavoriteMessagesFragment): FavoriteMessagesView { fun provideFavoriteMessagesView(frag: FavoriteMessagesFragment): FavoriteMessagesView {
return frag return frag
} }
@Provides
@PerFragment
fun provideJob() = Job()
@Provides
@PerFragment
fun provideLifecycleOwner(frag: FavoriteMessagesFragment): LifecycleOwner {
return frag
}
@Provides
@PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
...@@ -5,6 +5,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy ...@@ -5,6 +5,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
......
...@@ -115,9 +115,6 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView { ...@@ -115,9 +115,6 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
} }
private fun setupToolbar() { private fun setupToolbar() {
(activity as ChatRoomActivity).let { (activity as ChatRoomActivity).showToolbarTitle(getString(R.string.title_favorite_messages))
it.showToolbarTitle(getString(R.string.title_favorite_messages))
it.hideToolbarChatRoomIcon()
}
} }
} }
\ No newline at end of file
package chat.rocket.android.files.di package chat.rocket.android.files.di
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.files.presentation.FilesView import chat.rocket.android.files.presentation.FilesView
import chat.rocket.android.files.ui.FilesFragment import chat.rocket.android.files.ui.FilesFragment
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module @Module
class FilesFragmentModule { class FilesFragmentModule {
...@@ -17,20 +14,4 @@ class FilesFragmentModule { ...@@ -17,20 +14,4 @@ class FilesFragmentModule {
fun provideFilesView(frag: FilesFragment): FilesView { fun provideFilesView(frag: FilesFragment): FilesView {
return frag return frag
} }
@Provides
@PerFragment
fun provideJob() = Job()
@Provides
@PerFragment
fun provideLifecycleOwner(frag: FilesFragment): LifecycleOwner {
return frag
}
@Provides
@PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
} }
\ No newline at end of file
...@@ -7,6 +7,7 @@ import chat.rocket.android.files.uimodel.FileUiModel ...@@ -7,6 +7,7 @@ import chat.rocket.android.files.uimodel.FileUiModel
import chat.rocket.android.files.uimodel.FileUiModelMapper import chat.rocket.android.files.uimodel.FileUiModelMapper
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
......
...@@ -152,9 +152,11 @@ class FilesFragment : Fragment(), FilesView { ...@@ -152,9 +152,11 @@ class FilesFragment : Fragment(), FilesView {
} }
private fun setupToolbar(totalFiles: Long) { private fun setupToolbar(totalFiles: Long) {
(activity as ChatRoomActivity).let { (activity as ChatRoomActivity).showToolbarTitle(
it.showToolbarTitle(getString(R.string.title_files_total, totalFiles)) (getString(
it.hideToolbarChatRoomIcon() R.string.title_files_total,
} totalFiles
))
)
} }
} }
\ No newline at end of file
package chat.rocket.android.helper package chat.rocket.android.helper
import android.os.Build
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned import android.text.Spanned
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.BuildConfig
import chat.rocket.android.util.extensions.ifEmpty import chat.rocket.android.util.extensions.ifEmpty
object TextHelper { object TextHelper {
...@@ -23,7 +25,12 @@ object TextHelper { ...@@ -23,7 +25,12 @@ object TextHelper {
val link = links[i] val link = links[i]
val startIndexOfLink = textView.text.indexOf(link) val startIndexOfLink = textView.text.indexOf(link)
spannableString.setSpan(clickableSpan, startIndexOfLink, startIndexOfLink + link.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) spannableString.setSpan(
clickableSpan,
startIndexOfLink,
startIndexOfLink + link.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
} }
textView.movementMethod = LinkMovementMethod.getInstance() textView.movementMethod = LinkMovementMethod.getInstance()
textView.setText(spannableString, TextView.BufferType.SPANNABLE) textView.setText(spannableString, TextView.BufferType.SPANNABLE)
...@@ -39,4 +46,13 @@ object TextHelper { ...@@ -39,4 +46,13 @@ object TextHelper {
string.ifEmpty("?") string.ifEmpty("?")
return string.substring(0, 1).toUpperCase() return string.substring(0, 1).toUpperCase()
} }
/**
* Returns the user device information as well as the app information being used by the user.
*/
fun getDeviceAndAppInformation(): String {
return "v${BuildConfig.VERSION_NAME} - (${BuildConfig.VERSION_CODE}) \n" +
"${Build.MANUFACTURER} - ${Build.MODEL} \n" +
"Android ${Build.VERSION.RELEASE}"
}
} }
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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