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
- After checking out to `develop` branch as mentioned above, simply import the project in Android Studio.
#### v2+
- This version requires the [Kotlin SDK](https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK) for Rocket.Chat. Clone the Kotlin SDK in the **same directory** as the android repository by running `git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git`. Make sure that the android repository and the kotlin sdk have the same immediate parent directory.
- First, a build is required for the SDK. Change your directory to the SDK directory by running `cd Rocket.Chat.Kotlin.SDK/` in your terminal. Any of the following approaches can be followed to successfully build the SDK.
- **Command Line** - (Within the kotlin SDK directory) Run `./gradlew clean && ./gradlew assemble` to successfully build the project.
- **Android Studio** - Import the project in Android Studio. Go to `Build > Make Project` to build the SDK successfully.
After following the above methods, follow the following steps in your terminal window:
- This version requires the [Kotlin SDK](https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK) for Rocket.Chat. Clone the Kotlin SDK in by running `git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git`.
- First, a build is required for the SDK, so that required jar files are generated. Make sure that the android repository and the kotlin sdk have the same immediate parent directory. Change the current directory to `Rocket.Chat.Android/app` and run the `build-sdk.sh` which will result in creating of the required jar file `core*.jar` and `common*.jar` in `Rocket.Chat.Android/app/libs`,by the following steps in your terminal window:
```
cd ..
cd Rocket.Chat.Android/app/libs
ls
cd Rocket.Chat.Android/app
./build-sdk.sh
```
Two `jar` files will be found in this directory (the `common` and `core` jar files), this indicates that the SDK was built correctly.
- After the SDK has been built successfully, import the project in Android Studio and build it by following `Build > Make Project`.
## How to run
### Command Line
......
......@@ -13,8 +13,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 2008
versionName "2.0.0-beta6"
versionCode 2009
versionName "2.0.0-rc1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......
......@@ -20,7 +20,9 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config">
android:supportsRtl="true">
<activity
android:name=".authentication.ui.AuthenticationActivity"
android:configChanges="orientation"
......@@ -100,13 +102,18 @@
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<service
android:name=".chatroom.service.MessageService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<meta-data
android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
<activity android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme"/>
<activity
android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme" />
</application>
</manifest>
\ No newline at end of file
</manifest>
......@@ -2,7 +2,6 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
import android.support.v4.graphics.drawable.DrawableCompat
import android.widget.EditText
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.common.model.UserStatus
......@@ -16,7 +15,8 @@ object DrawableHelper {
* @param context The context.
* @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.
......@@ -68,7 +68,8 @@ object DrawableHelper {
* @see tintDrawables
* @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.
......@@ -96,13 +97,15 @@ object DrawableHelper {
* @param drawable The Drawable.
* @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.
*
* @param userStatus The user status.
* @param context The context.
* @see [UserStatus]
* @return The user status drawable.
*/
fun getUserStatusDrawable(userStatus: UserStatus, context: Context): Drawable {
......@@ -119,32 +122,4 @@ object DrawableHelper {
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
import android.content.BroadcastReceiver
import android.content.Context
import android.content.SharedPreferences
import androidx.content.edit
import androidx.core.content.edit
import chat.rocket.android.BuildConfig
import chat.rocket.android.app.migration.RealmMigration
import chat.rocket.android.app.migration.RocketChatLibraryModule
......
package chat.rocket.android.authentication.infraestructure
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.server.domain.TokenRepository
import chat.rocket.common.model.Token
......
......@@ -59,26 +59,36 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
}
override fun alertBlankUsername() {
vibrateSmartPhone()
text_username.shake()
ui {
vibrateSmartPhone()
text_username.shake()
}
}
override fun showLoading() {
disableUserInput()
view_loading.setVisible(true)
ui {
disableUserInput()
view_loading.setVisible(true)
}
}
override fun hideLoading() {
view_loading.setVisible(false)
enableUserInput()
ui {
view_loading.setVisible(false)
enableUserInput()
}
}
override fun showMessage(resId: Int) {
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
......@@ -86,10 +96,10 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
}
private fun tintEditTextDrawableStart() {
activity?.apply {
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
ui {
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, it)
DrawableHelper.wrapDrawable(atDrawable)
DrawableHelper.tintDrawable(atDrawable, this, R.color.colorDrawableTintGrey)
DrawableHelper.tintDrawable(atDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_username, atDrawable)
}
}
......
......@@ -16,6 +16,7 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val serverInteractor: SaveCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor) {
fun connect(server: String) {
if (!server.isValidUrl()) {
view.showInvalidServerUrlMessage()
......
......@@ -47,21 +47,29 @@ class ServerFragment : Fragment(), ServerView {
override fun showInvalidServerUrlMessage() = showMessage(getString(R.string.msg_invalid_server_url))
override fun showLoading() {
enableUserInput(false)
view_loading.setVisible(true)
ui {
enableUserInput(false)
view_loading.setVisible(true)
}
}
override fun hideLoading() {
view_loading.setVisible(false)
enableUserInput(true)
ui {
view_loading.setVisible(false)
enableUserInput(true)
}
}
override fun showMessage(resId: Int){
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
......
......@@ -25,7 +25,7 @@ class SignupFragment : Fragment(), SignupView {
} else {
bottom_container.apply {
postDelayed({
setVisible(true)
ui { setVisible(true) }
}, 3)
}
}
......@@ -64,45 +64,61 @@ class SignupFragment : Fragment(), SignupView {
}
override fun alertBlankName() {
vibrateSmartPhone()
text_name.shake()
text_name.requestFocus()
ui {
vibrateSmartPhone()
text_name.shake()
text_name.requestFocus()
}
}
override fun alertBlankUsername() {
vibrateSmartPhone()
text_username.shake()
text_username.requestFocus()
ui {
vibrateSmartPhone()
text_username.shake()
text_username.requestFocus()
}
}
override fun alertEmptyPassword() {
vibrateSmartPhone()
text_password.shake()
text_password.requestFocus()
ui {
vibrateSmartPhone()
text_password.shake()
text_password.requestFocus()
}
}
override fun alertBlankEmail() {
vibrateSmartPhone()
text_email.shake()
text_email.requestFocus()
ui {
vibrateSmartPhone()
text_email.shake()
text_email.requestFocus()
}
}
override fun showLoading() {
enableUserInput(false)
view_loading.setVisible(true)
ui {
enableUserInput(false)
view_loading.setVisible(true)
}
}
override fun hideLoading() {
view_loading.setVisible(false)
enableUserInput(true)
ui {
view_loading.setVisible(false)
enableUserInput(true)
}
}
override fun showMessage(resId: Int) {
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
......@@ -110,15 +126,15 @@ class SignupFragment : Fragment(), SignupView {
}
private fun tintEditTextDrawableStart() {
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, this)
ui {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, it)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, it)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, it)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, it)
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
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)
}
}
......
......@@ -63,8 +63,10 @@ class TwoFAFragment : Fragment(), TwoFAView {
}
override fun alertBlankTwoFactorAuthenticationCode() {
vibrateSmartPhone()
text_two_factor_auth.shake()
ui {
vibrateSmartPhone()
text_two_factor_auth.shake()
}
}
override fun alertInvalidTwoFactorAuthenticationCode() {
......@@ -72,30 +74,38 @@ class TwoFAFragment : Fragment(), TwoFAView {
}
override fun showLoading() {
enableUserInput(false)
view_loading.setVisible(true)
ui {
enableUserInput(false)
view_loading.setVisible(true)
}
}
override fun hideLoading() {
view_loading.setVisible(false)
enableUserInput(true)
ui {
view_loading.setVisible(false)
enableUserInput(true)
}
}
override fun showMessage(resId: Int) {
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
private fun tintEditTextDrawableStart() {
activity?.apply {
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, this)
ui {
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, it)
DrawableHelper.wrapDrawable(lockDrawable)
DrawableHelper.tintDrawable(lockDrawable, this, R.color.colorDrawableTintGrey)
DrawableHelper.tintDrawable(lockDrawable, it, R.color.colorDrawableTintGrey)
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(
val view = parent.inflate(R.layout.item_message_attachment)
MessageAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.AUTHOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_author_attachment)
AuthorAttachmentViewHolder(view, actionsListener, reactionListener)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
......@@ -92,6 +96,7 @@ class ChatRoomAdapter(
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
is AuthorAttachmentViewHolder -> holder.bind(dataSet[position] as AuthorAttachmentViewModel)
}
}
......@@ -100,6 +105,7 @@ class ChatRoomAdapter(
return when (model) {
is MessageViewModel -> model.messageId.hashCode().toLong()
is BaseFileAttachmentViewModel -> model.id
is AuthorAttachmentViewModel -> model.id
else -> return position.toLong()
}
}
......@@ -111,12 +117,22 @@ class ChatRoomAdapter(
}
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
}
if (item == null) {
if (item == -1) {
this.dataSet.addAll(0, dataSet)
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
import android.graphics.Color
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
......@@ -29,6 +30,9 @@ class MessageViewHolder(
text_sender.text = data.senderName
text_content.text = data.content
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
......@@ -7,7 +7,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewMo
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.behaviours.LoadingView
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 {
......@@ -92,10 +92,8 @@ interface ChatRoomView : LoadingView, MessageView {
/**
* Enables the send message button.
*
* @param sendFailed Whether the sent message has failed.
*/
fun enableSendMessageButton(sendFailed: Boolean)
fun enableSendMessageButton()
/**
* Clears the message composition.
......@@ -105,7 +103,9 @@ interface ChatRoomView : LoadingView, MessageView {
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
fun showConnectionState(state: State)
fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>)
fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>)
/**
* 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
......@@ -16,6 +16,7 @@ import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_pinned_messages.*
import javax.inject.Inject
......@@ -62,22 +63,30 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
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) {
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
activity?.apply {
ui {
if (recycler_view_pinned.adapter == null) {
// TODO - add a better constructor for this case...
adapter = ChatRoomAdapter(chatRoomType, chatRoomName, null, false)
......@@ -92,9 +101,22 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
}
})
}
togglePinView(pinnedMessages.size)
}
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(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int
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
interface BaseFileAttachmentViewModel<out T> : BaseViewModel<T> {
val attachmentUrl: String
interface BaseFileAttachmentViewModel<out T> : BaseAttachmentViewModel<T> {
val attachmentTitle: CharSequence
val id: Long
}
\ No newline at end of file
......@@ -12,6 +12,7 @@ interface BaseViewModel<out T> {
var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>?
var preview: Message?
var isTemporary: Boolean
enum class ViewType(val viewType: Int) {
MESSAGE(0),
......@@ -20,7 +21,8 @@ interface BaseViewModel<out T> {
IMAGE_ATTACHMENT(3),
VIDEO_ATTACHMENT(4),
AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6)
MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7)
}
}
......
......@@ -13,7 +13,8 @@ data class ImageAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
......@@ -14,7 +14,8 @@ data class MessageAttachmentViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
......
......@@ -15,7 +15,8 @@ data class MessageViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
var isFirstUnread: Boolean
var isFirstUnread: Boolean,
override var isTemporary: Boolean = false
) : BaseMessageViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType
......
......@@ -14,7 +14,8 @@ data class UrlPreviewViewModel(
val thumbUrl: String?,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Url> {
override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
......@@ -13,7 +13,8 @@ data class VideoAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
......@@ -4,14 +4,20 @@ import DateTimeHelper
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.support.v4.content.ContextCompat
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
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.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.isNotNullNorEmpty
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
......@@ -39,6 +45,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
private val baseUrl = settings.baseUrl()
private val token = tokenRepository.get(currentServer)
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<*>> {
return translate(message)
......@@ -98,10 +105,39 @@ class ViewModelMapper @Inject constructor(private val context: Context,
return when (attachment) {
is FileAttachment -> mapFileAttachment(message, attachment)
is MessageAttachment -> mapMessageAttachment(message, attachment)
is AuthorAttachment -> mapAuthorAttachment(message, attachment)
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 {
val attachmentAuthor = attachment.author
val time = attachment.timestamp?.let { getTime(it) }
......@@ -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()
}
......@@ -176,12 +212,13 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val time = getTime(message.timestamp)
val avatar = getUserAvatar(message)
val preview = mapMessagePreview(message)
val isTemp = message.isTemporary ?: false
val content = getContent(stripMessageQuotes(message))
MessageViewModel(message = stripMessageQuotes(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
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 {
......@@ -218,11 +255,21 @@ class ViewModelMapper @Inject constructor(private val context: Context,
}
private fun getSenderName(message: Message): CharSequence {
if (!message.senderAlias.isNullOrEmpty()) {
return message.senderAlias!!
val username = message.sender?.username
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 senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString()
......
......@@ -2,7 +2,7 @@ package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.behaviours.LoadingView
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
interface ChatRoomsView : LoadingView, MessageView {
......
......@@ -24,7 +24,6 @@ import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom
import com.facebook.drawee.view.SimpleDraweeView
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_chat.view.*
import kotlinx.android.synthetic.main.unread_messages_badge.view.*
......@@ -74,14 +73,17 @@ class ChatRoomsAdapter(private val context: Context,
}
private fun bindAvatar(chatRoom: ChatRoom, drawee: SimpleDraweeView) {
val avatarId = if (chatRoom.type is RoomType.DirectMessage) chatRoom.name else "@${chatRoom.name}"
drawee.setImageURI(chatRoom.client.url.avatarUrl(avatarId))
if (chatRoom.type is RoomType.DirectMessage) {
drawee.setImageURI(chatRoom.client.url.avatarUrl(chatRoom.name))
} else {
drawee.setImageURI(chatRoom.client.url.avatarUrl(chatRoom.name, true))
}
}
private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.textContent = chatRoom.name
var drawable = when (chatRoom.type) {
val drawable = when (chatRoom.type) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, context)
}
......
......@@ -17,24 +17,24 @@ import android.widget.RadioGroup
import chat.rocket.android.R
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants
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.SettingsRepository
import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.DividerItemDecoration
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 dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.NonCancellable.isActive
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
......@@ -101,8 +101,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_sort -> {
val dialogLayout = layoutInflater.inflate(R.layout.chatroom_sort_dialog, null)
val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY)
......@@ -154,50 +154,57 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) {
activity?.apply {
listJob?.cancel()
listJob = launch(UI) {
val adapter = recycler_view.adapter as SimpleSectionedRecyclerViewAdapter
// FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android/issues/5ac2916c36c7b235275ccccf
// TODO - fix this bug to re-enable DiffUtil
/*val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet))
}.await()*/
if (isActive) {
adapter.baseAdapter.updateRooms(newDataSet)
// TODO - fix crash to re-enable diff.dispatchUpdatesTo(adapter)
adapter.notifyDataSetChanged()
//Set sections always after data set is updated
setSections()
}
listJob?.cancel()
listJob = ui {
val adapter = recycler_view.adapter as SimpleSectionedRecyclerViewAdapter
// FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android/issues/5ac2916c36c7b235275ccccf
// TODO - fix this bug to re-enable DiffUtil
/*val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet))
}.await()*/
if (isActive) {
adapter.baseAdapter.updateRooms(newDataSet)
// TODO - fix crash to re-enable diff.dispatchUpdatesTo(adapter)
adapter.notifyDataSetChanged()
//Set sections always after data set is updated
setSections()
}
}
}
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() {
if (view_loading != null) {
ui {
view_loading.setVisible(false)
}
}
override fun showMessage(resId: Int) {
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showConnectionState(state: State) {
activity?.apply {
Timber.d("Got new state: $state")
ui {
connection_status_text.fadeIn()
handler.removeCallbacks(dismissStatus)
when (state) {
......@@ -221,22 +228,25 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
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() {
activity?.apply {
recycler_view.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(this,
ui {
recycler_view.layoutManager = LinearLayoutManager(it, LinearLayoutManager.VERTICAL, false)
recycler_view.addItemDecoration(DividerItemDecoration(it,
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start),
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)))
recycler_view.itemAnimator = DefaultItemAnimator()
// TODO - use a ViewModel Mapper instead of using settings on the adapter
val baseAdapter = ChatRoomsAdapter(this,
settingsRepository.get(serverInteractor.get()!!), localRepository) { chatRoom -> presenter.loadChatRoom(chatRoom) }
val baseAdapter = ChatRoomsAdapter(it,
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
}
}
......
......@@ -2,6 +2,7 @@ package chat.rocket.android.dagger
import android.app.Application
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.AppModule
import chat.rocket.android.dagger.module.ReceiverBuilder
......@@ -29,6 +30,8 @@ interface AppComponent {
fun inject(service: FirebaseTokenService)
fun inject(service: MessageService)
/*@Component.Builder
abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/
}
......@@ -2,15 +2,19 @@ package chat.rocket.android.dagger.module
import android.app.Application
import android.app.NotificationManager
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.arch.persistence.room.Room
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import androidx.content.systemService
import androidx.core.content.systemService
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
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.helper.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser
......@@ -19,19 +23,17 @@ import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.push.GroupedPush
import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository
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.server.infraestructure.*
import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger
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.core.RocketChatClient
import chat.rocket.core.internal.AttachmentAdapterFactory
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig
......@@ -121,7 +123,7 @@ class AppModule {
@Provides
@ForFresco
@Singleton
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
fun provideFrescoAuthInterceptor(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
return FrescoAuthInterceptor(tokenRepository, currentServerInteractor)
}
......@@ -202,10 +204,16 @@ class AppModule {
@Provides
@Singleton
fun provideMoshi(): Moshi {
fun provideMoshi(logger: PlatformLogger,
currentServerInteractor: GetCurrentServerInteractor):
Moshi {
val url = currentServerInteractor.get() ?: ""
return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.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()
}
......@@ -217,8 +225,9 @@ class AppModule {
@Provides
@Singleton
fun provideMessageRepository(): MessagesRepository {
return MemoryMessagesRepository()
fun provideMessageRepository(context: Application, moshi: Moshi, currentServerInteractor: GetCurrentServerInteractor): MessagesRepository {
val preferences = context.getSharedPreferences("messages", Context.MODE_PRIVATE)
return SharedPreferencesMessagesRepository(preferences, moshi, currentServerInteractor)
}
@Provides
......@@ -261,8 +270,7 @@ class AppModule {
SharedPreferencesAccountsRepository(preferences, moshi)
@Provides
fun provideNotificationManager(context: Context): NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
fun provideNotificationManager(context: Context): NotificationManager = context.systemService()
@Provides
@Singleton
......@@ -279,4 +287,22 @@ class AppModule {
getSettingsInteractor: GetSettingsInteractor): PushManager {
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
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.GcmListenerService
import chat.rocket.android.push.di.FirebaseTokenServiceProvider
......@@ -14,4 +16,7 @@ import dagger.android.ContributesAndroidInjector
@ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class])
abstract fun bindGcmListenerService(): GcmListenerService
@ContributesAndroidInjector(modules = [MessageServiceProvider::class])
abstract fun bindMessageService(): MessageService
}
\ No newline at end of file
......@@ -24,4 +24,5 @@ interface LocalRepository {
}
}
fun LocalRepository.checkIfMyself(username: String) = get(LocalRepository.CURRENT_USERNAME_KEY) == username
\ No newline at end of file
fun LocalRepository.checkIfMyself(username: String) = username() == username
fun LocalRepository.username() = get(LocalRepository.CURRENT_USERNAME_KEY)
\ No newline at end of file
......@@ -5,7 +5,7 @@ import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.server.domain.model.Account
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_ACCOUNT = 1
......
......@@ -2,17 +2,17 @@ package chat.rocket.android.main.adapter
import android.support.v7.widget.RecyclerView
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.*
class StatusViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(listener: (UserStatus) -> Unit) {
with(itemView) {
text_online.setOnClickListener { listener(UserStatus.Online) }
text_away.setOnClickListener { listener(UserStatus.Away) }
text_busy.setOnClickListener { listener(UserStatus.Busy) }
text_invisible.setOnClickListener { listener(UserStatus.Offline) }
text_online.setOnClickListener { listener(UserStatus.Online()) }
text_away.setOnClickListener { listener(UserStatus.Away()) }
text_busy.setOnClickListener { listener(UserStatus.Busy()) }
text_invisible.setOnClickListener { listener(UserStatus.Offline()) }
}
}
}
\ No newline at end of file
......@@ -15,36 +15,40 @@ import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.UserStatus
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.UserStatus
import chat.rocket.core.internal.realtime.setDefaultStatus
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.channels.Channel
import timber.log.Timber
import javax.inject.Inject
class MainPresenter @Inject constructor(
private val view: MainView,
private val strategy: CancelStrategy,
private val navigator: MainNavigator,
private val tokenRepository: TokenRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository,
private val navHeaderMapper: NavHeaderViewModelMapper,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInterector: RemoveAccountInterector,
private val factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory
private val view: MainView,
private val strategy: CancelStrategy,
private val navigator: MainNavigator,
private val tokenRepository: TokenRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository,
private val navHeaderMapper: NavHeaderViewModelMapper,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInteractor: RemoveAccountInteractor,
private val factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory
) : CheckServerPresenter(strategy, client = factory.create(serverInteractor.get()!!), view = view) {
private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
private val userDataChannel = Channel<Myself>()
fun toChatList() = navigator.toChatList()
fun toUserProfile() = navigator.toUserProfile()
......@@ -55,7 +59,9 @@ class MainPresenter @Inject constructor(
checkServerInfo()
launchUI(strategy) {
try {
val me = retryIO("me") { client.me() }
val me = retryIO("me") {
client.me()
}
val model = navHeaderMapper.mapToViewModel(me)
saveAccount(model)
view.setupNavHeader(model, getAccountsInteractor.get())
......@@ -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.
*/
......@@ -110,7 +103,7 @@ class MainPresenter @Inject constructor(
try {
disconnect()
removeAccountInterector.remove(currentServer)
removeAccountInteractor.remove(currentServer)
tokenRepository.remove(currentServer)
navigator.toNewServer()
} catch (ex: Exception) {
......@@ -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() {
manager.connect()
}
fun disconnect() {
manager.removeUserDataChannel(userDataChannel)
manager.disconnect()
}
......@@ -175,4 +156,43 @@ class MainPresenter @Inject constructor(
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
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
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 {
......
......@@ -21,7 +21,7 @@ import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.rotateBy
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.iid.InstanceID
import dagger.android.AndroidInjection
......@@ -95,16 +95,23 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
override fun setupNavHeader(viewModel: NavHeaderViewModel, accounts: List<Account>) {
Timber.d("Setting up nav header: $viewModel")
with(headerLayout) {
image_user_status.setImageDrawable(
DrawableHelper.getUserStatusDrawable(
viewModel.userStatus!!,
this.context
)
)
text_user_name.text = viewModel.userDisplayName
text_server_url.text = viewModel.serverUrl
image_avatar.setImageURI(viewModel.userAvatar)
server_logo.setImageURI(viewModel.serverLogo)
with(viewModel) {
if (userStatus != null) {
image_user_status.setImageDrawable(
DrawableHelper.getUserStatusDrawable(userStatus, context)
)
}
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
}
setupAccountsList(headerLayout, accounts)
}
}
......
......@@ -4,7 +4,7 @@ import chat.rocket.common.model.UserStatus
data class NavHeaderViewModel(
val userDisplayName: String,
val userDisplayName: String?,
val userStatus: UserStatus?,
val userAvatar: String?,
val serverUrl: String,
......
......@@ -23,10 +23,10 @@ class NavHeaderViewModelMapper @Inject constructor(
return NavHeaderViewModel(displayName, status, avatar, currentServer, logo)
}
private fun mapDisplayName(me: Myself): String {
private fun mapDisplayName(me: Myself): String? {
val username = me.username
val realName = me.name
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
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) {
......
package chat.rocket.android.member.ui
package chat.rocket.android.members.ui
import android.os.Bundle
import android.support.design.widget.BottomSheetDialogFragment
......
......@@ -19,6 +19,7 @@ import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.widget.DividerItemDecoration
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_members.*
......@@ -70,7 +71,7 @@ class MembersFragment : Fragment(), MembersView {
}
override fun showMembers(dataSet: List<MemberViewModel>, total: Long) {
activity?.apply {
ui {
setupToolbar(total)
if (adapter.itemCount == 0) {
adapter.prependData(dataSet)
......@@ -84,8 +85,8 @@ class MembersFragment : Fragment(), MembersView {
} else {
adapter.appendData(dataSet)
}
if (this is ChatRoomActivity) {
this.showRoomTypeIcon(false)
if (it is ChatRoomActivity) {
it.showRoomTypeIcon(false)
}
}
}
......@@ -98,29 +99,37 @@ class MembersFragment : Fragment(), MembersView {
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) {
showToast(resId)
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
showToast(message)
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
private fun setupRecyclerView() {
activity?.apply {
ui {
recycler_view.layoutManager = linearLayoutManager
recycler_view.addItemDecoration(DividerItemDecoration(this))
recycler_view.addItemDecoration(DividerItemDecoration(it))
recycler_view.adapter = adapter
}
}
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
private fun getUserAvatar(): String? {
val username = member.username ?: "?"
return baseUrl?.let {
baseUrl.avatarUrl(username,"png")
baseUrl.avatarUrl(username, format = "png")
}
}
......
......@@ -27,7 +27,7 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView,
view.showLoading()
try {
val myself = retryIO("me") { client.me() }
myselfId = myself.id
myselfId = myself.id!!
val avatarUrl = serverUrl.avatarUrl(myself.username!!)
view.showProfile(
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 {
*
* @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.
......@@ -19,7 +19,7 @@ interface MessagesRepository {
* @param rid The room id.
* @return A list of Message objects for the room with given room id or an empty list.
*/
fun getByRoomId(rid: String): List<Message>
suspend fun getByRoomId(rid: String): List<Message>
/**
* Get most recent messages up to count different users.
......@@ -29,43 +29,47 @@ interface MessagesRepository {
*
* @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!
*
* @return All messages or an empty list.
*/
fun getAll(): List<Message>
suspend fun getAll(): List<Message>
/**
* Save a single message object.
*
* @param The message object to saveAll.
*/
fun save(message: Message)
suspend fun save(message: Message)
/**
* Save a list of messages.
*/
fun saveAll(newMessages: List<Message>)
suspend fun saveAll(newMessages: List<Message>)
/**
* Removes all messages.
*/
fun clear()
suspend fun clear()
/**
* Remove message by id.
*
* @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.
*
* @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
import javax.inject.Inject
class RemoveAccountInterector @Inject constructor(val repository: AccountsRepository) {
class RemoveAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun remove(serverUrl: String) {
repository.remove(serverUrl)
}
......
......@@ -2,31 +2,40 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.common.model.BaseRoom
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.model.Message
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
class ConnectionManager(internal val client: RocketChatClient) {
private val statusChannelList = CopyOnWriteArrayList<Channel<State>>()
private val statusChannel = Channel<State>()
private val statusChannel = Channel<State>(Channel.CONFLATED)
private var connectJob: Job? = null
private val roomAndSubscriptionChannels = ArrayList<Channel<StreamMessage<BaseRoom>>>()
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val userDataChannels = ArrayList<Channel<Myself>>()
private val subscriptionIdMap = HashMap<String, String>()
private var subscriptionId: String? = null
private var roomsId: String? = null
private var userId: String? = null
fun connect() {
if (connectJob?.isActive == true
&& (state !is State.Disconnected)) {
if (connectJob?.isActive == true && (state !is State.Disconnected)) {
Timber.d("Already connected, just returning...")
return
}
......@@ -51,6 +60,10 @@ class ConnectionManager(internal val client: RocketChatClient) {
Timber.d("Subscribed to rooms: $id")
roomsId = id
}
client.subscribeUserDataChanges { _, id ->
Timber.d("Subscribed to the user: $id")
userId = id
}
resubscribeRooms()
}
......@@ -60,7 +73,8 @@ class ConnectionManager(internal val client: RocketChatClient) {
}
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) {
}
}
launch(parent = connectJob) {
for (myself in client.userDataChannel) {
Timber.d("Got userData")
for (channel in userDataChannels) {
channel.send(myself)
}
}
}
client.connect()
// Broadcast initial state...
......@@ -121,9 +144,15 @@ class ConnectionManager(internal val client: RocketChatClient) {
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>) {
val oldSub = roomMessagesChannels.put(roomId, channel)
......@@ -149,8 +178,8 @@ class ConnectionManager(internal val client: RocketChatClient) {
}
}
suspend fun ConnectionManager.chatRooms(timestamp: Long = 0, filterCustom: Boolean = true)
= client.chatRooms(timestamp, filterCustom)
suspend fun ConnectionManager.chatRooms(timestamp: Long = 0, filterCustom: Boolean = true) =
client.chatRooms(timestamp, filterCustom)
val ConnectionManager.state: 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
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.model.Message
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
class MemoryMessagesRepository : MessagesRepository {
private val messages: HashMap<String, Message> = HashMap()
override fun getById(id: String): Message? {
return messages[id]
override suspend fun getById(id: String): Message? = withContext(CommonPool) {
return@withContext messages[id]
}
override fun getByRoomId(rid: String): List<Message> {
return messages.filter { it.value.roomId == rid }.values.toList()
override suspend fun getByRoomId(rid: String): List<Message> = withContext(CommonPool) {
return@withContext messages.filter { it.value.roomId == rid }.values.toList()
}
override fun getRecentMessages(rid: String, count: Long): List<Message> {
return getByRoomId(rid).sortedByDescending { it.timestamp }
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 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
}
override fun saveAll(newMessages: List<Message>) {
override suspend fun saveAll(newMessages: List<Message>) = withContext(CommonPool) {
for (msg in newMessages) {
messages[msg.id] = msg
}
}
override fun clear() {
override suspend fun clear() = withContext(CommonPool) {
messages.clear()
}
override fun removeById(id: String) {
messages.remove(id)
override suspend fun removeById(id: String) {
withContext(CommonPool) {
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
roomMessages.forEach {
messages.remove(it.roomId)
......
package chat.rocket.android.server.infraestructure
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.model.Account
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,
val me = retryIO("me") { client.me() }
retryIO("updateProfile(${me.id})") {
client.updateProfile(me.id, null, null, password, null)
client.updateProfile(me.id!!, null, null, password, null)
}
view.showPasswordSuccessfullyUpdatedMessage()
......
......@@ -11,7 +11,10 @@ import chat.rocket.android.util.extensions.asObservable
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.textContent
import android.support.v7.view.ActionMode
import chat.rocket.android.util.extensions.ui
import dagger.android.support.AndroidSupportInjection
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.fragment_password.*
import javax.inject.Inject
......@@ -19,6 +22,7 @@ import javax.inject.Inject
class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.ActionMode.Callback {
@Inject lateinit var presenter: PasswordPresenter
private var actionMode: ActionMode? = null
private val disposables = CompositeDisposable()
companion object {
fun newInstance() = PasswordFragment()
......@@ -34,13 +38,20 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listenToChanges()
disposables.add(listenToChanges())
}
override fun onDestroyView() {
disposables.clear()
super.onDestroyView()
}
override fun hideLoading() {
layout_new_password.visibility = View.VISIBLE
layout_confirm_password.visibility = View.VISIBLE
view_loading.visibility = View.GONE
ui {
layout_new_password.visibility = View.VISIBLE
layout_confirm_password.visibility = View.VISIBLE
view_loading.visibility = View.GONE
}
}
override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
......@@ -69,9 +80,11 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action
}
override fun showLoading() {
layout_new_password.visibility = View.GONE
layout_confirm_password.visibility = View.GONE
view_loading.visibility = View.VISIBLE
ui {
layout_new_password.visibility = View.GONE
layout_confirm_password.visibility = View.GONE
view_loading.visibility = View.VISIBLE
}
}
override fun showPasswordFailsUpdateMessage(error: String?) {
......@@ -84,8 +97,9 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action
private fun finishActionMode() = actionMode?.finish()
private fun listenToChanges() {
Observables.combineLatest(text_new_password.asObservable(), text_confirm_password.asObservable()).subscribe {
private fun listenToChanges(): Disposable {
return Observables.combineLatest(text_new_password.asObservable(),
text_confirm_password.asObservable()).subscribe {
val textPassword = text_new_password.textContent
val textConfirmPassword = text_confirm_password.textContent
......@@ -97,7 +111,9 @@ class PasswordFragment: Fragment(), PasswordView, android.support.v7.view.Action
}
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() {
......
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 {
}
}
fun String.avatarUrl(avatar: String, format: String = "jpeg") =
"${removeTrailingSlash()}/avatar/${avatar.removeTrailingSlash()}?format=$format"
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"
}
}
fun String.serverLogoUrl(favicon: String) = "${removeTrailingSlash()}/$favicon"
......
......@@ -8,7 +8,7 @@ import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.net.toUri
import androidx.core.net.toUri
import chat.rocket.android.R
import chat.rocket.android.util.extensions.decodeUrl
import chat.rocket.android.util.extensions.toJsonObject
......
<vector android:height="24dp" android:viewportHeight="800.0"
android:viewportWidth="800.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/actionMenuColor" android:pathData="M511.8,390H520V84C567,67.8 596.3,42 596.3,16H203.7c0,26 29.3,51.8 76.3,68V390h8.2c-92.9,16 -157.5,61 -157.5,108H377v288h48V498h244.3C669.3,451 604.7,406 511.8,390z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
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>
\ No newline at end of file
......@@ -2,7 +2,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorPrimary" />
<solid android:color="@color/quoteBar" />
<size
android:width="4dp"
android:height="4dp" />
......
......@@ -32,4 +32,50 @@
app:layout_constraintTop_toTopOf="parent"
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>
\ 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 @@
android:paddingTop="@dimen/chat_item_top_and_bottom_padding"
android:paddingBottom="@dimen/chat_item_top_and_bottom_padding">
<include
android:id="@+id/layout_avatar"
layout="@layout/avatar"
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
app:roundedCornerRadius="3dp"
android:layout_marginTop="6dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/text_chat_name"
......@@ -25,7 +25,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/layout_avatar"
app:layout_constraintStart_toEndOf="@id/image_avatar"
android:textDirection="locale"
tools:text="General"/>
......@@ -51,7 +51,7 @@
app:layout_constraintTop_toBottomOf="@id/text_chat_name"
app:layout_constraintEnd_toStartOf="@id/layout_unread_messages_badge"
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
android:id="@+id/layout_unread_messages_badge"
......
......@@ -123,6 +123,8 @@
<!-- Pinned Messages -->
<string name="title_pinned_messages">पिन किए गए संदेश</string>
<string name="no_pinned_messages">कोई पिन संदेश नहीं</string>
<string name="no_pinned_description">सभी पिन किए गए संदेश यहां\nदिखाई देते हैं।</string>
<!-- Upload Messages -->
<string name="max_file_size_exceeded">फ़ाइल का आकार %1$d बाइट्स ने %2$d बाइट्स के अधिकतम अपलोड आकार को पार कर लिया है</string>
......
......@@ -123,6 +123,8 @@
<!-- Pinned Messages -->
<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 -->
<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 @@
<color name="colorEmojiIcon">#FF767676</color>
<color name="quoteBar">#A0A0A0</color>
<!-- Suggestions -->
<color name="suggestion_background_color">@android:color/white</color>
......
......@@ -124,6 +124,8 @@
<!-- Pinned Messages -->
<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 -->
<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 {
}
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.dokka:dokka-gradle-plugin:${versions.dokka}"
classpath 'com.google.gms:google-services:3.2.0'
......
......@@ -11,7 +11,7 @@ ext {
// Main dependencies
support : '27.1.0',
constraintLayout : '1.0.2',
androidKtx : '0.2',
androidKtx : '0.3',
dagger : '2.14.1',
exoPlayer : '2.6.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