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 @@
<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #ISSUE_NUMBER
<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->
\ No newline at end of file
#### Changes: [Add here what changes were made in this issue and if possible provide links.]
#### Screenshots or GIF for the change:
......@@ -2,29 +2,33 @@
# 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
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
- 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.
### 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`.
- 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
./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
### Command Line
......@@ -35,7 +39,7 @@ cd Rocket.Chat.Android/app
### 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
......@@ -43,4 +47,4 @@ Are you having a technical issue trying to compile the app, or setting up Push N
## 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 {
applicationId "chat.rocket.android"
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
versionCode 2050
versionName "3.1.1"
versionCode 2057
versionName "3.2.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
......@@ -140,6 +140,7 @@ dependencies {
implementation libraries.frescoAnimatedWebP
implementation libraries.glide
implementation libraries.glideTransformations
kapt libraries.kotshiCompiler
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
import timber.log.Timber
import android.util.Log
// Production logger... Just ignore it
class CrashlyticsTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) {
Log.println(priority, tag, message)
if (throwable != null) {
Log.e(tag,throwable.toString())
}
throwable?.printStackTrace()
}
}
......@@ -105,6 +105,15 @@
<meta-data
android:name="io.fabric.ApiKey"
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>
......@@ -61,7 +61,14 @@ interface Analytics {
fun logServerSwitch(serverUrl: String, serverCount: Int) {}
/**
* Logs the admin opening.
* Logs the admin opening event.
*/
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(
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) {
object ChatRoom : ScreenViewEvent("ChatRoomFragment")
object ChatRooms : ScreenViewEvent("ChatRoomsFragment")
object CreateChannel : ScreenViewEvent("CreateChannelFragment")
object UserDetails : ScreenViewEvent("UserDetailsFragment")
object FavoriteMessages : ScreenViewEvent("FavoriteMessagesFragment")
object Files : ScreenViewEvent("FilesFragment")
object MemberBottomSheet : ScreenViewEvent("MemberBottomSheetFragment")
object Members : ScreenViewEvent("MembersFragment")
object Mentions : ScreenViewEvent("MentionsFragment")
object MessageInfo : ScreenViewEvent("MessageInfoFragment")
......
......@@ -23,10 +23,17 @@ class OnBoardingPresenter @Inject constructor(
private val getAccountsInteractor: GetAccountsInteractor,
val settingsInteractor: GetSettingsInteractor,
val factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, settingsInteractor) {
) : CheckServerPresenter(
strategy = strategy,
factory = factory,
settingsInteractor = settingsInteractor,
refreshSettingsInteractor = refreshSettingsInteractor
) {
fun toSignInToYourServer() = navigator.toSignInToYourServer()
fun toCreateANewServer(createServerUrl: String) = navigator.toWebPage(createServerUrl)
fun connectToCommunityServer(communityServerUrl: String) {
connectToServer(communityServerUrl) {
if (totalSocialAccountsEnabled == 0 && !isNewAccountCreationEnabled) {
......@@ -63,8 +70,6 @@ class OnBoardingPresenter @Inject constructor(
}
}
fun toCreateANewServer(createServerUrl: String) = navigator.toWebPage(createServerUrl)
private fun connectToServer(serverUrl: String, block: () -> Unit) {
launchUI(strategy) {
// Check if we already have an account for this server...
......@@ -77,9 +82,9 @@ class OnBoardingPresenter @Inject constructor(
try {
withContext(DefaultDispatcher) {
setupConnectionInfo(serverUrl)
refreshSettingsInteractor.refresh(serverUrl)
// preparing next fragment before showing it
refreshServerAccounts()
checkEnabledAccounts(serverUrl)
checkIfLoginFormIsEnabled()
checkIfCreateNewAccountIsEnabled()
......
......@@ -25,7 +25,13 @@ class ServerPresenter @Inject constructor(
private val getAccountsInteractor: GetAccountsInteractor,
val settingsInteractor: GetSettingsInteractor,
val factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, settingsInteractor, view) {
) : CheckServerPresenter(
strategy = strategy,
factory = factory,
settingsInteractor = settingsInteractor,
versionCheckView = view,
refreshSettingsInteractor = refreshSettingsInteractor
) {
fun checkServer(server: String) {
if (!server.isValidUrl()) {
......@@ -93,9 +99,8 @@ class ServerPresenter @Inject constructor(
view.showLoading()
try {
withContext(DefaultDispatcher) {
refreshSettingsInteractor.refresh(serverUrl)
// preparing next fragment before showing it
refreshServerAccounts()
checkEnabledAccounts(serverUrl)
checkIfLoginFormIsEnabled()
checkIfCreateNewAccountIsEnabled()
......@@ -112,5 +117,4 @@ class ServerPresenter @Inject constructor(
}
}
}
}
\ No newline at end of file
package chat.rocket.android.authentication.server.ui
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
......@@ -11,6 +12,7 @@ import android.widget.ScrollView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.text.color
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
......@@ -51,6 +53,8 @@ class ServerFragment : Fragment(), ServerView {
lateinit var analyticsManager: AnalyticsManager
private var deepLinkInfo: LoginDeepLinkInfo? = null
private var protocol = "https://"
private var isDomainAppended = false
private var appendedText = ""
private lateinit var serverUrlDisposable: Disposable
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) {
......@@ -131,7 +135,7 @@ class ServerFragment : Fragment(), ServerView {
}
private fun setupOnClickListener() =
ui { _ ->
ui {
button_connect.setOnClickListener {
presenter.checkServer("$protocol${text_server_url.textContent.sanitize()}")
}
......@@ -244,16 +248,41 @@ class ServerFragment : Fragment(), ServerView {
private fun subscribeEditText() {
serverUrlDisposable = text_server_url.asObservable()
.filter { it.isNotBlank() }
.subscribe {
if ("$protocol${it.toString()}".isValidUrl()) {
.subscribe { processUserInput(it.toString()) }
}
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()
} else {
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() {
enableButtonConnect()
......
......@@ -5,7 +5,8 @@ import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class TwoFAFragmentProvider {
@Module
abstract class TwoFAFragmentProvider {
@ContributesAndroidInjector(modules = [TwoFAFragmentModule::class])
@PerFragment
......
......@@ -19,7 +19,6 @@ import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.coroutines.experimental.Job
import javax.inject.Inject
class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
......@@ -27,7 +26,6 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var presenter: AuthenticationPresenter
val job = Job()
override fun onCreate(savedInstanceState: Bundle?) {
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(
private fun bindAudioOrVideo(data: AttachmentUiModel) {
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.text = data.text
......
......@@ -61,6 +61,10 @@ abstract class BaseViewHolder<T : BaseUiModel<*>>(
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
......
package chat.rocket.android.chatroom.adapter
import androidx.recyclerview.widget.RecyclerView
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.*
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
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.util.extensions.inflate
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.ButtonAction
import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage
import timber.log.Timber
import java.security.InvalidParameterException
......@@ -22,7 +28,8 @@ class ChatRoomAdapter(
private val roomName: String? = null,
private val actionSelectListener: OnActionSelected? = null,
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null
private val reactionListener: EmojiReactionListener? = null,
private val navigator: ChatRoomNavigator? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseUiModel<*>>()
......@@ -34,7 +41,11 @@ class ChatRoomAdapter(
return when (viewType.toViewType()) {
BaseUiModel.ViewType.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 -> {
val view = parent.inflate(R.layout.message_url_preview)
......@@ -42,11 +53,20 @@ class ChatRoomAdapter(
}
BaseUiModel.ViewType.ATTACHMENT -> {
val view = parent.inflate(R.layout.item_message_attachment)
AttachmentViewHolder(view, actionsListener, reactionListener, actionAttachmentOnClickListener)
AttachmentViewHolder(
view,
actionsListener,
reactionListener,
actionAttachmentOnClickListener
)
}
BaseUiModel.ViewType.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)
}
}
......@@ -117,7 +137,8 @@ class ChatRoomAdapter(
fun prependData(dataSet: List<BaseUiModel<*>>) {
//---At first we will update all already saved elements with received updated ones
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) {
this.dataSet[matchedIndex] = newItem
notifyItemChanged(matchedIndex)
......@@ -251,6 +272,9 @@ class ChatRoomAdapter(
R.id.action_message_permalink -> {
actionSelectListener?.copyPermalink(id)
}
R.id.action_message_report -> {
actionSelectListener?.reportMessage(id)
}
else -> {
TODO("Not implemented")
}
......@@ -260,16 +284,34 @@ class ChatRoomAdapter(
}
interface OnActionSelected {
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 editMessage(roomId: String, messageId: String, text: String)
fun toogleStar(id: String, star: Boolean)
fun tooglePin(id: String, pin: Boolean)
fun deleteMessage(roomId: String, id: String)
fun showReactions(id: String)
fun openDirectMessage(roomName: String, message: String)
fun sendMessage(chatRoomId: String, text: String)
fun copyPermalink(id: String)
fun reportMessage(id: String)
}
}
......@@ -75,7 +75,7 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
class ReactionViewHolder(
view: View,
private val listener: EmojiReactionListener?
) : RecyclerView.ViewHolder(view), View.OnClickListener {
) : RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
@Inject
lateinit var localRepository: LocalRepository
......@@ -121,6 +121,8 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
view_flipper_reaction.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>()
}
}
}
override fun onLongClick(v: View?): Boolean {
listener?.onReactionLongClicked(reaction.shortname, reaction.isCustom, reaction.url, reaction.usernames)
return true
}
}
class AddReactionViewHolder(
......
......@@ -18,7 +18,8 @@ import kotlinx.android.synthetic.main.item_message.view.*
class MessageViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
reactionListener: EmojiReactionListener? = null,
private val avatarListener: (String) -> Unit
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener), Drawable.Callback {
init {
......@@ -70,6 +71,12 @@ class MessageViewHolder(
)
read_receipt_view.isVisible = true
}
image_avatar.setOnClickListener {
data.message.sender?.id?.let { userId ->
avatarListener(userId)
}
}
}
}
......
package chat.rocket.android.chatroom.di
import android.app.Application
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.ChatRoomView
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.db.ChatRoomDao
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.db.UserDao
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.Provides
import kotlinx.coroutines.experimental.Job
import javax.inject.Named
@Module
class ChatRoomFragmentModule {
@Provides
@PerFragment
fun provideJob() = Job()
@Provides
@PerFragment
fun chatRoomView(frag: ChatRoomFragment): ChatRoomView {
......@@ -35,11 +34,37 @@ class ChatRoomFragmentModule {
@Provides
@PerFragment
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
fun provideChatRoomDao(manager: DatabaseManager): ChatRoomDao = manager.chatRoomDao()
@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
@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
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
class ChatRoomModule {
@Provides
@PerActivity
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
import dagger.android.ContributesAndroidInjector
@Module abstract class MessageServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideMessageService(): MessageService
}
\ No newline at end of file
package chat.rocket.android.chatroom.presentation
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.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.chatRoomIntent
......@@ -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.pinnedmessages.ui.TAG_PINNED_MESSAGES_FRAGMENT
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.userdetails.ui.TAG_USER_DETAILS_FRAGMENT
import chat.rocket.android.util.extensions.addFragmentBackStack
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) {
activity.addFragmentBackStack(TAG_MEMBERS_FRAGMENT, R.id.fragment_container) {
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) {
activity.addFragmentBackStack(TAG_MENTIONS_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.mentions.ui.newInstance(chatRoomId)
......
......@@ -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.EmojiSuggestionUiModel
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.MessageView
import chat.rocket.core.internal.realtime.socket.model.State
......@@ -131,12 +132,7 @@ interface ChatRoomView : LoadingView, MessageView {
fun populateEmojiSuggestions(emojis: List<EmojiSuggestionUiModel>)
/**
* This user has joined the chat callback.
*
* @param userCanPost Whether the user can post a message or not.
*/
fun onJoined(userCanPost: Boolean)
fun onJoined(roomUiModel: RoomUiModel)
fun showReactionsPopup(messageId: String)
......@@ -147,9 +143,6 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun populateCommandSuggestions(commands: List<CommandSuggestionUiModel>)
/**
* Communicate whether it's a broadcast channel and if current user can post to it.
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
fun onRoomUpdated(roomUiModel: RoomUiModel)
}
......@@ -8,63 +8,25 @@ import androidx.appcompat.widget.SearchView
import androidx.core.content.res.ResourcesCompat
import chat.rocket.android.R
import chat.rocket.android.util.extension.onQueryTextListener
import chat.rocket.common.model.RoomType
internal fun ChatRoomFragment.setupMenu(menu: Menu) {
setupSearchMessageMenuItem(menu, requireContext())
setupFavoriteMenuItem(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
)
}
setupDetailsMenuItem(menu)
}
internal fun ChatRoomFragment.setOnMenuItemClickListener(item: MenuItem) {
when (item.itemId) {
MENU_ACTION_FAVORITE_UNFAVORITE_CHAT -> presenter.toggleFavoriteChatRoom(
MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT -> presenter.toggleFavoriteChatRoom(
chatRoomId,
isFavorite
)
MENU_ACTION_MEMBER -> presenter.toMembersList(chatRoomId)
MENU_ACTION_MENTIONS -> presenter.toMentions(chatRoomId)
MENU_ACTION_PINNED_MESSAGES -> presenter.toPinnedMessageList(chatRoomId)
MENU_ACTION_FAVORITE_MESSAGES -> presenter.toFavoriteMessageList(chatRoomId)
MENU_ACTION_FILES -> presenter.toFileList(chatRoomId)
MENU_ACTION_SHOW_DETAILS -> presenter.toChatDetails(
chatRoomId,
chatRoomType,
isSubscribed,
disableMenu
)
}
}
......@@ -125,7 +87,7 @@ private fun ChatRoomFragment.setupFavoriteMenuItem(menu: Menu) {
if (isFavorite) {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_UNFAVORITE_CHAT,
MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT,
Menu.NONE,
R.string.title_unfavorite_chat
).setIcon(R.drawable.ic_star_yellow_24dp)
......@@ -133,10 +95,20 @@ private fun ChatRoomFragment.setupFavoriteMenuItem(menu: Menu) {
} else {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_UNFAVORITE_CHAT,
MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT,
Menu.NONE,
R.string.title_favorite_chat
).setIcon(R.drawable.ic_star_border_white_24dp)
.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(
var isFirstUnread: Boolean,
override var isTemporary: Boolean = false,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var permalink: String
override var permalink: String,
val subscriptionId: String
) : BaseMessageUiModel<Message> {
override val viewType: Int
get() = BaseUiModel.ViewType.MESSAGE.viewType
......
......@@ -6,5 +6,6 @@ data class ReactionUiModel(
val unicode: CharSequence,
val count: Int,
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
import chat.rocket.android.server.domain.messageReadReceiptStoreUsers
import chat.rocket.android.server.domain.useRealName
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.ifNotNullNorEmpty
import chat.rocket.android.util.extensions.isNotNullNorEmpty
......@@ -125,8 +126,13 @@ class UiModelMapper @Inject constructor(
getChatRoomAsync(message.roomId)?.let { chatRoom ->
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) }
}
}
message.attachments?.mapNotNull { attachment ->
mapAttachment(message, attachment, chatRoom)
......@@ -162,7 +168,7 @@ class UiModelMapper @Inject constructor(
// TODO: move this to new interactor or FetchChatRoomsInteractor?
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) {
ChatRoom(
id = id,
......@@ -454,7 +460,8 @@ class UiModelMapper @Inject constructor(
messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, currentDayMarkerText = dayMarkerText,
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 {
......@@ -469,7 +476,7 @@ class UiModelMapper @Inject constructor(
val list = mutableListOf<ReactionUiModel>()
val customEmojis = EmojiRepository.getCustomEmojis()
it.getShortNames().forEach { shortname ->
val usernames = it.getUsernames(shortname) ?: emptyList()
val usernames = it.getUsernames(shortname).orEmpty()
val count = usernames.size
val custom = customEmojis.firstOrNull { emoji -> emoji.shortname == shortname }
list.add(
......@@ -478,7 +485,8 @@ class UiModelMapper @Inject constructor(
unicode = EmojiParser.parse(context, shortname),
count = count,
usernames = usernames,
url = custom?.url)
url = custom?.url,
isCustom = custom != null)
)
}
list
......
......@@ -2,15 +2,11 @@ package chat.rocket.android.chatrooms.adapter
import android.app.Application
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.chatrooms.adapter.model.RoomUiModel
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.PermissionsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.showLastMessage
import chat.rocket.android.server.domain.useRealName
......@@ -30,20 +26,16 @@ class RoomUiModelMapper(
private val context: Application,
private val settings: PublicSettings,
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 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 { userInteractor.get() }
private val currentUser by lazy {
userInteractor.get()
}
fun map(rooms: List<ChatRoom>, grouped: Boolean = false, showLastMessage: Boolean = true): List<ItemHolder<*>> {
fun map(
rooms: List<ChatRoom>,
grouped: Boolean = false,
showLastMessage: Boolean = true
): List<ItemHolder<*>> {
val list = ArrayList<ItemHolder<*>>(rooms.size + 4)
var lastType: String? = null
rooms.forEach { room ->
......@@ -72,7 +64,7 @@ class RoomUiModelMapper(
private fun mapUser(user: User): RoomUiModel {
return with(user) {
val name = mapName(user.username!!, user.name, false)
val name = mapName(user.username!!, user.name)
val status = user.status
val avatar = serverUrl.avatarUrl(user.username!!)
val username = user.username!!
......@@ -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) {
RoomUiModel(
id = id,
name = name!!,
type = type,
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,
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) {
val isUnread = alert || unread > 0
val type = roomTypeOf(type)
val status = chatRoom.status?.let { userStatusOf(it) }
val roomName = mapName(name, fullname, isUnread)
val timestamp = mapDate(lastMessageTimestamp ?: updatedAt, isUnread)
val roomName = mapName(name, fullname)
val timestamp = mapDate(lastMessageTimestamp ?: updatedAt)
val avatar = if (type is RoomType.DirectMessage) {
serverUrl.avatarUrl(name)
} else {
serverUrl.avatarUrl(name, isGroupOrChannel = true)
}
val unread = mapUnread(unread)
val lastMessage = if(showLastMessage) { mapLastMessage(lastMessageUserId, chatRoom.lastMessageUserName,
chatRoom.lastMessageUserFullName, lastMessageText, isUnread,
type is RoomType.DirectMessage) } else { null }
val lastMessage = if (showLastMessage) {
mapLastMessage(
lastMessageUserId,
chatRoom.lastMessageUserName,
chatRoom.lastMessageUserFullName,
lastMessageText,
type is RoomType.DirectMessage
)
} else {
null
}
val hasMentions = mapMentions(userMentions, groupMentions)
val open = open
val lastMessageMarkdown = lastMessage?.let { Markwon.markdown(context, it.toString()).toString() }
val lastMessageMarkdown =
lastMessage?.let { Markwon.markdown(context, it.toString()).toString() }
RoomUiModel(
id = id,
......@@ -130,14 +139,22 @@ class RoomUiModelMapper(
open = open,
date = timestamp,
unread = unread,
mentions = hasMentions,
alert = isUnread,
lastMessage = lastMessageMarkdown,
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 {
val resources = context.resources
return when (type) {
......@@ -149,47 +166,37 @@ class RoomUiModelMapper(
}
}
private fun mapLastMessage(userId: String?, name: String?, fullName: String?, text: String?,
unread: Boolean = false,
isDirectMessage: Boolean = false): CharSequence? {
private fun mapLastMessage(
userId: String?,
name: String?,
fullName: String?,
text: String?,
isDirectMessage: Boolean = false
): CharSequence? {
return if (!settings.showLastMessage()) {
null
} else if (name != null && text != null) {
val user = if (currentUser != null && currentUser!!.id == userId) {
"${context.getString(R.string.msg_you)}: "
} else {
if (isDirectMessage) "" else "${mapName(name, fullName, unread)}: "
}
val color = if (unread) messageUnreadColor else messageColor
SpannableStringBuilder()
.color(color) {
bold { append(user) }
append(text)
if (isDirectMessage) "" else "${mapName(name, fullName)}: "
}
SpannableStringBuilder().append(user).append(text)
} else {
context.getText(R.string.msg_no_messages_yet)
}
}
private fun mapName(name: String, fullName: String?, unread: Boolean): CharSequence {
val roomName = if (settings.useSpecialCharsOnRoom() || settings.useRealName()) {
private fun mapName(name: String, fullName: String?): CharSequence {
return if (settings.useSpecialCharsOnRoom() || settings.useRealName()) {
fullName ?: name
} else {
name
}
val color = if (unread) nameUnreadColor else nameColor
return SpannableStringBuilder()
.color(color) {
append(roomName)
}
}
private fun mapUnread(unread: Long): String? {
return when(unread) {
return when (unread) {
0L -> null
in 1..99 -> unread.toString()
else -> context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
......@@ -197,12 +204,14 @@ class RoomUiModelMapper(
}
}
private fun mapDate(date: Long?, unread: Boolean): CharSequence? {
return date?.localDateTime()?.date(context)?.let {
val color = if (unread) dateUnreadColor else dateColor
SpannableStringBuilder().color(color) {
append(it)
private fun mapMentions(userMentions: Long?, groupMentions: Long?): Boolean {
if (userMentions != null && groupMentions != null) {
if (userMentions > 0 || groupMentions > 0) {
return true
}
}
return false
}
private fun mapDate(date: Long?): CharSequence? = date?.localDateTime()?.date(context)
}
......@@ -4,6 +4,7 @@ import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
......@@ -11,19 +12,16 @@ import chat.rocket.common.model.RoomType
import chat.rocket.common.model.UserStatus
import kotlinx.android.synthetic.main.item_chat.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 channelUnread: Drawable = resources.getDrawable(R.drawable.ic_hashtag_black_12dp)
private val channel: Drawable = resources.getDrawable(R.drawable.ic_hashtag_12dp)
private val groupUnread: Drawable = resources.getDrawable(R.drawable.ic_lock_black_12_dp)
private val group: Drawable = resources.getDrawable(R.drawable.ic_lock_12_dp)
private val online: Drawable = resources.getDrawable(R.drawable.ic_status_online_12dp)
private val away: Drawable = resources.getDrawable(R.drawable.ic_status_away_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)
private val channelIcon: Drawable = resources.getDrawable(R.drawable.ic_hashtag_12dp)
private val groupIcon: Drawable = resources.getDrawable(R.drawable.ic_lock_12_dp)
private val onlineIcon: Drawable = resources.getDrawable(R.drawable.ic_status_online_12dp)
private val awayIcon: Drawable = resources.getDrawable(R.drawable.ic_status_away_12dp)
private val busyIcon: Drawable = resources.getDrawable(R.drawable.ic_status_busy_12dp)
private val offlineIcon: Drawable = resources.getDrawable(R.drawable.ic_status_invisible_12dp)
override fun bindViews(data: RoomItemHolder) {
val room = data.data
......@@ -31,53 +29,59 @@ class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit
image_avatar.setImageURI(room.avatar)
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) {
text_last_message.isVisible = true
text_last_message.text = room.lastMessage
text_last_message.isVisible = true
} else {
text_last_message.isGone = true
}
if (room.date != null) {
text_last_message_date_time.isVisible = true
text_last_message_date_time.text = room.date
text_timestamp.text = room.date
text_timestamp.isVisible = true
} 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.text = room.unread
} 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) {
image_chat_icon.setImageDrawable(getStatusDrawable(room.status))
} else {
image_chat_icon.setImageDrawable(getRoomDrawable(room.type, room.alert))
}
setOnClickListener {
listener(room)
}
setOnClickListener { listener(room) }
}
}
private fun getRoomDrawable(type: RoomType, alert: Boolean): Drawable? {
return when(type) {
is RoomType.Channel -> if (alert) channelUnread else channel
is RoomType.PrivateGroup -> if (alert) groupUnread else group
private fun getRoomDrawable(type: RoomType): Drawable? {
return when (type) {
is RoomType.Channel -> channelIcon
is RoomType.PrivateGroup -> groupIcon
else -> null
}
}
private fun getStatusDrawable(status: UserStatus): Drawable {
return when(status) {
is UserStatus.Online -> online
is UserStatus.Away -> away
is UserStatus.Busy -> busy
else -> offline
return when (status) {
is UserStatus.Online -> onlineIcon
is UserStatus.Away -> awayIcon
is UserStatus.Busy -> busyIcon
else -> offlineIcon
}
}
}
\ No newline at end of file
......@@ -6,7 +6,8 @@ import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
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 {
setHasStableIds(true)
......@@ -40,16 +41,16 @@ class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) : RecyclerView.A
override fun getItemId(position: Int): Long {
val item = values[position]
return when(item) {
is HeaderItemHolder -> item.data.hashCode().toLong()
return when (item) {
is RoomItemHolder -> item.data.id.hashCode().toLong()
is HeaderItemHolder -> item.data.hashCode().toLong()
is LoadingItemHolder -> "loading".hashCode().toLong()
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
}
}
override fun getItemViewType(position: Int): Int {
return when(values[position]) {
return when (values[position]) {
is RoomItemHolder -> VIEW_TYPE_ROOM
is HeaderItemHolder -> VIEW_TYPE_HEADER
is LoadingItemHolder -> VIEW_TYPE_LOADING
......
......@@ -12,7 +12,12 @@ data class RoomUiModel(
val date: CharSequence? = null,
val unread: String? = null,
val alert: Boolean = false,
val mentions: Boolean = false,
val lastMessage: CharSequence? = 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
import chat.rocket.android.db.UserDao
import chat.rocket.android.infrastructure.LocalRepository
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.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
......@@ -88,9 +89,10 @@ class ChatRoomsFragmentModule {
context: Application,
repository: SettingsRepository,
userInteractor: GetCurrentUserInteractor,
@Named("currentServer") serverUrl: String
@Named("currentServer") serverUrl: String,
permissionsInteractor: PermissionsInteractor
): RoomUiModelMapper {
return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl)
return RoomUiModelMapper(context, repository.get(serverUrl), userInteractor, serverUrl, permissionsInteractor)
}
@Provides
......
......@@ -3,9 +3,12 @@ package chat.rocket.android.chatrooms.infrastructure
import androidx.lifecycle.LiveData
import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.util.retryDB
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>> {
return when(order) {
Order.ACTIVITY -> dao.getAll()
......@@ -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 {
ACTIVITY,
......
......@@ -13,6 +13,7 @@ import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.domain.useSpecialCharsOnRoom
import chat.rocket.android.server.infraestructure.ConnectionManager
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType
......@@ -41,11 +42,31 @@ class ChatRoomsPresenter @Inject constructor(
private val client = manager.client
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) {
launchUI(strategy) {
view.showLoadingRoom(chatRoom.name)
try {
val room = dbManager.getRoom(chatRoom.id)
val room = retryDB("getRoom(${chatRoom.id}") { dbManager.getRoom(chatRoom.id) }
if (room != null) {
loadChatRoom(room.chatRoom, true)
} else {
......@@ -56,7 +77,8 @@ class ChatRoomsPresenter @Inject constructor(
type = type.toString(),
name = username ?: name.toString(),
fullname = name.toString(),
open = open
open = open,
muted = muted
)
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
import chat.rocket.android.util.extension.onQueryTextListener
import chat.rocket.android.util.extensions.fadeIn
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.showToast
import chat.rocket.android.util.extensions.ui
......@@ -83,9 +85,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomId?.let {
// TODO - bring back support to load a room from id.
//presenter.goToChatRoomWithId(it)
chatRoomId.ifNotNullNotEmpty { roomId ->
presenter.loadChatRoom(roomId)
chatRoomId = null
}
}
......@@ -238,7 +239,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
AlertDialog.Builder(context)
.setTitle(R.string.dialog_sort_title)
.setView(dialogLayout)
.setPositiveButton(R.string.dialog_button_done) { dialog, _ ->
.setPositiveButton(R.string.msg_sort) { dialog, _ ->
invalidateQueryOnSearch()
updateSort()
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
import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.withContext
import timber.log.Timber
import java.security.InvalidParameterException
import java.lang.IllegalArgumentException
import kotlin.coroutines.experimental.coroutineContext
......@@ -171,6 +171,6 @@ fun Query.asSortingOrder(): ChatRoomsRepository.Order {
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
import chat.rocket.android.authentication.signup.di.SignupFragmentProvider
import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatdetails.di.ChatDetailsFragmentProvider
import chat.rocket.android.chatinformation.di.MessageInfoFragmentProvider
import chat.rocket.android.chatinformation.ui.MessageInfoActivity
import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider
......@@ -35,6 +36,7 @@ import chat.rocket.android.server.ui.ChangeServerActivity
import chat.rocket.android.settings.di.SettingsFragmentProvider
import chat.rocket.android.settings.password.di.PasswordFragmentProvider
import chat.rocket.android.settings.password.ui.PasswordActivity
import chat.rocket.android.userdetails.di.UserDetailsFragmentProvider
import chat.rocket.android.webview.adminpanel.di.AdminPanelWebViewFragmentProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -73,9 +75,10 @@ abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(
modules = [
ChatRoomModule::class,
modules = [ChatRoomModule::class,
ChatRoomFragmentProvider::class,
UserDetailsFragmentProvider::class,
ChatDetailsFragmentProvider::class,
MembersFragmentProvider::class,
MentionsFragmentProvider::class,
PinnedMessagesFragmentProvider::class,
......@@ -96,6 +99,8 @@ abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(modules = [MessageInfoFragmentProvider::class])
abstract fun bindMessageInfoActiviy(): MessageInfoActivity
@PerActivity
@ContributesAndroidInjector(modules = [DrawModule::class])
abstract fun bindDrawingActivity(): DrawingActivity
}
......@@ -64,6 +64,7 @@ import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger
import chat.rocket.common.util.NoOpLogger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.internal.AttachmentAdapterFactory
import chat.rocket.core.internal.ReactionsAdapter
......@@ -231,7 +232,7 @@ class AppModule {
return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE)
.add(AttachmentAdapterFactory(Logger(logger, url)))
.add(AttachmentAdapterFactory(NoOpLogger))
.add(
java.lang.Long::class.java,
ISO8601Date::class.java,
......
......@@ -14,6 +14,7 @@ import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger
import chat.rocket.common.util.NoOpLogger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.internal.AttachmentAdapterFactory
import chat.rocket.core.internal.ReactionsAdapter
......@@ -47,7 +48,7 @@ class LocalModule {
return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE)
.add(AttachmentAdapterFactory(Logger(logger, url)))
.add(AttachmentAdapterFactory(NoOpLogger))
.add(
java.lang.Long::class.java,
ISO8601Date::class.java,
......@@ -67,8 +68,6 @@ class LocalModule {
return context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
}
@Provides
@Singleton
fun provideLocalRepository(sharedPreferences: SharedPreferences, moshi: Moshi): LocalRepository {
......
......@@ -9,7 +9,6 @@ import androidx.room.Transaction
import androidx.room.Update
import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.db.model.ChatRoomEntity
import chat.rocket.common.model.RoomType
@Dao
abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
......@@ -18,7 +17,14 @@ abstract class ChatRoomDao : BaseDao<ChatRoomEntity> {
$BASE_QUERY
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
@Query("$BASE_QUERY $FILTER_NOT_OPENED")
......
......@@ -19,6 +19,7 @@ import chat.rocket.android.util.extensions.exhaustive
import chat.rocket.android.util.extensions.removeTrailingSlash
import chat.rocket.android.util.extensions.toEntity
import chat.rocket.android.util.extensions.userId
import chat.rocket.android.util.retryDB
import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.SimpleUser
......@@ -41,8 +42,7 @@ import timber.log.Timber
import java.util.HashSet
import kotlin.system.measureTimeMillis
class DatabaseManager(val context: Application,
val serverUrl: String) {
class DatabaseManager(val context: Application, val serverUrl: String) {
private val database: RCDatabase = androidx.room.Room.databaseBuilder(context,
RCDatabase::class.java, serverUrl.databaseName())
......@@ -92,12 +92,26 @@ class DatabaseManager(val context: Application,
}
}
fun logout() {
database.clearAllTables()
suspend fun logout() {
retryDB("clearAllTables") { database.clearAllTables() }
}
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>) {
......@@ -124,11 +138,11 @@ class DatabaseManager(val context: Application,
val toInsert = ArrayList<ChatRoomEntity>(batch.size / 2)
val toUpdate = ArrayList<ChatRoomEntity>(batch.size)
batch.forEach {
when(it.type) {
when (it.type) {
is Type.Removed -> toRemove.add(removeChatRoom(it.data))
is Type.Inserted -> insertChatRoom(it.data)?.let { toInsert.add(it) }
is Type.Updated -> {
when(it.data) {
when (it.data) {
is Subscription -> updateSubs[(it.data as Subscription).roomId] = it.data as Subscription
is Room -> updateRooms[(it.data as Room).id] = it.data as Room
}
......@@ -152,7 +166,7 @@ class DatabaseManager(val context: Application,
fun updateSelfUser(myself: Myself) {
launch(dbManagerContext) {
val user = userDao().getUser(myself.id)
val user = retryDB("getUser(${myself.id})") { userDao().getUser(myself.id) }
val entity = user?.copy(
name = myself.name ?: user.name,
username = myself.username ?: user.username,
......@@ -168,7 +182,7 @@ class DatabaseManager(val context: Application,
fun processRooms(rooms: List<ChatRoom>) {
launch(dbManagerContext) {
val entities = rooms.map { mapChatRoom(it) }
sendOperation(Operation.InsertRooms(entities))
sendOperation(Operation.CleanInsertRooms(entities))
}
}
......@@ -329,14 +343,14 @@ class DatabaseManager(val context: Application,
}
private fun removeChatRoom(data: BaseRoom): String {
return when(data) {
return when (data) {
is Subscription -> data.roomId
else -> data.id
}
}
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) {
val chatRoom = current.chatRoom
......@@ -349,9 +363,13 @@ class DatabaseManager(val context: Application,
ownerId = user?.id ?: chatRoom.ownerId,
readonly = readonly,
updatedAt = updatedAt ?: chatRoom.updatedAt,
topic = topic,
announcement = announcement,
description = description,
lastMessageText = mapLastMessageText(lastMessage),
lastMessageUserId = lastMessage?.sender?.id,
lastMessageTimestamp = lastMessage?.timestamp
lastMessageTimestamp = lastMessage?.timestamp,
muted = muted ?: chatRoom.muted
)
}
}
......@@ -373,7 +391,7 @@ class DatabaseManager(val context: Application,
context.getString(R.string.msg_sent_attachment)
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) {
val userId = if (type is RoomType.DirectMessage) {
......@@ -389,12 +407,16 @@ class DatabaseManager(val context: Application,
id = roomId,
subscriptionId = id,
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,
userId = userId ?: chatRoom.userId,
readonly = readonly ?: chatRoom.readonly,
isDefault = isDefault,
favorite = isFavorite,
topic = chatRoom.topic,
announcement = chatRoom.announcement,
description = chatRoom.description,
open = open,
alert = alert,
unread = unread,
......@@ -402,14 +424,15 @@ class DatabaseManager(val context: Application,
groupMentions = groupMentions ?: chatRoom.groupMentions,
updatedAt = updatedAt ?: chatRoom.updatedAt,
timestamp = timestamp ?: chatRoom.timestamp,
lastSeen = lastSeen ?: chatRoom.lastSeen
lastSeen = lastSeen ?: chatRoom.lastSeen,
muted = chatRoom.muted
)
}
}
}
private suspend fun insertChatRoom(data: BaseRoom): ChatRoomEntity? {
return when(data) {
return when (data) {
is Room -> insertRoom(data)
is Subscription -> insertSubscription(data)
else -> null
......@@ -451,13 +474,17 @@ class DatabaseManager(val context: Application,
id = room.id,
subscriptionId = subscription.id,
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,
userId = userId,
ownerId = room.user?.id,
readonly = subscription.readonly,
isDefault = subscription.isDefault,
favorite = subscription.isFavorite,
topic = room.topic,
announcement = room.announcement,
description = room.description,
open = subscription.open,
alert = subscription.alert,
unread = subscription.unread,
......@@ -495,6 +522,9 @@ class DatabaseManager(val context: Application,
readonly = readonly,
isDefault = default,
favorite = favorite,
topic = topic,
announcement = announcement,
description = description,
open = open,
alert = alert,
unread = unread,
......@@ -506,7 +536,8 @@ class DatabaseManager(val context: Application,
lastMessageText = mapLastMessageText(lastMessage),
lastMessageUserId = lastMessage?.sender?.id,
lastMessageTimestamp = lastMessage?.timestamp,
broadcast = broadcast
broadcast = broadcast,
muted = room.muted
)
}
}
......@@ -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) {
is Operation.ClearStatus -> userDao().clearStatus()
is Operation.UpdateRooms -> {
......@@ -569,6 +602,7 @@ class DatabaseManager(val context: Application,
}
}.exhaustive
}
}
}
sealed class Operation {
......@@ -579,6 +613,7 @@ sealed class Operation {
val toUpdate: List<ChatRoomEntity>,
val toRemove: List<String>
) : Operation()
data class InsertRooms(val chatRooms: List<ChatRoomEntity>) : Operation()
data class CleanInsertRooms(val chatRooms: List<ChatRoomEntity>) : Operation()
......@@ -609,7 +644,7 @@ private fun String.databaseName(): String {
val tmp = this.removePrefix("https://")
.removePrefix("http://")
.removeTrailingSlash()
.replace("/","-")
.replace("/", "-")
.replace(".", "_")
return "$tmp.db"
......
......@@ -2,6 +2,7 @@ package chat.rocket.android.db
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import chat.rocket.android.db.model.AttachmentActionEntity
import chat.rocket.android.db.model.AttachmentEntity
import chat.rocket.android.db.model.AttachmentFieldEntity
......@@ -14,6 +15,7 @@ import chat.rocket.android.db.model.MessagesSync
import chat.rocket.android.db.model.ReactionEntity
import chat.rocket.android.db.model.UrlEntity
import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.emoji.internal.db.StringListConverter
@Database(
entities = [
......@@ -23,9 +25,10 @@ import chat.rocket.android.db.model.UserEntity
AttachmentFieldEntity::class, AttachmentActionEntity::class, UrlEntity::class,
ReactionEntity::class, MessagesSync::class
],
version = 10,
version = 11,
exportSchema = true
)
@TypeConverters(StringListConverter::class)
abstract class RCDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun chatRoomDao(): ChatRoomDao
......
......@@ -29,7 +29,7 @@ abstract class UserDao : BaseDao<UserEntity> {
abstract fun findUser(id: String): String?
@Query("SELECT * FROM users WHERE ID = :id")
abstract fun getUser(id:String): UserEntity?
abstract fun getUser(id: String): UserEntity?
@Transaction
open fun upsert(user: BaseUserEntity) {
......
......@@ -5,6 +5,8 @@ import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import chat.rocket.android.emoji.internal.db.StringListConverter
@Entity(tableName = "chatrooms",
indices = [
......@@ -20,6 +22,7 @@ import androidx.room.PrimaryKey
ForeignKey(entity = UserEntity::class, parentColumns = ["id"], childColumns = ["lastMessageUserId"])
]
)
@TypeConverters(StringListConverter::class)
data class ChatRoomEntity(
@PrimaryKey var id: String,
var subscriptionId: String,
......@@ -31,6 +34,9 @@ data class ChatRoomEntity(
var readonly: Boolean? = false,
var isDefault: Boolean? = false,
var favorite: Boolean? = false,
var topic: String? = null,
var announcement: String? = null,
var description: String? = null,
var open: Boolean = true,
var alert: Boolean = false,
var unread: Long = 0,
......@@ -42,7 +48,8 @@ data class ChatRoomEntity(
var lastMessageText: String? = null,
var lastMessageUserId: String? = null,
var lastMessageTimestamp: Long? = null,
var broadcast: Boolean? = false
var broadcast: Boolean? = false,
var muted: List<String>? = null
)
data class ChatRoom(
......
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.db.DatabaseManager
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesView
import chat.rocket.android.favoritemessages.ui.FavoriteMessagesFragment
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import javax.inject.Named
@Module
class FavoriteMessagesFragmentModule {
......@@ -21,20 +14,4 @@ class FavoriteMessagesFragmentModule {
fun provideFavoriteMessagesView(frag: FavoriteMessagesFragment): FavoriteMessagesView {
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
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
......
......@@ -115,9 +115,6 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
}
private fun setupToolbar() {
(activity as ChatRoomActivity).let {
it.showToolbarTitle(getString(R.string.title_favorite_messages))
it.hideToolbarChatRoomIcon()
}
(activity as ChatRoomActivity).showToolbarTitle(getString(R.string.title_favorite_messages))
}
}
\ No newline at end of file
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.files.presentation.FilesView
import chat.rocket.android.files.ui.FilesFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
class FilesFragmentModule {
......@@ -17,20 +14,4 @@ class FilesFragmentModule {
fun provideFilesView(frag: FilesFragment): FilesView {
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
import chat.rocket.android.files.uimodel.FileUiModelMapper
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryDB
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
......
......@@ -152,9 +152,11 @@ class FilesFragment : Fragment(), FilesView {
}
private fun setupToolbar(totalFiles: Long) {
(activity as ChatRoomActivity).let {
it.showToolbarTitle(getString(R.string.title_files_total, totalFiles))
it.hideToolbarChatRoomIcon()
}
(activity as ChatRoomActivity).showToolbarTitle(
(getString(
R.string.title_files_total,
totalFiles
))
)
}
}
\ No newline at end of file
package chat.rocket.android.helper
import android.os.Build
import android.text.SpannableString
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.widget.TextView
import chat.rocket.android.BuildConfig
import chat.rocket.android.util.extensions.ifEmpty
object TextHelper {
......@@ -23,7 +25,12 @@ object TextHelper {
val link = links[i]
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.setText(spannableString, TextView.BufferType.SPANNABLE)
......@@ -39,4 +46,13 @@ object TextHelper {
string.ifEmpty("?")
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