Unverified Commit 513931f0 authored by divyanshu bhargava's avatar divyanshu bhargava Committed by GitHub

Merge pull request #6 from RocketChat/develop-2.x

merge
parents ed899cf4 97045a68
...@@ -19,18 +19,12 @@ Since both the versions use `kotlin` for some or all of their classes, following ...@@ -19,18 +19,12 @@ Since both the versions use `kotlin` for some or all of their classes, following
- After checking out to `develop` branch as mentioned above, simply import the project in Android Studio. - After checking out to `develop` branch as mentioned above, simply import the project in Android Studio.
#### v2+ #### v2+
- This version requires the [Kotlin SDK](https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK) for Rocket.Chat. Clone the Kotlin SDK in the **same directory** as the android repository by running `git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git`. Make sure that the android repository and the kotlin sdk have the same immediate parent directory. - 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. Change your directory to the SDK directory by running `cd Rocket.Chat.Kotlin.SDK/` in your terminal. Any of the following approaches can be followed to successfully build the SDK. - 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:
- **Command Line** - (Within the kotlin SDK directory) Run `./gradlew clean && ./gradlew assemble` to successfully build the project.
- **Android Studio** - Import the project in Android Studio. Go to `Build > Make Project` to build the SDK successfully.
After following the above methods, follow the following steps in your terminal window:
``` ```
cd .. cd Rocket.Chat.Android/app
cd Rocket.Chat.Android/app/libs ./build-sdk.sh
ls
``` ```
Two `jar` files will be found in this directory (the `common` and `core` jar files), this indicates that the SDK was built correctly.
- After the SDK has been built successfully, import the project in Android Studio and build it by following `Build > Make Project`.
## How to run ## How to run
### Command Line ### Command Line
......
...@@ -13,8 +13,8 @@ android { ...@@ -13,8 +13,8 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 2008 versionCode 2009
versionName "2.0.0-beta6" versionName "2.0.0-rc1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
} }
......
...@@ -20,7 +20,9 @@ ...@@ -20,7 +20,9 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config">
android:supportsRtl="true"> android:supportsRtl="true">
<activity <activity
android:name=".authentication.ui.AuthenticationActivity" android:name=".authentication.ui.AuthenticationActivity"
android:configChanges="orientation" android:configChanges="orientation"
...@@ -100,13 +102,18 @@ ...@@ -100,13 +102,18 @@
<action android:name="com.google.android.c2dm.intent.RECEIVE" /> <action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".chatroom.service.MessageService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<meta-data <meta-data
android:name="io.fabric.ApiKey" android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" /> android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
<activity android:name=".settings.about.ui.AboutActivity" <activity
android:theme="@style/AppTheme"/> android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme" />
</application> </application>
</manifest> </manifest>
...@@ -2,7 +2,6 @@ import android.content.Context ...@@ -2,7 +2,6 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v4.graphics.drawable.DrawableCompat import android.support.v4.graphics.drawable.DrawableCompat
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
...@@ -16,7 +15,8 @@ object DrawableHelper { ...@@ -16,7 +15,8 @@ object DrawableHelper {
* @param context The context. * @param context The context.
* @return A drawable. * @return A drawable.
*/ */
fun getDrawableFromId(id: Int, context: Context): Drawable = context.resources.getDrawable(id, null) fun getDrawableFromId(id: Int, context: Context): Drawable =
context.resources.getDrawable(id, null)
/** /**
* Wraps an array of Drawable to be used for example for tinting. * Wraps an array of Drawable to be used for example for tinting.
...@@ -68,7 +68,8 @@ object DrawableHelper { ...@@ -68,7 +68,8 @@ object DrawableHelper {
* @see tintDrawables * @see tintDrawables
* @see wrapDrawable * @see wrapDrawable
*/ */
fun tintDrawable(drawable: Drawable, context: Context, resId: Int) = DrawableCompat.setTint(drawable, ContextCompat.getColor(context, resId)) fun tintDrawable(drawable: Drawable, context: Context, resId: Int) =
DrawableCompat.setTint(drawable, ContextCompat.getColor(context, resId))
/** /**
* Compounds an array of Drawable (to appear to the left of the text) into an array of TextView. * Compounds an array of Drawable (to appear to the left of the text) into an array of TextView.
...@@ -96,13 +97,15 @@ object DrawableHelper { ...@@ -96,13 +97,15 @@ object DrawableHelper {
* @param drawable The Drawable. * @param drawable The Drawable.
* @see compoundDrawables * @see compoundDrawables
*/ */
fun compoundDrawable(textView: TextView, drawable: Drawable) = textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null) fun compoundDrawable(textView: TextView, drawable: Drawable) =
textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
/** /**
* Returns the user status drawable. * Returns the user status drawable.
* *
* @param userStatus The user status. * @param userStatus The user status.
* @param context The context. * @param context The context.
* @see [UserStatus]
* @return The user status drawable. * @return The user status drawable.
*/ */
fun getUserStatusDrawable(userStatus: UserStatus, context: Context): Drawable { fun getUserStatusDrawable(userStatus: UserStatus, context: Context): Drawable {
...@@ -119,32 +122,4 @@ object DrawableHelper { ...@@ -119,32 +122,4 @@ object DrawableHelper {
else -> getDrawableFromId(R.drawable.ic_status_invisible_24dp, context) else -> getDrawableFromId(R.drawable.ic_status_invisible_24dp, context)
} }
} }
// TODO Why we need two UserStatus?
/**
* Returns the user status drawable.
*
* @param userStatus The user status.
* @param context The context.
* @sse [chat.rocket.core.internal.realtime.UserStatus]
* @return The user status drawable.
*/
fun getUserStatusDrawable(
userStatus: chat.rocket.core.internal.realtime.UserStatus,
context: Context
): Drawable {
return when (userStatus) {
is chat.rocket.core.internal.realtime.UserStatus.Online -> {
getDrawableFromId(R.drawable.ic_status_online_24dp, context)
}
is chat.rocket.core.internal.realtime.UserStatus.Away -> {
getDrawableFromId(R.drawable.ic_status_away_24dp, context)
}
is chat.rocket.core.internal.realtime.UserStatus.Busy -> {
getDrawableFromId(R.drawable.ic_status_busy_24dp, context)
}
else -> getDrawableFromId(R.drawable.ic_status_invisible_24dp, context)
}
}
} }
\ No newline at end of file
...@@ -6,7 +6,7 @@ import android.app.Service ...@@ -6,7 +6,7 @@ import android.app.Service
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.content.edit import androidx.core.content.edit
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.app.migration.RealmMigration import chat.rocket.android.app.migration.RealmMigration
import chat.rocket.android.app.migration.RocketChatLibraryModule import chat.rocket.android.app.migration.RocketChatLibraryModule
......
package chat.rocket.android.authentication.infraestructure package chat.rocket.android.authentication.infraestructure
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.content.edit import androidx.core.content.edit
import chat.rocket.android.authentication.domain.model.TokenModel import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.server.domain.TokenRepository import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.common.model.Token import chat.rocket.common.model.Token
......
...@@ -59,37 +59,47 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView { ...@@ -59,37 +59,47 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
} }
override fun alertBlankUsername() { override fun alertBlankUsername() {
ui {
vibrateSmartPhone() vibrateSmartPhone()
text_username.shake() text_username.shake()
} }
}
override fun showLoading() { override fun showLoading() {
ui {
disableUserInput() disableUserInput()
view_loading.setVisible(true) view_loading.setVisible(true)
} }
}
override fun hideLoading() { override fun hideLoading() {
ui {
view_loading.setVisible(false) view_loading.setVisible(false)
enableUserInput() enableUserInput()
} }
}
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
ui {
showToast(resId) showToast(resId)
} }
}
override fun showMessage(message: String) { override fun showMessage(message: String) {
ui {
showToast(message) showToast(message)
} }
}
override fun showGenericErrorMessage() { override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error)) showMessage(getString(R.string.msg_generic_error))
} }
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
activity?.apply { ui {
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this) val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, it)
DrawableHelper.wrapDrawable(atDrawable) DrawableHelper.wrapDrawable(atDrawable)
DrawableHelper.tintDrawable(atDrawable, this, R.color.colorDrawableTintGrey) DrawableHelper.tintDrawable(atDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_username, atDrawable) DrawableHelper.compoundDrawable(text_username, atDrawable)
} }
} }
......
...@@ -16,6 +16,7 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -16,6 +16,7 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val serverInteractor: SaveCurrentServerInteractor, private val serverInteractor: SaveCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor, private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor) { private val getAccountsInteractor: GetAccountsInteractor) {
fun connect(server: String) { fun connect(server: String) {
if (!server.isValidUrl()) { if (!server.isValidUrl()) {
view.showInvalidServerUrlMessage() view.showInvalidServerUrlMessage()
......
...@@ -47,22 +47,30 @@ class ServerFragment : Fragment(), ServerView { ...@@ -47,22 +47,30 @@ class ServerFragment : Fragment(), ServerView {
override fun showInvalidServerUrlMessage() = showMessage(getString(R.string.msg_invalid_server_url)) override fun showInvalidServerUrlMessage() = showMessage(getString(R.string.msg_invalid_server_url))
override fun showLoading() { override fun showLoading() {
ui {
enableUserInput(false) enableUserInput(false)
view_loading.setVisible(true) view_loading.setVisible(true)
} }
}
override fun hideLoading() { override fun hideLoading() {
ui {
view_loading.setVisible(false) view_loading.setVisible(false)
enableUserInput(true) enableUserInput(true)
} }
}
override fun showMessage(resId: Int){ override fun showMessage(resId: Int){
ui {
showToast(resId) showToast(resId)
} }
}
override fun showMessage(message: String) { override fun showMessage(message: String) {
ui {
showToast(message) showToast(message)
} }
}
override fun showGenericErrorMessage() { override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error)) showMessage(getString(R.string.msg_generic_error))
......
...@@ -25,7 +25,7 @@ class SignupFragment : Fragment(), SignupView { ...@@ -25,7 +25,7 @@ class SignupFragment : Fragment(), SignupView {
} else { } else {
bottom_container.apply { bottom_container.apply {
postDelayed({ postDelayed({
setVisible(true) ui { setVisible(true) }
}, 3) }, 3)
} }
} }
...@@ -64,61 +64,77 @@ class SignupFragment : Fragment(), SignupView { ...@@ -64,61 +64,77 @@ class SignupFragment : Fragment(), SignupView {
} }
override fun alertBlankName() { override fun alertBlankName() {
ui {
vibrateSmartPhone() vibrateSmartPhone()
text_name.shake() text_name.shake()
text_name.requestFocus() text_name.requestFocus()
} }
}
override fun alertBlankUsername() { override fun alertBlankUsername() {
ui {
vibrateSmartPhone() vibrateSmartPhone()
text_username.shake() text_username.shake()
text_username.requestFocus() text_username.requestFocus()
} }
}
override fun alertEmptyPassword() { override fun alertEmptyPassword() {
ui {
vibrateSmartPhone() vibrateSmartPhone()
text_password.shake() text_password.shake()
text_password.requestFocus() text_password.requestFocus()
} }
}
override fun alertBlankEmail() { override fun alertBlankEmail() {
ui {
vibrateSmartPhone() vibrateSmartPhone()
text_email.shake() text_email.shake()
text_email.requestFocus() text_email.requestFocus()
} }
}
override fun showLoading() { override fun showLoading() {
ui {
enableUserInput(false) enableUserInput(false)
view_loading.setVisible(true) view_loading.setVisible(true)
} }
}
override fun hideLoading() { override fun hideLoading() {
ui {
view_loading.setVisible(false) view_loading.setVisible(false)
enableUserInput(true) enableUserInput(true)
} }
}
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
ui {
showToast(resId) showToast(resId)
} }
}
override fun showMessage(message: String) { override fun showMessage(message: String) {
ui {
showToast(message) showToast(message)
} }
}
override fun showGenericErrorMessage() { override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error)) showMessage(getString(R.string.msg_generic_error))
} }
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
activity?.apply { ui {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this) val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, it)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this) val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, it)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this) val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, it)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, this) val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, it)
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable) val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.wrapDrawables(drawables) DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey) DrawableHelper.tintDrawables(drawables, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_password, text_email), drawables) DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_password, text_email), drawables)
} }
} }
......
...@@ -63,39 +63,49 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -63,39 +63,49 @@ class TwoFAFragment : Fragment(), TwoFAView {
} }
override fun alertBlankTwoFactorAuthenticationCode() { override fun alertBlankTwoFactorAuthenticationCode() {
ui {
vibrateSmartPhone() vibrateSmartPhone()
text_two_factor_auth.shake() text_two_factor_auth.shake()
} }
}
override fun alertInvalidTwoFactorAuthenticationCode() { override fun alertInvalidTwoFactorAuthenticationCode() {
showMessage(getString(R.string.msg_invalid_2fa_code)) showMessage(getString(R.string.msg_invalid_2fa_code))
} }
override fun showLoading() { override fun showLoading() {
ui {
enableUserInput(false) enableUserInput(false)
view_loading.setVisible(true) view_loading.setVisible(true)
} }
}
override fun hideLoading() { override fun hideLoading() {
ui {
view_loading.setVisible(false) view_loading.setVisible(false)
enableUserInput(true) enableUserInput(true)
} }
}
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
ui {
showToast(resId) showToast(resId)
} }
}
override fun showMessage(message: String) { override fun showMessage(message: String) {
ui {
showToast(message) showToast(message)
} }
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
private fun tintEditTextDrawableStart() { private fun tintEditTextDrawableStart() {
activity?.apply { ui {
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, this) val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, it)
DrawableHelper.wrapDrawable(lockDrawable) DrawableHelper.wrapDrawable(lockDrawable)
DrawableHelper.tintDrawable(lockDrawable, this, R.color.colorDrawableTintGrey) DrawableHelper.tintDrawable(lockDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_two_factor_auth, lockDrawable) DrawableHelper.compoundDrawable(text_two_factor_auth, lockDrawable)
} }
} }
......
package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.net.Uri
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.viewmodel.AuthorAttachmentViewModel
import chat.rocket.android.util.extensions.content
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.common.util.ifNull
import kotlinx.android.synthetic.main.item_author_attachment.view.*
class AuthorAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<AuthorAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(author_attachment_container)
setupActionMenu(text_fields)
setupActionMenu(text_author_name)
}
}
override fun bindViews(data: AuthorAttachmentViewModel) {
with(itemView) {
data.icon?.let { icon ->
author_icon.isVisible = true
author_icon.setImageURI(icon)
}.ifNull {
author_icon.isGone = true
}
author_icon.setImageURI(data.icon)
text_author_name.content = data.name
data.fields?.let { fields ->
text_fields.content = fields
text_fields.isVisible = true
}.ifNull {
text_fields.isGone = true
}
text_author_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.attachmentUrl)))
}
}
}
}
\ No newline at end of file
...@@ -53,6 +53,10 @@ class ChatRoomAdapter( ...@@ -53,6 +53,10 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_message_attachment) val view = parent.inflate(R.layout.item_message_attachment)
MessageAttachmentViewHolder(view, actionsListener, reactionListener) MessageAttachmentViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.AUTHOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_author_attachment)
AuthorAttachmentViewHolder(view, actionsListener, reactionListener)
}
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
} }
...@@ -92,6 +96,7 @@ class ChatRoomAdapter( ...@@ -92,6 +96,7 @@ class ChatRoomAdapter(
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel) is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel) is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel) is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
is AuthorAttachmentViewHolder -> holder.bind(dataSet[position] as AuthorAttachmentViewModel)
} }
} }
...@@ -100,6 +105,7 @@ class ChatRoomAdapter( ...@@ -100,6 +105,7 @@ class ChatRoomAdapter(
return when (model) { return when (model) {
is MessageViewModel -> model.messageId.hashCode().toLong() is MessageViewModel -> model.messageId.hashCode().toLong()
is BaseFileAttachmentViewModel -> model.id is BaseFileAttachmentViewModel -> model.id
is AuthorAttachmentViewModel -> model.id
else -> return position.toLong() else -> return position.toLong()
} }
} }
...@@ -111,12 +117,22 @@ class ChatRoomAdapter( ...@@ -111,12 +117,22 @@ class ChatRoomAdapter(
} }
fun prependData(dataSet: List<BaseViewModel<*>>) { fun prependData(dataSet: List<BaseViewModel<*>>) {
val item = dataSet.firstOrNull { newItem -> val item = dataSet.indexOfFirst { newItem ->
this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1 this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1
} }
if (item == null) { if (item == -1) {
this.dataSet.addAll(0, dataSet) this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size) notifyItemRangeInserted(0, dataSet.size)
} else {
dataSet.forEach { item ->
val index = this.dataSet.indexOfFirst {
item.messageId == it.messageId && item.viewType == it.viewType
}
if (index > -1) {
this.dataSet[index] = item
notifyItemChanged(index)
}
}
} }
} }
......
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import android.graphics.Color
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageViewModel import chat.rocket.android.chatroom.viewmodel.MessageViewModel
...@@ -29,6 +30,9 @@ class MessageViewHolder( ...@@ -29,6 +30,9 @@ class MessageViewHolder(
text_sender.text = data.senderName text_sender.text = data.senderName
text_content.text = data.content text_content.text = data.content
image_avatar.setImageURI(data.avatar) image_avatar.setImageURI(data.avatar)
text_content.setTextColor(
if (data.isTemporary) Color.GRAY else Color.BLACK
)
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.chatroom.di
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.module.AppModule
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
...@@ -12,6 +12,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewMo ...@@ -12,6 +12,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewMo
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.username
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state import chat.rocket.android.server.infraestructure.state
...@@ -20,10 +21,11 @@ import chat.rocket.android.util.extensions.launchUI ...@@ -20,10 +21,11 @@ import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.SimpleUser
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
import chat.rocket.common.model.roomTypeOf import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.rest.* import chat.rocket.core.internal.rest.*
import chat.rocket.core.model.Command import chat.rocket.core.model.Command
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
...@@ -52,7 +54,8 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -52,7 +54,8 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val roomsRepository: RoomRepository, private val roomsRepository: RoomRepository,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
factory: ConnectionManagerFactory, factory: ConnectionManagerFactory,
private val mapper: ViewModelMapper) { private val mapper: ViewModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer) private val manager = factory.create(currentServer)
private val client = manager.client private val client = manager.client
...@@ -70,14 +73,18 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -70,14 +73,18 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
launchUI(strategy) { launchUI(strategy) {
view.showLoading() view.showLoading()
try { try {
val messages = if (offset == 0L) {
retryIO(description = "messages chatRoom: $chatRoomId, type: $chatRoomType, offset: $offset") { val localMessages = messagesRepository.getByRoomId(chatRoomId)
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result val oldMessages = mapper.map(localMessages)
if (oldMessages.isNotEmpty()) {
view.showMessages(oldMessages)
loadMissingMessages()
} else {
loadAndShowMessages(chatRoomId, chatRoomType, offset)
}
} else {
loadAndShowMessages(chatRoomId, chatRoomType, offset)
} }
messagesRepository.saveAll(messages)
val messagesViewModels = mapper.map(messages)
view.showMessages(messagesViewModels)
// TODO: For now we are marking the room as read if we can get the messages (I mean, no exception occurs) // TODO: For now we are marking the room as read if we can get the messages (I mean, no exception occurs)
// but should mark only when the user see the first unread message. // but should mark only when the user see the first unread message.
...@@ -85,7 +92,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -85,7 +92,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
subscribeMessages(chatRoomId) subscribeMessages(chatRoomId)
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace() Timber.e(ex)
ex.message?.let { ex.message?.let {
view.showMessage(it) view.showMessage(it)
}.ifNull { }.ifNull {
...@@ -101,28 +108,58 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -101,28 +108,58 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} }
} }
private suspend fun loadAndShowMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
val messages =
retryIO(description = "messages chatRoom: $chatRoomId, type: $chatRoomType, offset: $offset") {
client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
}
messagesRepository.saveAll(messages)
val allMessages = mapper.map(messages)
view.showMessages(allMessages)
}
fun sendMessage(chatRoomId: String, text: String, messageId: String?) { fun sendMessage(chatRoomId: String, text: String, messageId: String?) {
launchUI(strategy) { launchUI(strategy) {
view.disableSendMessageButton()
try { try {
// ignore message for now, will receive it on the stream // ignore message for now, will receive it on the stream
val message = retryIO {
if (messageId == null) {
val id = UUID.randomUUID().toString() val id = UUID.randomUUID().toString()
val message = if (messageId == null) {
val username = localRepository.username()
val newMessage = Message(
id = id,
roomId = chatRoomId,
message = text,
timestamp = Instant.now().toEpochMilli(),
sender = SimpleUser(null, username, username),
attachments = null,
avatar = currentServer.avatarUrl(username!!),
channels = null,
editedAt = null,
editedBy = null,
groupable = false,
parseUrls = false,
pinned = false,
mentions = emptyList(),
reactions = null,
senderAlias = null,
type = null,
updatedAt = null,
urls = null,
isTemporary = true
)
messagesRepository.save(newMessage)
view.showNewMessage(mapper.map(newMessage))
client.sendMessage(id, chatRoomId, text) client.sendMessage(id, chatRoomId, text)
} else { } else {
client.updateMessage(chatRoomId, messageId, text) client.updateMessage(chatRoomId, messageId, text)
} }
}
view.enableSendMessageButton(false) view.enableSendMessageButton()
} catch (ex: Exception) { } catch (ex: Exception) {
Timber.d(ex, "Error sending message...") Timber.d(ex, "Error sending message...")
ex.message?.let { jobSchedulerInteractor.scheduleSendingMessages()
view.showMessage(it) } finally {
}.ifNull { view.enableSendMessageButton()
view.showGenericErrorMessage()
}
view.enableSendMessageButton(true)
} }
} }
} }
...@@ -189,6 +226,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -189,6 +226,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} }
if (state is State.Connected) { if (state is State.Connected) {
jobSchedulerInteractor.scheduleSendingMessages()
loadMissingMessages() loadMissingMessages()
} }
} }
...@@ -563,12 +601,13 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -563,12 +601,13 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
// failed, command is not valid so post it // failed, command is not valid so post it
sendMessage(roomId, text, null) sendMessage(roomId, text, null)
} }
view.enableSendMessageButton(false)
} }
} catch (ex: RocketChatException) { } catch (ex: RocketChatException) {
Timber.e(ex) Timber.e(ex)
// command is not valid, post it // command is not valid, post it
sendMessage(roomId, text, null) sendMessage(roomId, text, null)
} finally {
view.enableSendMessageButton()
} }
} }
} }
......
...@@ -7,7 +7,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewMo ...@@ -7,7 +7,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewMo
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.socket.model.State
interface ChatRoomView : LoadingView, MessageView { interface ChatRoomView : LoadingView, MessageView {
...@@ -92,10 +92,8 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -92,10 +92,8 @@ interface ChatRoomView : LoadingView, MessageView {
/** /**
* Enables the send message button. * Enables the send message button.
*
* @param sendFailed Whether the sent message has failed.
*/ */
fun enableSendMessageButton(sendFailed: Boolean) fun enableSendMessageButton()
/** /**
* Clears the message composition. * Clears the message composition.
...@@ -105,7 +103,9 @@ interface ChatRoomView : LoadingView, MessageView { ...@@ -105,7 +103,9 @@ interface ChatRoomView : LoadingView, MessageView {
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int) fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
fun showConnectionState(state: State) fun showConnectionState(state: State)
fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>) fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>)
fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>) fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>)
/** /**
* This user has joined the chat callback. * This user has joined the chat callback.
......
package chat.rocket.android.chatroom.service
import android.app.job.JobParameters
import android.app.job.JobService
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.model.Message
import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
class MessageService : JobService() {
@Inject
lateinit var factory: ConnectionManagerFactory
@Inject
lateinit var currentServerRepository: CurrentServerRepository
@Inject
lateinit var messageRepository: MessagesRepository
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onStopJob(params: JobParameters?): Boolean {
return false
}
override fun onStartJob(params: JobParameters?): Boolean {
launch(CommonPool) {
val currentServer = currentServerRepository.get()
if (currentServer != null) {
retrySendingMessages(params, currentServer)
jobFinished(params, false)
}
}
return true
}
private suspend fun retrySendingMessages(params: JobParameters?, currentServer: String) {
val temporaryMessages = messageRepository.getAllUnsent()
.sortedWith(compareBy(Message::timestamp))
if (temporaryMessages.isNotEmpty()) {
val connectionManager = factory.create(currentServer)
val client = connectionManager.client
temporaryMessages.forEach { message ->
try {
client.sendMessage(
message = message.message,
messageId = message.id,
roomId = message.roomId,
avatar = message.avatar,
attachments = message.attachments,
alias = message.senderAlias
)
messageRepository.save(message.copy(isTemporary = false))
Timber.d("Sent scheduled message given by id: ${message.id}")
} catch (ex: RocketChatException) {
Timber.e(ex)
if (ex.message?.contains("E11000", true) == true) {
// XXX: Temporary solution. We need proper error codes from the api.
messageRepository.save(message.copy(isTemporary = false))
}
jobFinished(params, true)
}
}
}
}
companion object {
const val RETRY_SEND_MESSAGE_ID = 1
}
}
\ No newline at end of file
...@@ -4,7 +4,6 @@ import android.Manifest ...@@ -4,7 +4,6 @@ import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
...@@ -18,6 +17,7 @@ import android.support.v7.widget.DefaultItemAnimator ...@@ -18,6 +17,7 @@ import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.* import android.view.*
import androidx.core.content.systemService
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.* import chat.rocket.android.chatroom.adapter.*
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
...@@ -32,7 +32,7 @@ import chat.rocket.android.helper.KeyboardHelper ...@@ -32,7 +32,7 @@ import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.emoji.* import chat.rocket.android.widget.emoji.*
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.socket.model.State
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.fragment_chat_room.* import kotlinx.android.synthetic.main.fragment_chat_room.*
...@@ -41,7 +41,6 @@ import kotlinx.android.synthetic.main.message_composer.* ...@@ -41,7 +41,6 @@ import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.* import kotlinx.android.synthetic.main.message_list.*
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.absoluteValue
fun newInstance(chatRoomId: String, fun newInstance(chatRoomId: String,
chatRoomName: String, chatRoomName: String,
...@@ -70,8 +69,10 @@ private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen" ...@@ -70,8 +69,10 @@ private const val BUNDLE_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val BUNDLE_CHAT_ROOM_IS_SUBSCRIBED = "chat_room_is_subscribed" private const val BUNDLE_CHAT_ROOM_IS_SUBSCRIBED = "chat_room_is_subscribed"
class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener { class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiReactionListener {
@Inject lateinit var presenter: ChatRoomPresenter @Inject
@Inject lateinit var parser: MessageParser lateinit var presenter: ChatRoomPresenter
@Inject
lateinit var parser: MessageParser
private lateinit var adapter: ChatRoomAdapter private lateinit var adapter: ChatRoomAdapter
private lateinit var chatRoomId: String private lateinit var chatRoomId: String
private lateinit var chatRoomName: String private lateinit var chatRoomName: String
...@@ -183,6 +184,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -183,6 +184,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
override fun showMessages(dataSet: List<BaseViewModel<*>>) { override fun showMessages(dataSet: List<BaseViewModel<*>>) {
ui {
// track the message sent immediately after the current message // track the message sent immediately after the current message
var prevMessageViewModel: MessageViewModel? = null var prevMessageViewModel: MessageViewModel? = null
...@@ -204,7 +206,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -204,7 +206,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
activity?.apply {
if (recycler_view.adapter == null) { if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter, adapter = ChatRoomAdapter(chatRoomType, chatRoomName, presenter,
reactionListener = this@ChatRoomFragment) reactionListener = this@ChatRoomFragment)
...@@ -282,6 +283,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -282,6 +283,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
override fun sendMessage(text: String) { override fun sendMessage(text: String) {
ui {
if (!text.isBlank()) { if (!text.isBlank()) {
if (!text.startsWith("/")) { if (!text.startsWith("/")) {
presenter.sendMessage(chatRoomId, text, editingMessageId) presenter.sendMessage(chatRoomId, text, editingMessageId)
...@@ -290,6 +292,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -290,6 +292,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
} }
}
override fun uploadFile(uri: Uri) { override fun uploadFile(uri: Uri) {
// TODO Just leaving a blank message that comes with the file for now. In the future lets add the possibility to add a message with the file to be uploaded. // TODO Just leaving a blank message that comes with the file for now. In the future lets add the possibility to add a message with the file to be uploaded.
...@@ -301,43 +304,54 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -301,43 +304,54 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
override fun showNewMessage(message: List<BaseViewModel<*>>) { override fun showNewMessage(message: List<BaseViewModel<*>>) {
ui {
adapter.prependData(message) adapter.prependData(message)
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0) verticalScrollOffset.set(0)
} }
}
override fun disableSendMessageButton() { override fun disableSendMessageButton() {
ui {
button_send.isEnabled = false button_send.isEnabled = false
} }
}
override fun enableSendMessageButton(sendFailed: Boolean) { override fun enableSendMessageButton() {
ui {
button_send.isEnabled = true button_send.isEnabled = true
text_message.isEnabled = true text_message.isEnabled = true
if (!sendFailed) {
clearMessageComposition() clearMessageComposition()
} }
} }
override fun clearMessageComposition() { override fun clearMessageComposition() {
ui {
citation = null citation = null
editingMessageId = null editingMessageId = null
text_message.textContent = "" text_message.textContent = ""
actionSnackbar.dismiss() actionSnackbar.dismiss()
} }
}
override fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>) { override fun dispatchUpdateMessage(index: Int, message: List<BaseViewModel<*>>) {
ui {
adapter.updateItem(message.last()) adapter.updateItem(message.last())
if (message.size > 1) { if (message.size > 1) {
adapter.prependData(listOf(message.first())) adapter.prependData(listOf(message.first()))
} }
} }
}
override fun dispatchDeleteMessage(msgId: String) { override fun dispatchDeleteMessage(msgId: String) {
ui {
adapter.removeItem(msgId) adapter.removeItem(msgId)
} }
}
override fun showReplyingAction(username: String, replyMarkdown: String, quotedMessage: String) { override fun showReplyingAction(username: String, replyMarkdown: String, quotedMessage: String) {
activity?.apply { ui {
citation = replyMarkdown citation = replyMarkdown
actionSnackbar.title = username actionSnackbar.title = username
actionSnackbar.text = quotedMessage actionSnackbar.text = quotedMessage
...@@ -352,41 +366,55 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -352,41 +366,55 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
} }
override fun showLoading() = view_loading.setVisible(true) override fun showLoading() {
ui { view_loading.setVisible(true) }
}
override fun hideLoading() = view_loading.setVisible(false) override fun hideLoading() {
ui { view_loading.setVisible(false) }
}
override fun showMessage(message: String) { override fun showMessage(message: String) {
ui {
showToast(message) showToast(message)
} }
}
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
ui {
showToast(resId) showToast(resId)
} }
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>) { override fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>) {
ui {
suggestions_view.addItems("@", members) suggestions_view.addItems("@", members)
} }
}
override fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>) { override fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>) {
ui {
suggestions_view.addItems("#", chatRooms) suggestions_view.addItems("#", chatRooms)
} }
}
override fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>) { override fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>) {
ui {
suggestions_view.addItems("/", commands) suggestions_view.addItems("/", commands)
} }
}
override fun copyToClipboard(message: String) { override fun copyToClipboard(message: String) {
activity?.apply { ui {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard: ClipboardManager = it.systemService()
clipboard.primaryClip = ClipData.newPlainText("", message) clipboard.primaryClip = ClipData.newPlainText("", message)
} }
} }
override fun showEditingAction(roomId: String, messageId: String, text: String) { override fun showEditingAction(roomId: String, messageId: String, text: String) {
activity?.apply { ui {
actionSnackbar.title = getString(R.string.action_title_editing) actionSnackbar.title = getString(R.string.action_title_editing)
actionSnackbar.text = text actionSnackbar.text = text
actionSnackbar.show() actionSnackbar.show()
...@@ -422,7 +450,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -422,7 +450,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
override fun showReactionsPopup(messageId: String) { override fun showReactionsPopup(messageId: String) {
context?.let { ui {
val emojiPickerPopup = EmojiPickerPopup(it) val emojiPickerPopup = EmojiPickerPopup(it)
emojiPickerPopup.listener = object : EmojiListenerAdapter() { emojiPickerPopup.listener = object : EmojiListenerAdapter() {
override fun onEmojiAdded(emoji: Emoji) { override fun onEmojiAdded(emoji: Emoji) {
...@@ -435,11 +463,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -435,11 +463,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private fun setReactionButtonIcon(@DrawableRes drawableId: Int) { private fun setReactionButtonIcon(@DrawableRes drawableId: Int) {
button_add_reaction.setImageResource(drawableId) button_add_reaction.setImageResource(drawableId)
button_add_reaction.setTag(drawableId) button_add_reaction.tag = drawableId
} }
override fun showFileSelection(filter: Array<String>) { override fun showFileSelection(filter: Array<String>) {
activity?.let { ui {
if (ContextCompat.checkSelfPermission(it, Manifest.permission.READ_EXTERNAL_STORAGE) if (ContextCompat.checkSelfPermission(it, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) { != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(it, ActivityCompat.requestPermissions(it,
...@@ -459,7 +487,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -459,7 +487,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
1 -> { 1 -> {
if (!(grantResults.isNotEmpty() && grantResults.first() == PackageManager.PERMISSION_GRANTED)) { if (!(grantResults.isNotEmpty() && grantResults.first() == PackageManager.PERMISSION_GRANTED)) {
handler.postDelayed({ handler.postDelayed({
hideAttachmentOptions() ui { hideAttachmentOptions() }
}, 400) }, 400)
} }
} }
...@@ -471,7 +499,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -471,7 +499,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
override fun showConnectionState(state: State) { override fun showConnectionState(state: State) {
activity?.apply { ui {
connection_status_text.fadeIn() connection_status_text.fadeIn()
handler.removeCallbacks(dismissStatus) handler.removeCallbacks(dismissStatus)
when (state) { when (state) {
...@@ -489,11 +517,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -489,11 +517,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
override fun onJoined() { override fun onJoined() {
ui {
input_container.setVisible(true) input_container.setVisible(true)
button_join_chat.setVisible(false) button_join_chat.setVisible(false)
isSubscribed = true isSubscribed = true
setupMessageComposer() setupMessageComposer()
} }
}
private val dismissStatus = { private val dismissStatus = {
connection_status_text.fadeOut() connection_status_text.fadeOut()
...@@ -559,6 +589,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -559,6 +589,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
var textMessage = citation ?: "" var textMessage = citation ?: ""
textMessage += text_message.textContent textMessage += text_message.textContent
sendMessage(textMessage) sendMessage(textMessage)
clearMessageComposition()
} }
button_show_attachment_options.setOnClickListener { button_show_attachment_options.setOnClickListener {
......
...@@ -16,6 +16,7 @@ import chat.rocket.android.chatroom.viewmodel.BaseViewModel ...@@ -16,6 +16,7 @@ import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_pinned_messages.* import kotlinx.android.synthetic.main.fragment_pinned_messages.*
import javax.inject.Inject import javax.inject.Inject
...@@ -62,22 +63,30 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -62,22 +63,30 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
presenter.loadPinnedMessages(chatRoomId) presenter.loadPinnedMessages(chatRoomId)
} }
override fun showLoading() = view_loading.setVisible(true) override fun showLoading() {
ui { view_loading.setVisible(true) }
}
override fun hideLoading() = view_loading.setVisible(false) override fun hideLoading() {
ui { view_loading.setVisible(false) }
}
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
ui {
showToast(resId) showToast(resId)
} }
}
override fun showMessage(message: String) { override fun showMessage(message: String) {
ui {
showToast(message) showToast(message)
} }
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) { override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
activity?.apply { ui {
if (recycler_view_pinned.adapter == null) { if (recycler_view_pinned.adapter == null) {
// TODO - add a better constructor for this case... // TODO - add a better constructor for this case...
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, null, false) adapter = ChatRoomAdapter(chatRoomType, chatRoomName, null, false)
...@@ -92,9 +101,22 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -92,9 +101,22 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
} }
}) })
} }
togglePinView(pinnedMessages.size)
} }
adapter.appendData(pinnedMessages) adapter.appendData(pinnedMessages)
} }
} }
private fun togglePinView(size: Int) {
if (size == 0){
iv_pin_icon.setVisible(true)
tv_pin_title.setVisible(true)
tv_pin_description.setVisible(true)
}else{
iv_pin_icon.setVisible(false)
tv_pin_title.setVisible(false)
tv_pin_description.setVisible(false)
}
}
} }
\ No newline at end of file
...@@ -13,7 +13,8 @@ data class AudioAttachmentViewModel( ...@@ -13,7 +13,8 @@ data class AudioAttachmentViewModel(
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<AudioAttachment> { ) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.AuthorAttachment
data class AuthorAttachmentViewModel(
override val attachmentUrl: String,
val id: Long,
val name: CharSequence?,
val icon: String?,
val fields: CharSequence?,
override val message: Message,
override val rawData: AuthorAttachment,
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseAttachmentViewModel<AuthorAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUTHOR_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_author_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
interface BaseAttachmentViewModel<out T> : BaseViewModel<T> {
val attachmentUrl: String
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel package chat.rocket.android.chatroom.viewmodel
interface BaseFileAttachmentViewModel<out T> : BaseViewModel<T> { interface BaseFileAttachmentViewModel<out T> : BaseAttachmentViewModel<T> {
val attachmentUrl: String
val attachmentTitle: CharSequence val attachmentTitle: CharSequence
val id: Long val id: Long
} }
\ No newline at end of file
...@@ -12,6 +12,7 @@ interface BaseViewModel<out T> { ...@@ -12,6 +12,7 @@ interface BaseViewModel<out T> {
var reactions: List<ReactionViewModel> var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>? var nextDownStreamMessage: BaseViewModel<*>?
var preview: Message? var preview: Message?
var isTemporary: Boolean
enum class ViewType(val viewType: Int) { enum class ViewType(val viewType: Int) {
MESSAGE(0), MESSAGE(0),
...@@ -20,7 +21,8 @@ interface BaseViewModel<out T> { ...@@ -20,7 +21,8 @@ interface BaseViewModel<out T> {
IMAGE_ATTACHMENT(3), IMAGE_ATTACHMENT(3),
VIDEO_ATTACHMENT(4), VIDEO_ATTACHMENT(4),
AUDIO_ATTACHMENT(5), AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6) MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7)
} }
} }
......
...@@ -13,7 +13,8 @@ data class ImageAttachmentViewModel( ...@@ -13,7 +13,8 @@ data class ImageAttachmentViewModel(
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<ImageAttachment> { ) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
...@@ -14,7 +14,8 @@ data class MessageAttachmentViewModel( ...@@ -14,7 +14,8 @@ data class MessageAttachmentViewModel(
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null, var messageLink: String? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Message> { ) : BaseViewModel<Message> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
......
...@@ -15,7 +15,8 @@ data class MessageViewModel( ...@@ -15,7 +15,8 @@ data class MessageViewModel(
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null, override var preview: Message? = null,
var isFirstUnread: Boolean var isFirstUnread: Boolean,
override var isTemporary: Boolean = false
) : BaseMessageViewModel<Message> { ) : BaseMessageViewModel<Message> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType get() = BaseViewModel.ViewType.MESSAGE.viewType
......
...@@ -14,7 +14,8 @@ data class UrlPreviewViewModel( ...@@ -14,7 +14,8 @@ data class UrlPreviewViewModel(
val thumbUrl: String?, val thumbUrl: String?,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Url> { ) : BaseViewModel<Url> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
...@@ -13,7 +13,8 @@ data class VideoAttachmentViewModel( ...@@ -13,7 +13,8 @@ data class VideoAttachmentViewModel(
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<VideoAttachment> { ) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
...@@ -4,14 +4,20 @@ import DateTimeHelper ...@@ -4,14 +4,20 @@ import DateTimeHelper
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.support.v4.content.ContextCompat
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan import android.text.style.StyleSpan
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.text.scale
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.util.extensions.avatarUrl import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.isNotNullNorEmpty
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType import chat.rocket.core.model.MessageType
...@@ -39,6 +45,7 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -39,6 +45,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
private val baseUrl = settings.baseUrl() private val baseUrl = settings.baseUrl()
private val token = tokenRepository.get(currentServer) private val token = tokenRepository.get(currentServer)
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY) private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
private val secundaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
suspend fun map(message: Message): List<BaseViewModel<*>> { suspend fun map(message: Message): List<BaseViewModel<*>> {
return translate(message) return translate(message)
...@@ -98,10 +105,39 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -98,10 +105,39 @@ class ViewModelMapper @Inject constructor(private val context: Context,
return when (attachment) { return when (attachment) {
is FileAttachment -> mapFileAttachment(message, attachment) is FileAttachment -> mapFileAttachment(message, attachment)
is MessageAttachment -> mapMessageAttachment(message, attachment) is MessageAttachment -> mapMessageAttachment(message, attachment)
is AuthorAttachment -> mapAuthorAttachment(message, attachment)
else -> null else -> null
} }
} }
private suspend fun mapAuthorAttachment(message: Message, attachment: AuthorAttachment): AuthorAttachmentViewModel {
return with(attachment) {
val content = stripMessageQuotes(message)
val fieldsText = fields?.let {
buildSpannedString {
it.forEachIndexed { index, field ->
bold { append(field.title) }
append("\n")
if (field.value.isNotEmpty()) {
append(field.value)
}
if (index != it.size - 1) { // it is not the last one, append a new line
append("\n\n")
}
}
}
}
val id = attachmentId(message, attachment)
AuthorAttachmentViewModel(attachmentUrl = url, id = id, name = authorName,
icon = authorIcon, fields = fieldsText, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message))
}
}
private suspend fun mapMessageAttachment(message: Message, attachment: MessageAttachment): MessageAttachmentViewModel { private suspend fun mapMessageAttachment(message: Message, attachment: MessageAttachment): MessageAttachmentViewModel {
val attachmentAuthor = attachment.author val attachmentAuthor = attachment.author
val time = attachment.timestamp?.let { getTime(it) } val time = attachment.timestamp?.let { getTime(it) }
...@@ -136,7 +172,7 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -136,7 +172,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
} }
} }
private fun attachmentId(message: Message, attachment: FileAttachment): Long { private fun attachmentId(message: Message, attachment: Attachment): Long {
return "${message.id}_${attachment.url}".hashCode().toLong() return "${message.id}_${attachment.url}".hashCode().toLong()
} }
...@@ -176,12 +212,13 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -176,12 +212,13 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val time = getTime(message.timestamp) val time = getTime(message.timestamp)
val avatar = getUserAvatar(message) val avatar = getUserAvatar(message)
val preview = mapMessagePreview(message) val preview = mapMessagePreview(message)
val isTemp = message.isTemporary ?: false
val content = getContent(stripMessageQuotes(message)) val content = getContent(stripMessageQuotes(message))
MessageViewModel(message = stripMessageQuotes(message), rawData = message, MessageViewModel(message = stripMessageQuotes(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender, messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, reactions = getReactions(message), content = content, isPinned = message.pinned, reactions = getReactions(message),
isFirstUnread = false, preview = preview) isFirstUnread = false, preview = preview, isTemporary = isTemp)
} }
private suspend fun mapMessagePreview(message: Message): Message { private suspend fun mapMessagePreview(message: Message): Message {
...@@ -218,11 +255,21 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -218,11 +255,21 @@ class ViewModelMapper @Inject constructor(private val context: Context,
} }
private fun getSenderName(message: Message): CharSequence { private fun getSenderName(message: Message): CharSequence {
if (!message.senderAlias.isNullOrEmpty()) { val username = message.sender?.username
return message.senderAlias!! message.senderAlias.isNotNullNorEmpty { alias ->
return buildSpannedString {
append(alias)
username?.let {
append(" ")
scale(0.8f) {
color(secundaryTextColor) {
append("@$username")
}
}
}
}
} }
val username = message.sender?.username
val realName = message.sender?.name val realName = message.sender?.name
val senderName = if (settings.useRealName()) realName else username val senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString() return senderName ?: username.toString()
......
...@@ -19,9 +19,9 @@ import chat.rocket.common.model.RoomType ...@@ -19,9 +19,9 @@ import chat.rocket.common.model.RoomType
import chat.rocket.common.model.SimpleUser import chat.rocket.common.model.SimpleUser
import chat.rocket.common.model.User import chat.rocket.common.model.User
import chat.rocket.core.internal.model.Subscription import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.realtime.StreamMessage import chat.rocket.core.internal.realtime.socket.model.StreamMessage
import chat.rocket.core.internal.realtime.Type import chat.rocket.core.internal.realtime.socket.model.Type
import chat.rocket.core.internal.rest.spotlight import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Room import chat.rocket.core.model.Room
...@@ -40,6 +40,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -40,6 +40,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor, private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor, private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val viewModelMapper: ViewModelMapper, private val viewModelMapper: ViewModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor,
settingsRepository: SettingsRepository, settingsRepository: SettingsRepository,
factory: ConnectionManagerFactory) { factory: ConnectionManagerFactory) {
private val manager: ConnectionManager = factory.create(serverInteractor.get()!!) private val manager: ConnectionManager = factory.create(serverInteractor.get()!!)
...@@ -258,6 +259,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -258,6 +259,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
} }
if (state is State.Connected) { if (state is State.Connected) {
jobSchedulerInteractor.scheduleSendingMessages()
reloadRooms() reloadRooms()
updateRooms() updateRooms()
} }
...@@ -270,6 +272,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -270,6 +272,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
// TODO - Temporary stuff, remove when adding DB support // TODO - Temporary stuff, remove when adding DB support
private suspend fun subscribeRoomUpdates() { private suspend fun subscribeRoomUpdates() {
manager.addStatusChannel(stateChannel) manager.addStatusChannel(stateChannel)
view.showConnectionState(client.state)
manager.addRoomsAndSubscriptionsChannel(subscriptionsChannel) manager.addRoomsAndSubscriptionsChannel(subscriptionsChannel)
launch(CommonPool + strategy.jobs) { launch(CommonPool + strategy.jobs) {
for (message in subscriptionsChannel) { for (message in subscriptionsChannel) {
......
...@@ -2,7 +2,7 @@ package chat.rocket.android.chatrooms.presentation ...@@ -2,7 +2,7 @@ package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
interface ChatRoomsView : LoadingView, MessageView { interface ChatRoomsView : LoadingView, MessageView {
......
...@@ -24,7 +24,6 @@ import chat.rocket.android.util.extensions.textContent ...@@ -24,7 +24,6 @@ import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_chat.view.* import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.* import kotlinx.android.synthetic.main.unread_messages_badge.view.*
...@@ -74,14 +73,17 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -74,14 +73,17 @@ class ChatRoomsAdapter(private val context: Context,
} }
private fun bindAvatar(chatRoom: ChatRoom, drawee: SimpleDraweeView) { private fun bindAvatar(chatRoom: ChatRoom, drawee: SimpleDraweeView) {
val avatarId = if (chatRoom.type is RoomType.DirectMessage) chatRoom.name else "@${chatRoom.name}" if (chatRoom.type is RoomType.DirectMessage) {
drawee.setImageURI(chatRoom.client.url.avatarUrl(avatarId)) drawee.setImageURI(chatRoom.client.url.avatarUrl(chatRoom.name))
} else {
drawee.setImageURI(chatRoom.client.url.avatarUrl(chatRoom.name, true))
}
} }
private fun bindName(chatRoom: ChatRoom, textView: TextView) { private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.textContent = chatRoom.name textView.textContent = chatRoom.name
var drawable = when (chatRoom.type) { val drawable = when (chatRoom.type) {
is RoomType.Channel -> { is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, context) DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, context)
} }
......
...@@ -17,24 +17,24 @@ import android.widget.RadioGroup ...@@ -17,24 +17,24 @@ import android.widget.RadioGroup
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.helper.ChatRoomsSortOrder import chat.rocket.android.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants import chat.rocket.android.helper.Constants
import chat.rocket.android.helper.SharedPreferenceHelper import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.DividerItemDecoration import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_rooms.* import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.NonCancellable.isActive
import kotlinx.coroutines.experimental.android.UI import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -101,8 +101,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -101,8 +101,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
override fun onOptionsItemSelected(item: MenuItem?): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item?.itemId) { when (item.itemId) {
R.id.action_sort -> { R.id.action_sort -> {
val dialogLayout = layoutInflater.inflate(R.layout.chatroom_sort_dialog, null) val dialogLayout = layoutInflater.inflate(R.layout.chatroom_sort_dialog, null)
val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY) val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY)
...@@ -154,9 +154,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -154,9 +154,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) { override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) {
activity?.apply {
listJob?.cancel() listJob?.cancel()
listJob = launch(UI) { listJob = ui {
val adapter = recycler_view.adapter as SimpleSectionedRecyclerViewAdapter val adapter = recycler_view.adapter as SimpleSectionedRecyclerViewAdapter
// FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android/issues/5ac2916c36c7b235275ccccf // FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android/issues/5ac2916c36c7b235275ccccf
// TODO - fix this bug to re-enable DiffUtil // TODO - fix this bug to re-enable DiffUtil
...@@ -174,30 +173,38 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -174,30 +173,38 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
} }
} }
}
override fun showNoChatRoomsToDisplay() = text_no_data_to_display.setVisible(true) override fun showNoChatRoomsToDisplay() {
ui { text_no_data_to_display.setVisible(true) }
}
override fun showLoading() = view_loading.setVisible(true) override fun showLoading(){
ui { view_loading.setVisible(true) }
}
override fun hideLoading() { override fun hideLoading() {
if (view_loading != null) { ui {
view_loading.setVisible(false) view_loading.setVisible(false)
} }
} }
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
ui {
showToast(resId) showToast(resId)
} }
}
override fun showMessage(message: String) { override fun showMessage(message: String) {
ui {
showToast(message) showToast(message)
} }
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showConnectionState(state: State) { override fun showConnectionState(state: State) {
activity?.apply { Timber.d("Got new state: $state")
ui {
connection_status_text.fadeIn() connection_status_text.fadeIn()
handler.removeCallbacks(dismissStatus) handler.removeCallbacks(dismissStatus)
when (state) { when (state) {
...@@ -221,22 +228,25 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -221,22 +228,25 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
} }
private fun setupToolbar() { private fun setupToolbar() {
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.title_chats) (activity as AppCompatActivity?)?.supportActionBar?.title = getString(R.string.title_chats)
} }
private fun setupRecyclerView() { private fun setupRecyclerView() {
activity?.apply { ui {
recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) recycler_view.layoutManager = LinearLayoutManager(it, LinearLayoutManager.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(this, recycler_view.addItemDecoration(DividerItemDecoration(it,
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start), resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start),
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end))) resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)))
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
// TODO - use a ViewModel Mapper instead of using settings on the adapter // TODO - use a ViewModel Mapper instead of using settings on the adapter
val baseAdapter = ChatRoomsAdapter(this, val baseAdapter = ChatRoomsAdapter(it,
settingsRepository.get(serverInteractor.get()!!), localRepository) { chatRoom -> presenter.loadChatRoom(chatRoom) } settingsRepository.get(serverInteractor.get()!!), localRepository) {
chatRoom -> presenter.loadChatRoom(chatRoom)
}
sectionedAdapter = SimpleSectionedRecyclerViewAdapter(this, R.layout.item_chatroom_header, R.id.text_chatroom_header, baseAdapter!!) sectionedAdapter = SimpleSectionedRecyclerViewAdapter(it,
R.layout.item_chatroom_header, R.id.text_chatroom_header, baseAdapter)
recycler_view.adapter = sectionedAdapter recycler_view.adapter = sectionedAdapter
} }
} }
......
...@@ -2,6 +2,7 @@ package chat.rocket.android.dagger ...@@ -2,6 +2,7 @@ package chat.rocket.android.dagger
import android.app.Application import android.app.Application
import chat.rocket.android.app.RocketChatApplication import chat.rocket.android.app.RocketChatApplication
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.module.ActivityBuilder import chat.rocket.android.dagger.module.ActivityBuilder
import chat.rocket.android.dagger.module.AppModule import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.dagger.module.ReceiverBuilder import chat.rocket.android.dagger.module.ReceiverBuilder
...@@ -29,6 +30,8 @@ interface AppComponent { ...@@ -29,6 +30,8 @@ interface AppComponent {
fun inject(service: FirebaseTokenService) fun inject(service: FirebaseTokenService)
fun inject(service: MessageService)
/*@Component.Builder /*@Component.Builder
abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/ abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/
} }
...@@ -2,15 +2,19 @@ package chat.rocket.android.dagger.module ...@@ -2,15 +2,19 @@ package chat.rocket.android.dagger.module
import android.app.Application import android.app.Application
import android.app.NotificationManager import android.app.NotificationManager
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.arch.persistence.room.Room import android.arch.persistence.room.Room
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.content.systemService import androidx.core.content.systemService
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.qualifier.ForFresco import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.helper.FrescoAuthInterceptor import chat.rocket.android.helper.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
...@@ -19,19 +23,17 @@ import chat.rocket.android.infrastructure.SharedPrefsLocalRepository ...@@ -19,19 +23,17 @@ import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.push.GroupedPush import chat.rocket.android.push.GroupedPush
import chat.rocket.android.push.PushManager import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository import chat.rocket.android.server.infraestructure.*
import chat.rocket.android.server.infraestructure.MemoryMessagesRepository
import chat.rocket.android.server.infraestructure.MemoryRoomRepository
import chat.rocket.android.server.infraestructure.MemoryUsersRepository
import chat.rocket.android.server.infraestructure.ServerDao
import chat.rocket.android.server.infraestructure.SharedPreferencesAccountsRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesSettingsRepository
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.util.AppJsonAdapterFactory import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
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.PlatformLogger import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.AttachmentAdapterFactory
import com.facebook.drawee.backends.pipeline.DraweeConfig import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.core.ImagePipelineConfig
...@@ -121,7 +123,7 @@ class AppModule { ...@@ -121,7 +123,7 @@ class AppModule {
@Provides @Provides
@ForFresco @ForFresco
@Singleton @Singleton
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor { fun provideFrescoAuthInterceptor(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
return FrescoAuthInterceptor(tokenRepository, currentServerInteractor) return FrescoAuthInterceptor(tokenRepository, currentServerInteractor)
} }
...@@ -202,10 +204,16 @@ class AppModule { ...@@ -202,10 +204,16 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideMoshi(): Moshi { fun provideMoshi(logger: PlatformLogger,
currentServerInteractor: GetCurrentServerInteractor):
Moshi {
val url = currentServerInteractor.get() ?: ""
return Moshi.Builder() return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY) .add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE) .add(AppJsonAdapterFactory.INSTANCE)
.add(AttachmentAdapterFactory(Logger(logger, url)))
.add(java.lang.Long::class.java, ISO8601Date::class.java, TimestampAdapter(CalendarISO8601Converter()))
.add(Long::class.java, ISO8601Date::class.java, TimestampAdapter(CalendarISO8601Converter()))
.build() .build()
} }
...@@ -217,8 +225,9 @@ class AppModule { ...@@ -217,8 +225,9 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideMessageRepository(): MessagesRepository { fun provideMessageRepository(context: Application, moshi: Moshi, currentServerInteractor: GetCurrentServerInteractor): MessagesRepository {
return MemoryMessagesRepository() val preferences = context.getSharedPreferences("messages", Context.MODE_PRIVATE)
return SharedPreferencesMessagesRepository(preferences, moshi, currentServerInteractor)
} }
@Provides @Provides
...@@ -261,8 +270,7 @@ class AppModule { ...@@ -261,8 +270,7 @@ class AppModule {
SharedPreferencesAccountsRepository(preferences, moshi) SharedPreferencesAccountsRepository(preferences, moshi)
@Provides @Provides
fun provideNotificationManager(context: Context): NotificationManager = fun provideNotificationManager(context: Context): NotificationManager = context.systemService()
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Provides @Provides
@Singleton @Singleton
...@@ -279,4 +287,22 @@ class AppModule { ...@@ -279,4 +287,22 @@ class AppModule {
getSettingsInteractor: GetSettingsInteractor): PushManager { getSettingsInteractor: GetSettingsInteractor): PushManager {
return PushManager(groupedPushes, manager, moshi, getAccountInteractor, getSettingsInteractor, context) return PushManager(groupedPushes, manager, moshi, getAccountInteractor, getSettingsInteractor, context)
} }
@Provides
fun provideJobScheduler(context: Application): JobScheduler {
return context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
}
@Provides
fun provideSendMessageJob(context: Application): JobInfo {
return JobInfo.Builder(MessageService.RETRY_SEND_MESSAGE_ID,
ComponentName(context, MessageService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build()
}
@Provides
fun provideJobSchedulerInteractor(jobScheduler: JobScheduler, jobInfo: JobInfo): JobSchedulerInteractor {
return JobSchedulerInteractorImpl(jobScheduler, jobInfo)
}
} }
\ No newline at end of file
package chat.rocket.android.dagger.module package chat.rocket.android.dagger.module
import chat.rocket.android.chatroom.di.MessageServiceProvider
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.push.FirebaseTokenService import chat.rocket.android.push.FirebaseTokenService
import chat.rocket.android.push.GcmListenerService import chat.rocket.android.push.GcmListenerService
import chat.rocket.android.push.di.FirebaseTokenServiceProvider import chat.rocket.android.push.di.FirebaseTokenServiceProvider
...@@ -14,4 +16,7 @@ import dagger.android.ContributesAndroidInjector ...@@ -14,4 +16,7 @@ import dagger.android.ContributesAndroidInjector
@ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class]) @ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class])
abstract fun bindGcmListenerService(): GcmListenerService abstract fun bindGcmListenerService(): GcmListenerService
@ContributesAndroidInjector(modules = [MessageServiceProvider::class])
abstract fun bindMessageService(): MessageService
} }
\ No newline at end of file
...@@ -24,4 +24,5 @@ interface LocalRepository { ...@@ -24,4 +24,5 @@ interface LocalRepository {
} }
} }
fun LocalRepository.checkIfMyself(username: String) = get(LocalRepository.CURRENT_USERNAME_KEY) == username fun LocalRepository.checkIfMyself(username: String) = username() == username
\ No newline at end of file fun LocalRepository.username() = get(LocalRepository.CURRENT_USERNAME_KEY)
\ No newline at end of file
...@@ -5,7 +5,7 @@ import android.view.ViewGroup ...@@ -5,7 +5,7 @@ import android.view.ViewGroup
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.core.internal.realtime.UserStatus import chat.rocket.common.model.UserStatus
private const val VIEW_TYPE_CHANGE_STATUS = 0 private const val VIEW_TYPE_CHANGE_STATUS = 0
private const val VIEW_TYPE_ACCOUNT = 1 private const val VIEW_TYPE_ACCOUNT = 1
......
...@@ -2,17 +2,17 @@ package chat.rocket.android.main.adapter ...@@ -2,17 +2,17 @@ package chat.rocket.android.main.adapter
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import chat.rocket.core.internal.realtime.UserStatus import chat.rocket.common.model.UserStatus
import kotlinx.android.synthetic.main.item_change_status.view.* import kotlinx.android.synthetic.main.item_change_status.view.*
class StatusViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class StatusViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(listener: (UserStatus) -> Unit) { fun bind(listener: (UserStatus) -> Unit) {
with(itemView) { with(itemView) {
text_online.setOnClickListener { listener(UserStatus.Online) } text_online.setOnClickListener { listener(UserStatus.Online()) }
text_away.setOnClickListener { listener(UserStatus.Away) } text_away.setOnClickListener { listener(UserStatus.Away()) }
text_busy.setOnClickListener { listener(UserStatus.Busy) } text_busy.setOnClickListener { listener(UserStatus.Busy()) }
text_invisible.setOnClickListener { listener(UserStatus.Offline) } text_invisible.setOnClickListener { listener(UserStatus.Offline()) }
} }
} }
} }
\ No newline at end of file
...@@ -15,13 +15,15 @@ import chat.rocket.android.util.extensions.serverLogoUrl ...@@ -15,13 +15,15 @@ import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatAuthException import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.UserStatus
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.UserStatus
import chat.rocket.core.internal.realtime.setDefaultStatus import chat.rocket.core.internal.realtime.setDefaultStatus
import chat.rocket.core.internal.rest.logout import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.channels.Channel
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
...@@ -35,7 +37,7 @@ class MainPresenter @Inject constructor( ...@@ -35,7 +37,7 @@ class MainPresenter @Inject constructor(
private val navHeaderMapper: NavHeaderViewModelMapper, private val navHeaderMapper: NavHeaderViewModelMapper,
private val saveAccountInteractor: SaveAccountInteractor, private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInterector: RemoveAccountInterector, private val removeAccountInteractor: RemoveAccountInteractor,
private val factory: RocketChatClientFactory, private val factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor, getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory managerFactory: ConnectionManagerFactory
...@@ -45,6 +47,8 @@ class MainPresenter @Inject constructor( ...@@ -45,6 +47,8 @@ class MainPresenter @Inject constructor(
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!) private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
private val userDataChannel = Channel<Myself>()
fun toChatList() = navigator.toChatList() fun toChatList() = navigator.toChatList()
fun toUserProfile() = navigator.toUserProfile() fun toUserProfile() = navigator.toUserProfile()
...@@ -55,7 +59,9 @@ class MainPresenter @Inject constructor( ...@@ -55,7 +59,9 @@ class MainPresenter @Inject constructor(
checkServerInfo() checkServerInfo()
launchUI(strategy) { launchUI(strategy) {
try { try {
val me = retryIO("me") { client.me() } val me = retryIO("me") {
client.me()
}
val model = navHeaderMapper.mapToViewModel(me) val model = navHeaderMapper.mapToViewModel(me)
saveAccount(model) saveAccount(model)
view.setupNavHeader(model, getAccountsInteractor.get()) view.setupNavHeader(model, getAccountsInteractor.get())
...@@ -74,23 +80,10 @@ class MainPresenter @Inject constructor( ...@@ -74,23 +80,10 @@ class MainPresenter @Inject constructor(
} }
} }
} }
subscribeMyselfUpdates()
} }
} }
private suspend fun saveAccount(viewModel: NavHeaderViewModel) {
val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it)
}
val account = Account(
currentServer,
icon,
viewModel.serverLogo,
viewModel.userDisplayName,
viewModel.userAvatar
)
saveAccountInteractor.save(account)
}
/** /**
* Logout from current server. * Logout from current server.
*/ */
...@@ -110,7 +103,7 @@ class MainPresenter @Inject constructor( ...@@ -110,7 +103,7 @@ class MainPresenter @Inject constructor(
try { try {
disconnect() disconnect()
removeAccountInterector.remove(currentServer) removeAccountInteractor.remove(currentServer)
tokenRepository.remove(currentServer) tokenRepository.remove(currentServer)
navigator.toNewServer() navigator.toNewServer()
} catch (ex: Exception) { } catch (ex: Exception) {
...@@ -121,24 +114,12 @@ class MainPresenter @Inject constructor( ...@@ -121,24 +114,12 @@ class MainPresenter @Inject constructor(
} }
} }
private suspend fun clearTokens() {
serverInteractor.clear()
val pushToken = localRepository.get(LocalRepository.KEY_PUSH_TOKEN)
if (pushToken != null) {
try {
retryIO("unregisterPushToken") { client.unregisterPushToken(pushToken) }
} catch (ex: Exception) {
Timber.d(ex, "Error unregistering push token")
}
}
localRepository.clearAllFromServer(currentServer)
}
fun connect() { fun connect() {
manager.connect() manager.connect()
} }
fun disconnect() { fun disconnect() {
manager.removeUserDataChannel(userDataChannel)
manager.disconnect() manager.disconnect()
} }
...@@ -175,4 +156,43 @@ class MainPresenter @Inject constructor( ...@@ -175,4 +156,43 @@ class MainPresenter @Inject constructor(
client.registerPushToken(token, getAccountsInteractor.get(), factory) client.registerPushToken(token, getAccountsInteractor.get(), factory)
} }
} }
private suspend fun saveAccount(viewModel: NavHeaderViewModel) {
val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it)
}
val account = Account(
currentServer,
icon,
viewModel.serverLogo,
viewModel.userDisplayName!!,
viewModel.userAvatar
)
saveAccountInteractor.save(account)
}
private suspend fun clearTokens() {
serverInteractor.clear()
val pushToken = localRepository.get(LocalRepository.KEY_PUSH_TOKEN)
if (pushToken != null) {
try {
retryIO("unregisterPushToken") { client.unregisterPushToken(pushToken) }
} catch (ex: Exception) {
Timber.d(ex, "Error unregistering push token")
}
}
localRepository.clearAllFromServer(currentServer)
}
private suspend fun subscribeMyselfUpdates() {
manager.addUserDataChannel(userDataChannel)
for (myself in userDataChannel) {
updateMyself(myself)
}
}
private suspend fun updateMyself(myself: Myself) {
val model = navHeaderMapper.mapToViewModel(myself)
view.setupNavHeader(model, getAccountsInteractor.get())
}
} }
\ No newline at end of file
...@@ -4,7 +4,7 @@ import chat.rocket.android.authentication.server.presentation.VersionCheckView ...@@ -4,7 +4,7 @@ import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.main.viewmodel.NavHeaderViewModel import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import chat.rocket.core.internal.realtime.UserStatus import chat.rocket.common.model.UserStatus
interface MainView : MessageView, VersionCheckView { interface MainView : MessageView, VersionCheckView {
......
...@@ -21,7 +21,7 @@ import chat.rocket.android.util.extensions.fadeIn ...@@ -21,7 +21,7 @@ import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.rotateBy import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.core.internal.realtime.UserStatus import chat.rocket.common.model.UserStatus
import com.google.android.gms.gcm.GoogleCloudMessaging import com.google.android.gms.gcm.GoogleCloudMessaging
import com.google.android.gms.iid.InstanceID import com.google.android.gms.iid.InstanceID
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
...@@ -95,16 +95,23 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp ...@@ -95,16 +95,23 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
override fun setupNavHeader(viewModel: NavHeaderViewModel, accounts: List<Account>) { override fun setupNavHeader(viewModel: NavHeaderViewModel, accounts: List<Account>) {
Timber.d("Setting up nav header: $viewModel") Timber.d("Setting up nav header: $viewModel")
with(headerLayout) { with(headerLayout) {
with(viewModel) {
if (userStatus != null) {
image_user_status.setImageDrawable( image_user_status.setImageDrawable(
DrawableHelper.getUserStatusDrawable( DrawableHelper.getUserStatusDrawable(userStatus, context)
viewModel.userStatus!!,
this.context
) )
) }
text_user_name.text = viewModel.userDisplayName if (userDisplayName != null) {
text_user_name.text = userDisplayName
}
if (userAvatar != null) {
image_avatar.setImageURI(userAvatar)
}
if (serverLogo != null) {
server_logo.setImageURI(serverLogo)
}
text_server_url.text = viewModel.serverUrl text_server_url.text = viewModel.serverUrl
image_avatar.setImageURI(viewModel.userAvatar) }
server_logo.setImageURI(viewModel.serverLogo)
setupAccountsList(headerLayout, accounts) setupAccountsList(headerLayout, accounts)
} }
} }
......
...@@ -4,7 +4,7 @@ import chat.rocket.common.model.UserStatus ...@@ -4,7 +4,7 @@ import chat.rocket.common.model.UserStatus
data class NavHeaderViewModel( data class NavHeaderViewModel(
val userDisplayName: String, val userDisplayName: String?,
val userStatus: UserStatus?, val userStatus: UserStatus?,
val userAvatar: String?, val userAvatar: String?,
val serverUrl: String, val serverUrl: String,
......
...@@ -23,10 +23,10 @@ class NavHeaderViewModelMapper @Inject constructor( ...@@ -23,10 +23,10 @@ class NavHeaderViewModelMapper @Inject constructor(
return NavHeaderViewModel(displayName, status, avatar, currentServer, logo) return NavHeaderViewModel(displayName, status, avatar, currentServer, logo)
} }
private fun mapDisplayName(me: Myself): String { private fun mapDisplayName(me: Myself): String? {
val username = me.username val username = me.username
val realName = me.name val realName = me.name
val senderName = if (settings.useRealName()) realName else username val senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString() return senderName ?: username
} }
} }
\ No newline at end of file
package chat.rocket.android.members.presentation package chat.rocket.android.members.presentation
import chat.rocket.android.chatroom.ui.ChatRoomActivity import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.member.ui.newInstance import chat.rocket.android.members.ui.newInstance
class MembersNavigator(internal val activity: ChatRoomActivity) { class MembersNavigator(internal val activity: ChatRoomActivity) {
......
package chat.rocket.android.member.ui package chat.rocket.android.members.ui
import android.os.Bundle import android.os.Bundle
import android.support.design.widget.BottomSheetDialogFragment import android.support.design.widget.BottomSheetDialogFragment
......
...@@ -19,6 +19,7 @@ import chat.rocket.android.members.viewmodel.MemberViewModel ...@@ -19,6 +19,7 @@ import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.widget.DividerItemDecoration import chat.rocket.android.widget.DividerItemDecoration
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_members.* import kotlinx.android.synthetic.main.fragment_members.*
...@@ -70,7 +71,7 @@ class MembersFragment : Fragment(), MembersView { ...@@ -70,7 +71,7 @@ class MembersFragment : Fragment(), MembersView {
} }
override fun showMembers(dataSet: List<MemberViewModel>, total: Long) { override fun showMembers(dataSet: List<MemberViewModel>, total: Long) {
activity?.apply { ui {
setupToolbar(total) setupToolbar(total)
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
adapter.prependData(dataSet) adapter.prependData(dataSet)
...@@ -84,8 +85,8 @@ class MembersFragment : Fragment(), MembersView { ...@@ -84,8 +85,8 @@ class MembersFragment : Fragment(), MembersView {
} else { } else {
adapter.appendData(dataSet) adapter.appendData(dataSet)
} }
if (this is ChatRoomActivity) { if (it is ChatRoomActivity) {
this.showRoomTypeIcon(false) it.showRoomTypeIcon(false)
} }
} }
} }
...@@ -98,29 +99,37 @@ class MembersFragment : Fragment(), MembersView { ...@@ -98,29 +99,37 @@ class MembersFragment : Fragment(), MembersView {
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
override fun showLoading() = view_loading.setVisible(true) override fun showLoading() {
ui { view_loading.setVisible(true) }
}
override fun hideLoading() = view_loading.setVisible(false) override fun hideLoading() {
ui { view_loading.setVisible(false) }
}
override fun showMessage(resId: Int) { override fun showMessage(resId: Int) {
ui {
showToast(resId) showToast(resId)
} }
}
override fun showMessage(message: String) { override fun showMessage(message: String) {
ui {
showToast(message) showToast(message)
} }
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
private fun setupRecyclerView() { private fun setupRecyclerView() {
activity?.apply { ui {
recycler_view.layoutManager = linearLayoutManager recycler_view.layoutManager = linearLayoutManager
recycler_view.addItemDecoration(DividerItemDecoration(this)) recycler_view.addItemDecoration(DividerItemDecoration(it))
recycler_view.adapter = adapter recycler_view.adapter = adapter
} }
} }
private fun setupToolbar(totalMembers: Long) { private fun setupToolbar(totalMembers: Long) {
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_members, totalMembers)) (activity as ChatRoomActivity?)?.setupToolbarTitle(getString(R.string.title_members, totalMembers))
} }
} }
\ No newline at end of file
...@@ -25,7 +25,7 @@ class MemberViewModel(private val member: User, private val settings: Map<String ...@@ -25,7 +25,7 @@ class MemberViewModel(private val member: User, private val settings: Map<String
private fun getUserAvatar(): String? { private fun getUserAvatar(): String? {
val username = member.username ?: "?" val username = member.username ?: "?"
return baseUrl?.let { return baseUrl?.let {
baseUrl.avatarUrl(username,"png") baseUrl.avatarUrl(username, format = "png")
} }
} }
......
...@@ -27,7 +27,7 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView, ...@@ -27,7 +27,7 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView,
view.showLoading() view.showLoading()
try { try {
val myself = retryIO("me") { client.me() } val myself = retryIO("me") { client.me() }
myselfId = myself.id myselfId = myself.id!!
val avatarUrl = serverUrl.avatarUrl(myself.username!!) val avatarUrl = serverUrl.avatarUrl(myself.username!!)
view.showProfile( view.showProfile(
avatarUrl, avatarUrl,
......
package chat.rocket.android.server.domain
interface JobSchedulerInteractor {
/**
* Schedule job to retry previously failed sending messages.
*/
fun scheduleSendingMessages()
}
\ No newline at end of file
...@@ -11,7 +11,7 @@ interface MessagesRepository { ...@@ -11,7 +11,7 @@ interface MessagesRepository {
* *
* @return The Message object given by the id or null if message wasn't found. * @return The Message object given by the id or null if message wasn't found.
*/ */
fun getById(id: String): Message? suspend fun getById(id: String): Message?
/** /**
* Get all messages from the current room id. * Get all messages from the current room id.
...@@ -19,7 +19,7 @@ interface MessagesRepository { ...@@ -19,7 +19,7 @@ interface MessagesRepository {
* @param rid The room id. * @param rid The room id.
* @return A list of Message objects for the room with given room id or an empty list. * @return A list of Message objects for the room with given room id or an empty list.
*/ */
fun getByRoomId(rid: String): List<Message> suspend fun getByRoomId(rid: String): List<Message>
/** /**
* Get most recent messages up to count different users. * Get most recent messages up to count different users.
...@@ -29,43 +29,47 @@ interface MessagesRepository { ...@@ -29,43 +29,47 @@ interface MessagesRepository {
* *
* @return List of last count messages. * @return List of last count messages.
*/ */
fun getRecentMessages(rid: String, count: Long): List<Message> suspend fun getRecentMessages(rid: String, count: Long): List<Message>
/** /**
* Get all messages. Use carefully! * Get all messages. Use carefully!
* *
* @return All messages or an empty list. * @return All messages or an empty list.
*/ */
fun getAll(): List<Message> suspend fun getAll(): List<Message>
/** /**
* Save a single message object. * Save a single message object.
* *
* @param The message object to saveAll. * @param The message object to saveAll.
*/ */
fun save(message: Message) suspend fun save(message: Message)
/** /**
* Save a list of messages. * Save a list of messages.
*/ */
fun saveAll(newMessages: List<Message>) suspend fun saveAll(newMessages: List<Message>)
/** /**
* Removes all messages. * Removes all messages.
*/ */
fun clear() suspend fun clear()
/** /**
* Remove message by id. * Remove message by id.
* *
* @param id The id of the message to remove. * @param id The id of the message to remove.
*/ */
fun removeById(id: String) suspend fun removeById(id: String)
/** /**
* Remove all messages from a given room. * Remove all messages from a given room.
* *
* @param rid The room id where messages are to be removed. * @param rid The room id where messages are to be removed.
*/ */
fun removeByRoomId(rid: String) suspend fun removeByRoomId(rid: String)
suspend fun getAllUnsent(): List<Message>
suspend fun getUnsentByRoomId(roomId: String): List<Message>
} }
\ No newline at end of file
...@@ -2,7 +2,7 @@ package chat.rocket.android.server.domain ...@@ -2,7 +2,7 @@ package chat.rocket.android.server.domain
import javax.inject.Inject import javax.inject.Inject
class RemoveAccountInterector @Inject constructor(val repository: AccountsRepository) { class RemoveAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun remove(serverUrl: String) { suspend fun remove(serverUrl: String) {
repository.remove(serverUrl) repository.remove(serverUrl)
} }
......
...@@ -2,31 +2,40 @@ package chat.rocket.android.server.infraestructure ...@@ -2,31 +2,40 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.common.model.BaseRoom import chat.rocket.common.model.BaseRoom
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.* import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.realtime.socket.connect
import chat.rocket.core.internal.realtime.socket.disconnect
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.realtime.socket.model.StreamMessage
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.subscribeRooms
import chat.rocket.core.internal.realtime.subscribeSubscriptions
import chat.rocket.core.internal.realtime.subscribeUserDataChanges
import chat.rocket.core.internal.rest.chatRooms import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
class ConnectionManager(internal val client: RocketChatClient) { class ConnectionManager(internal val client: RocketChatClient) {
private val statusChannelList = CopyOnWriteArrayList<Channel<State>>() private val statusChannelList = CopyOnWriteArrayList<Channel<State>>()
private val statusChannel = Channel<State>() private val statusChannel = Channel<State>(Channel.CONFLATED)
private var connectJob: Job? = null private var connectJob: Job? = null
private val roomAndSubscriptionChannels = ArrayList<Channel<StreamMessage<BaseRoom>>>() private val roomAndSubscriptionChannels = ArrayList<Channel<StreamMessage<BaseRoom>>>()
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>() private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val userDataChannels = ArrayList<Channel<Myself>>()
private val subscriptionIdMap = HashMap<String, String>() private val subscriptionIdMap = HashMap<String, String>()
private var subscriptionId: String? = null private var subscriptionId: String? = null
private var roomsId: String? = null private var roomsId: String? = null
private var userId: String? = null
fun connect() { fun connect() {
if (connectJob?.isActive == true if (connectJob?.isActive == true && (state !is State.Disconnected)) {
&& (state !is State.Disconnected)) {
Timber.d("Already connected, just returning...") Timber.d("Already connected, just returning...")
return return
} }
...@@ -51,6 +60,10 @@ class ConnectionManager(internal val client: RocketChatClient) { ...@@ -51,6 +60,10 @@ class ConnectionManager(internal val client: RocketChatClient) {
Timber.d("Subscribed to rooms: $id") Timber.d("Subscribed to rooms: $id")
roomsId = id roomsId = id
} }
client.subscribeUserDataChanges { _, id ->
Timber.d("Subscribed to the user: $id")
userId = id
}
resubscribeRooms() resubscribeRooms()
} }
...@@ -60,7 +73,8 @@ class ConnectionManager(internal val client: RocketChatClient) { ...@@ -60,7 +73,8 @@ class ConnectionManager(internal val client: RocketChatClient) {
} }
for (channel in statusChannelList) { for (channel in statusChannelList) {
channel.send(status) Timber.d("Sending status: $status to $channel")
channel.offer(status)
} }
} }
} }
...@@ -92,6 +106,15 @@ class ConnectionManager(internal val client: RocketChatClient) { ...@@ -92,6 +106,15 @@ class ConnectionManager(internal val client: RocketChatClient) {
} }
} }
launch(parent = connectJob) {
for (myself in client.userDataChannel) {
Timber.d("Got userData")
for (channel in userDataChannels) {
channel.send(myself)
}
}
}
client.connect() client.connect()
// Broadcast initial state... // Broadcast initial state...
...@@ -121,9 +144,15 @@ class ConnectionManager(internal val client: RocketChatClient) { ...@@ -121,9 +144,15 @@ class ConnectionManager(internal val client: RocketChatClient) {
fun removeStatusChannel(channel: Channel<State>) = statusChannelList.remove(channel) fun removeStatusChannel(channel: Channel<State>) = statusChannelList.remove(channel)
fun addRoomsAndSubscriptionsChannel(channel: Channel<StreamMessage<BaseRoom>>) = roomAndSubscriptionChannels.add(channel) fun addRoomsAndSubscriptionsChannel(channel: Channel<StreamMessage<BaseRoom>>) =
roomAndSubscriptionChannels.add(channel)
fun removeRoomsAndSubscriptionsChannel(channel: Channel<StreamMessage<BaseRoom>>) =
roomAndSubscriptionChannels.remove(channel)
fun addUserDataChannel(channel: Channel<Myself>) = userDataChannels.add(channel)
fun removeRoomsAndSubscriptionsChannel(channel: Channel<StreamMessage<BaseRoom>>) = roomAndSubscriptionChannels.remove(channel) fun removeUserDataChannel(channel: Channel<Myself>) = userDataChannels.remove(channel)
fun subscribeRoomMessages(roomId: String, channel: Channel<Message>) { fun subscribeRoomMessages(roomId: String, channel: Channel<Message>) {
val oldSub = roomMessagesChannels.put(roomId, channel) val oldSub = roomMessagesChannels.put(roomId, channel)
...@@ -149,8 +178,8 @@ class ConnectionManager(internal val client: RocketChatClient) { ...@@ -149,8 +178,8 @@ class ConnectionManager(internal val client: RocketChatClient) {
} }
} }
suspend fun ConnectionManager.chatRooms(timestamp: Long = 0, filterCustom: Boolean = true) suspend fun ConnectionManager.chatRooms(timestamp: Long = 0, filterCustom: Boolean = true) =
= client.chatRooms(timestamp, filterCustom) client.chatRooms(timestamp, filterCustom)
val ConnectionManager.state: State val ConnectionManager.state: State
get() = client.state get() = client.state
\ No newline at end of file
package chat.rocket.android.server.infraestructure
import android.app.job.JobInfo
import android.app.job.JobScheduler
import chat.rocket.android.server.domain.JobSchedulerInteractor
import timber.log.Timber
import javax.inject.Inject
/**
* Uses the Android framework [JobScheduler].
*/
class JobSchedulerInteractorImpl @Inject constructor(
private val jobScheduler: JobScheduler,
private val jobInfo: JobInfo
) : JobSchedulerInteractor {
override fun scheduleSendingMessages() {
Timber.d("Scheduling unsent messages to send...")
jobScheduler.schedule(jobInfo)
}
}
\ No newline at end of file
...@@ -2,45 +2,67 @@ package chat.rocket.android.server.infraestructure ...@@ -2,45 +2,67 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.MessagesRepository import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
class MemoryMessagesRepository : MessagesRepository { class MemoryMessagesRepository : MessagesRepository {
private val messages: HashMap<String, Message> = HashMap() private val messages: HashMap<String, Message> = HashMap()
override fun getById(id: String): Message? { override suspend fun getById(id: String): Message? = withContext(CommonPool) {
return messages[id] return@withContext messages[id]
} }
override fun getByRoomId(rid: String): List<Message> { override suspend fun getByRoomId(rid: String): List<Message> = withContext(CommonPool) {
return messages.filter { it.value.roomId == rid }.values.toList() return@withContext messages.filter { it.value.roomId == rid }.values.toList()
} }
override fun getRecentMessages(rid: String, count: Long): List<Message> { override suspend fun getRecentMessages(rid: String, count: Long): List<Message> = withContext(CommonPool) {
return getByRoomId(rid).sortedByDescending { it.timestamp } return@withContext getByRoomId(rid).sortedByDescending { it.timestamp }
.distinctBy { it.sender }.take(count.toInt()) .distinctBy { it.sender }.take(count.toInt())
} }
override fun getAll(): List<Message> = messages.values.toList() override suspend fun getAll(): List<Message> = withContext(CommonPool) {
return@withContext messages.values.toList()
}
override suspend fun getUnsentByRoomId(roomId: String): List<Message> = withContext(CommonPool) {
val allByRoomId = getByRoomId(roomId)
if (allByRoomId.isEmpty()) {
return@withContext emptyList<Message>()
}
return@withContext allByRoomId.filter { it.isTemporary ?: false && it.roomId == roomId }
}
override fun save(message: Message) { override suspend fun getAllUnsent(): List<Message> = withContext(CommonPool) {
val all = getAll()
if (all.isEmpty()) {
return@withContext emptyList<Message>()
}
return@withContext all.filter { it.isTemporary ?: false }
}
override suspend fun save(message: Message) = withContext(CommonPool) {
messages[message.id] = message messages[message.id] = message
} }
override fun saveAll(newMessages: List<Message>) { override suspend fun saveAll(newMessages: List<Message>) = withContext(CommonPool) {
for (msg in newMessages) { for (msg in newMessages) {
messages[msg.id] = msg messages[msg.id] = msg
} }
} }
override fun clear() { override suspend fun clear() = withContext(CommonPool) {
messages.clear() messages.clear()
} }
override fun removeById(id: String) { override suspend fun removeById(id: String) {
withContext(CommonPool) {
messages.remove(id) messages.remove(id)
} }
}
override fun removeByRoomId(rid: String) { override suspend fun removeByRoomId(rid: String) = withContext(CommonPool) {
val roomMessages = messages.filter { it.value.roomId == rid }.values val roomMessages = messages.filter { it.value.roomId == rid }.values
roomMessages.forEach { roomMessages.forEach {
messages.remove(it.roomId) messages.remove(it.roomId)
......
package chat.rocket.android.server.infraestructure package chat.rocket.android.server.infraestructure
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.content.edit import androidx.core.content.edit
import chat.rocket.android.server.domain.AccountsRepository import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.model.Account import chat.rocket.android.server.domain.model.Account
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
......
package chat.rocket.android.server.infraestructure
import android.content.SharedPreferences
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.model.Message
import com.squareup.moshi.Moshi
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
class SharedPreferencesMessagesRepository(
private val prefs: SharedPreferences,
private val moshi: Moshi,
private val currentServerInteractor: GetCurrentServerInteractor
) : MessagesRepository {
override suspend fun getById(id: String): Message? = withContext(CommonPool) {
currentServerInteractor.get()?.also { server ->
if (prefs.all.values.isEmpty()) {
return@withContext null
}
val adapter = moshi.adapter<Message>(Message::class.java)
val values = prefs.all.entries.filter { it.key.startsWith(server) }
.map { it.value } as Collection<String>
return@withContext values.map { adapter.fromJson(it) }.firstOrNull { it?.id == id }
}
return@withContext null
}
override suspend fun getByRoomId(rid: String): List<Message> = withContext(CommonPool) {
currentServerInteractor.get()?.also { server ->
val adapter = moshi.adapter<Message>(Message::class.java)
if (prefs.all.values.isEmpty()) {
return@withContext emptyList<Message>()
}
val values = prefs.all.entries.filter { it.key.startsWith(server) }
.map { it.value } as Collection<String>
return@withContext values.mapNotNull { adapter.fromJson(it) }.filter {
it.roomId == rid
}.toList().sortedWith(compareBy(Message::timestamp)).reversed()
}
return@withContext emptyList<Message>()
}
override suspend fun getRecentMessages(rid: String, count: Long): List<Message> = withContext(CommonPool) {
return@withContext getByRoomId(rid).sortedByDescending { it.timestamp }
.distinctBy { it.sender }.take(count.toInt())
}
override suspend fun getAll(): List<Message> = withContext(CommonPool) {
val adapter = moshi.adapter<Message>(Message::class.java)
if (prefs.all.values.isEmpty()) {
return@withContext emptyList<Message>()
}
currentServerInteractor.get()?.also { server ->
val values = prefs.all.entries.filter { it.key.startsWith(server) }
.map { it.value } as Collection<String>
return@withContext values.mapNotNull { adapter.fromJson(it) }
}
return@withContext emptyList<Message>()
}
override suspend fun getAllUnsent(): List<Message> = withContext(CommonPool) {
if (prefs.all.values.isEmpty()) {
return@withContext emptyList<Message>()
}
currentServerInteractor.get()?.also { server ->
val values = prefs.all.entries.filter { it.key.startsWith(server) }
.map { it.value } as Collection<String>
val adapter = moshi.adapter<Message>(Message::class.java)
return@withContext values.mapNotNull { adapter.fromJson(it) }
.filter { it.isTemporary ?: false }
}
return@withContext emptyList<Message>()
}
override suspend fun getUnsentByRoomId(roomId: String): List<Message> = withContext(CommonPool) {
val allByRoomId = getByRoomId(roomId)
if (allByRoomId.isEmpty()) {
return@withContext emptyList<Message>()
}
return@withContext allByRoomId.filter { it.isTemporary ?: false }
}
override suspend fun save(message: Message) {
withContext(CommonPool) {
currentServerInteractor.get()?.also {
val adapter = moshi.adapter<Message>(Message::class.java)
prefs.edit().putString("${it}_${message.id}", adapter.toJson(message)).apply()
}
}
}
override suspend fun saveAll(newMessages: List<Message>) {
withContext(CommonPool) {
currentServerInteractor.get()?.also {
val adapter = moshi.adapter<Message>(Message::class.java)
val editor = prefs.edit()
for (msg in newMessages) {
editor.putString("${it}_${msg.id}", adapter.toJson(msg))
}
editor.apply()
}
}
}
override suspend fun clear() = withContext(CommonPool) {
prefs.edit().clear().apply()
}
override suspend fun removeById(id: String) {
withContext(CommonPool) {
currentServerInteractor.get()?.also {
prefs.edit().putString("${it}_$id", null).apply()
}
}
}
override suspend fun removeByRoomId(rid: String) {
withContext(CommonPool) {
currentServerInteractor.get()?.also { server ->
val adapter = moshi.adapter<Message>(Message::class.java)
val editor = prefs.edit()
prefs.all.entries.forEach {
val value = it.value
if (value is String) {
val message = adapter.fromJson(value)
if (message?.roomId == rid) {
editor.putString("${server}_${message.id}", null)
}
}
}
editor.apply()
}
}
}
}
\ No newline at end of file
...@@ -25,7 +25,7 @@ class PasswordPresenter @Inject constructor (private val view: PasswordView, ...@@ -25,7 +25,7 @@ class PasswordPresenter @Inject constructor (private val view: PasswordView,
val me = retryIO("me") { client.me() } val me = retryIO("me") { client.me() }
retryIO("updateProfile(${me.id})") { retryIO("updateProfile(${me.id})") {
client.updateProfile(me.id, null, null, password, null) client.updateProfile(me.id!!, null, null, password, null)
} }
view.showPasswordSuccessfullyUpdatedMessage() view.showPasswordSuccessfullyUpdatedMessage()
......
...@@ -11,7 +11,10 @@ import chat.rocket.android.util.extensions.asObservable ...@@ -11,7 +11,10 @@ import chat.rocket.android.util.extensions.asObservable
import chat.rocket.android.util.extensions.inflate import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.textContent import chat.rocket.android.util.extensions.textContent
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
import chat.rocket.android.util.extensions.ui
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.rxkotlin.Observables import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.fragment_password.* import kotlinx.android.synthetic.main.fragment_password.*
import javax.inject.Inject import javax.inject.Inject
...@@ -19,6 +22,7 @@ import javax.inject.Inject ...@@ -19,6 +22,7 @@ import javax.inject.Inject
class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.ActionMode.Callback { class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.ActionMode.Callback {
@Inject lateinit var presenter: PasswordPresenter @Inject lateinit var presenter: PasswordPresenter
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private val disposables = CompositeDisposable()
companion object { companion object {
fun newInstance() = PasswordFragment() fun newInstance() = PasswordFragment()
...@@ -34,14 +38,21 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action ...@@ -34,14 +38,21 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
listenToChanges() disposables.add(listenToChanges())
}
override fun onDestroyView() {
disposables.clear()
super.onDestroyView()
} }
override fun hideLoading() { override fun hideLoading() {
ui {
layout_new_password.visibility = View.VISIBLE layout_new_password.visibility = View.VISIBLE
layout_confirm_password.visibility = View.VISIBLE layout_confirm_password.visibility = View.VISIBLE
view_loading.visibility = View.GONE view_loading.visibility = View.GONE
} }
}
override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean { override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
return when (menuItem.itemId) { return when (menuItem.itemId) {
...@@ -69,10 +80,12 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action ...@@ -69,10 +80,12 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action
} }
override fun showLoading() { override fun showLoading() {
ui {
layout_new_password.visibility = View.GONE layout_new_password.visibility = View.GONE
layout_confirm_password.visibility = View.GONE layout_confirm_password.visibility = View.GONE
view_loading.visibility = View.VISIBLE view_loading.visibility = View.VISIBLE
} }
}
override fun showPasswordFailsUpdateMessage(error: String?) { override fun showPasswordFailsUpdateMessage(error: String?) {
showToast("Password fails to update: " + error) showToast("Password fails to update: " + error)
...@@ -84,8 +97,9 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action ...@@ -84,8 +97,9 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action
private fun finishActionMode() = actionMode?.finish() private fun finishActionMode() = actionMode?.finish()
private fun listenToChanges() { private fun listenToChanges(): Disposable {
Observables.combineLatest(text_new_password.asObservable(), text_confirm_password.asObservable()).subscribe { return Observables.combineLatest(text_new_password.asObservable(),
text_confirm_password.asObservable()).subscribe {
val textPassword = text_new_password.textContent val textPassword = text_new_password.textContent
val textConfirmPassword = text_confirm_password.textContent val textConfirmPassword = text_confirm_password.textContent
...@@ -97,7 +111,9 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action ...@@ -97,7 +111,9 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action
} }
private fun showToast(msg: String?) { private fun showToast(msg: String?) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show() ui {
Toast.makeText(it, msg, Toast.LENGTH_LONG).show()
}
} }
private fun startActionMode() { private fun startActionMode() {
......
package chat.rocket.android.util.extensions
inline fun CharSequence?.isNotNullNorEmpty(block: (CharSequence) -> Unit) {
if (this != null && this.isNotEmpty()) {
block(this)
}
}
\ No newline at end of file
package chat.rocket.android.util.extensions
import android.os.Looper
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentActivity
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
inline fun Fragment.ui(crossinline block: (activity: FragmentActivity) -> Unit): Job? {
// Checking first for activity and view saves us from some synchronyzed and thread local checks
if (activity != null && view != null) {
// If we already are running on the Main Thread (UI Thread), just go ahead and execute the block
return if (Looper.getMainLooper() == Looper.myLooper()) {
block(activity!!)
null
} else {
// Launch a Job on the UI context and check again if the activity and view are still valid
launch(UI) {
if (activity != null && view != null) {
block(activity!!)
}
}
}
}
return null
}
\ No newline at end of file
...@@ -10,8 +10,13 @@ fun String.removeTrailingSlash(): String { ...@@ -10,8 +10,13 @@ fun String.removeTrailingSlash(): String {
} }
} }
fun String.avatarUrl(avatar: String, format: String = "jpeg") = fun String.avatarUrl(avatar: String, isGroupOrChannel: Boolean = false, format: String = "jpeg"): String {
return if (isGroupOrChannel) {
"${removeTrailingSlash()}/avatar/%23${avatar.removeTrailingSlash()}?format=$format"
} else {
"${removeTrailingSlash()}/avatar/${avatar.removeTrailingSlash()}?format=$format" "${removeTrailingSlash()}/avatar/${avatar.removeTrailingSlash()}?format=$format"
}
}
fun String.serverLogoUrl(favicon: String) = "${removeTrailingSlash()}/$favicon" fun String.serverLogoUrl(favicon: String) = "${removeTrailingSlash()}/$favicon"
......
...@@ -8,7 +8,7 @@ import android.os.Bundle ...@@ -8,7 +8,7 @@ import android.os.Bundle
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.net.toUri import androidx.core.net.toUri
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.util.extensions.decodeUrl import chat.rocket.android.util.extensions.decodeUrl
import chat.rocket.android.util.extensions.toJsonObject import chat.rocket.android.util.extensions.toJsonObject
......
<vector android:height="24dp" android:viewportHeight="800.0" <?xml version="1.0" encoding="utf-8"?>
android:viewportWidth="800.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> <vector xmlns:android="http://schemas.android.com/apk/res/android"
<path android:fillColor="@color/actionMenuColor" android:pathData="M511.8,390H520V84C567,67.8 596.3,42 596.3,16H203.7c0,26 29.3,51.8 76.3,68V390h8.2c-92.9,16 -157.5,61 -157.5,108H377v288h48V498h244.3C669.3,451 604.7,406 511.8,390z"/> android:viewportWidth="197.218"
android:viewportHeight="197.218"
android:width="197.218dp"
android:height="197.218dp">
<group
android:translateX="-570.396"
android:translateY="-306.782">
<path
android:pathData="M704.445 306.782l-6.785 6.785c-6.084 6.084 -7.622 14.712 -4.309 21.871l-44.068 35.44 -3.086 -3.086c-7.889 -7.889 -19.525 -7.889 -27.414 0l-8.944 8.953 87.821 87.811 8.934 -8.933c7.899 -7.899 7.899 -19.525 0 -27.433l-3.076 -3.077 36.051 -44.68c6.824 2.466 14.367 1.036 20.037 -4.624l8.008 -5.858 -63.169 -63.169zm-66.867 116.487l-67.182 66.857 0 13.874 13.864 0 66.867 -67.182 -13.549 -13.549z"
android:fillColor="@color/actionMenuColor"
/>
</group>
</vector> </vector>
\ No newline at end of file
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/colorPrimary" /> <solid android:color="@color/quoteBar" />
<size <size
android:width="4dp" android:width="4dp"
android:height="4dp" /> android:height="4dp" />
......
...@@ -32,4 +32,50 @@ ...@@ -32,4 +32,50 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" /> tools:visibility="visible" />
<ImageView
android:id="@+id/iv_pin_icon"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_pin_black_24dp"
android:tint="#AFADAF"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_pin_title"
app:layout_constraintVertical_chainStyle="packed"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_pin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_pinned_messages"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_pin_icon"
app:layout_constraintBottom_toTopOf="@id/tv_pin_description"
android:textSize="20sp"
android:layout_marginTop="24dp"
android:textStyle="bold"
android:textColor="#8B8B8B"
android:visibility="gone"
tools:visibility="visible"/>
<TextView
android:id="@+id/tv_pin_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_pinned_description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_pin_title"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginTop="16dp"
android:textAlignment="center"
android:textSize="16sp"
android:textColor="#c1c1c1"
android:visibility="gone"
tools:visibility="visible"/>
</android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/author_attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding">
<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:layout_marginStart="56dp"
android:background="@drawable/quote_vertical_bar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/recycler_view_reactions"/>
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/author_icon"
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_marginTop="6dp"
android:layout_marginStart="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="@id/quote_bar"
tools:src="@tools:sample/avatars"/>
<TextView
android:id="@+id/text_author_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/author_icon"
app:layout_constraintEnd_toEndOf="parent"
tools:text="#5571 - User profile from SSO must not have password change option" />
<TextView
android:id="@+id/text_fields"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@id/text_author_name"
app:layout_constraintStart_toStartOf="@id/text_author_name"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/text_fields" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -10,14 +10,14 @@ ...@@ -10,14 +10,14 @@
android:paddingTop="@dimen/chat_item_top_and_bottom_padding" android:paddingTop="@dimen/chat_item_top_and_bottom_padding"
android:paddingBottom="@dimen/chat_item_top_and_bottom_padding"> android:paddingBottom="@dimen/chat_item_top_and_bottom_padding">
<include <com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/layout_avatar" android:id="@+id/image_avatar"
layout="@layout/avatar"
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
app:roundedCornerRadius="3dp"
android:layout_marginTop="6dp" android:layout_marginTop="6dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"/>
<TextView <TextView
android:id="@+id/text_chat_name" android:id="@+id/text_chat_name"
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/layout_avatar" app:layout_constraintStart_toEndOf="@id/image_avatar"
android:textDirection="locale" android:textDirection="locale"
tools:text="General"/> tools:text="General"/>
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
app:layout_constraintTop_toBottomOf="@id/text_chat_name" app:layout_constraintTop_toBottomOf="@id/text_chat_name"
app:layout_constraintEnd_toStartOf="@id/layout_unread_messages_badge" app:layout_constraintEnd_toStartOf="@id/layout_unread_messages_badge"
android:textDirection="locale" android:textDirection="locale"
tools:text="You: Type something"/> tools:text="You: Type something that is very big and need at least to lines, or maybe even more"/>
<include <include
android:id="@+id/layout_unread_messages_badge" android:id="@+id/layout_unread_messages_badge"
......
...@@ -123,6 +123,8 @@ ...@@ -123,6 +123,8 @@
<!-- Pinned Messages --> <!-- Pinned Messages -->
<string name="title_pinned_messages">पिन किए गए संदेश</string> <string name="title_pinned_messages">पिन किए गए संदेश</string>
<string name="no_pinned_messages">कोई पिन संदेश नहीं</string>
<string name="no_pinned_description">सभी पिन किए गए संदेश यहां\nदिखाई देते हैं।</string>
<!-- Upload Messages --> <!-- Upload Messages -->
<string name="max_file_size_exceeded">फ़ाइल का आकार %1$d बाइट्स ने %2$d बाइट्स के अधिकतम अपलोड आकार को पार कर लिया है</string> <string name="max_file_size_exceeded">फ़ाइल का आकार %1$d बाइट्स ने %2$d बाइट्स के अधिकतम अपलोड आकार को पार कर लिया है</string>
......
...@@ -123,6 +123,8 @@ ...@@ -123,6 +123,8 @@
<!-- Pinned Messages --> <!-- Pinned Messages -->
<string name="title_pinned_messages">Mensagens Pinadas</string> <string name="title_pinned_messages">Mensagens Pinadas</string>
<string name="no_pinned_messages">Nenhuma mensagem pinada</string>
<string name="no_pinned_description">Todas as mensagens pinadas\naparecerão aqui</string>
<!-- Upload Messages --> <!-- Upload Messages -->
<string name="max_file_size_exceeded">Tamanho de arquivo (%1$d bytes) excedeu tamanho máximo de upload (%2$d bytes)</string> <string name="max_file_size_exceeded">Tamanho de arquivo (%1$d bytes) excedeu tamanho máximo de upload (%2$d bytes)</string>
......
...@@ -39,6 +39,8 @@ ...@@ -39,6 +39,8 @@
<color name="colorEmojiIcon">#FF767676</color> <color name="colorEmojiIcon">#FF767676</color>
<color name="quoteBar">#A0A0A0</color>
<!-- Suggestions --> <!-- Suggestions -->
<color name="suggestion_background_color">@android:color/white</color> <color name="suggestion_background_color">@android:color/white</color>
......
...@@ -124,6 +124,8 @@ ...@@ -124,6 +124,8 @@
<!-- Pinned Messages --> <!-- Pinned Messages -->
<string name="title_pinned_messages">Pinned Messages</string> <string name="title_pinned_messages">Pinned Messages</string>
<string name="no_pinned_messages">No pinned messages</string>
<string name="no_pinned_description">All the pinned messages\nappear here.</string>
<!-- Upload Messages --> <!-- Upload Messages -->
<string name="max_file_size_exceeded">File size %1$d bytes exceeded max upload size of %2$d bytes</string> <string name="max_file_size_exceeded">File size %1$d bytes exceeded max upload size of %2$d bytes</string>
......
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
...@@ -10,7 +10,7 @@ buildscript { ...@@ -10,7 +10,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.1.0' classpath 'com.android.tools.build:gradle:3.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"
classpath 'com.google.gms:google-services:3.2.0' classpath 'com.google.gms:google-services:3.2.0'
......
...@@ -11,7 +11,7 @@ ext { ...@@ -11,7 +11,7 @@ ext {
// Main dependencies // Main dependencies
support : '27.1.0', support : '27.1.0',
constraintLayout : '1.0.2', constraintLayout : '1.0.2',
androidKtx : '0.2', androidKtx : '0.3',
dagger : '2.14.1', dagger : '2.14.1',
exoPlayer : '2.6.0', exoPlayer : '2.6.0',
playServices : '11.8.0', playServices : '11.8.0',
......
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