Commit 6988897c authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Merge branch 'develop-2.x' into feature/show-and-update-user-profile

parents 9a5d9ad9 cf8c6513
apply plugin: 'com.android.application'
apply plugin: 'io.fabric'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
......@@ -11,8 +12,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 1001
versionName "2.0.0-dev1"
versionCode 1007
versionName "2.0.0-dev5.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......@@ -63,6 +64,7 @@ dependencies {
implementation libraries.daggerSupport
kapt libraries.daggerProcessor
kapt libraries.daggerAndroidApt
implementation libraries.playServicesGcm
implementation libraries.room
......@@ -81,6 +83,7 @@ dependencies {
implementation libraries.rxBinding
implementation libraries.fresco
api libraries.frescoOkHttp
implementation libraries.frescoAnimatedGif
implementation libraries.frescoWebP
implementation libraries.frescoAnimatedWebP
......@@ -98,6 +101,10 @@ dependencies {
implementation libraries.moshiLazyAdapters
implementation('com.crashlytics.sdk.android:crashlytics:2.6.8@aar') {
transitive = true
}
testImplementation libraries.junit
androidTestImplementation(libraries.expressoCore, {
exclude group: 'com.android.support', module: 'support-annotations'
......
......@@ -74,6 +74,11 @@
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<meta-data
android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
</application>
</manifest>
\ No newline at end of file
......@@ -4,8 +4,15 @@ import android.app.Activity
import android.app.Application
import android.app.Service
import chat.rocket.android.BuildConfig
import chat.rocket.android.app.utils.CustomImageFormatConfigurator
import chat.rocket.android.dagger.DaggerAppComponent
import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
......@@ -14,6 +21,7 @@ import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.HasServiceInjector
import io.fabric.sdk.android.Fabric
import timber.log.Timber
import javax.inject.Inject
......@@ -25,32 +33,60 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
@Inject
lateinit var serviceDispatchingAndroidInjector: DispatchingAndroidInjector<Service>
@Inject
lateinit var imagePipelineConfig: ImagePipelineConfig
@Inject
lateinit var draweeConfig: DraweeConfig
// TODO - remove this from here when we have a proper service handling the connection.
@Inject
lateinit var getCurrentServerInteractor: GetCurrentServerInteractor
@Inject
lateinit var multiServerRepository: MultiServerTokenRepository
@Inject
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var tokenRepository: TokenRepository
override fun onCreate() {
super.onCreate()
DaggerAppComponent.builder().application(this).build().inject(this)
// TODO - remove this when we have a proper service handling connection...
initCurrentServer()
AndroidThreeTen.init(this)
setupCrashlytics()
setupFresco()
setupTimber()
}
private fun setupFresco() {
val imagePipelineConfig = ImagePipelineConfig.newBuilder(this)
.setImageDecoderConfig(CustomImageFormatConfigurator.createImageDecoderConfig())
.build()
val draweeConfigBuilder = DraweeConfig.newBuilder()
// TODO - remove this when we have a proper service handling connection...
private fun initCurrentServer() {
val currentServer = getCurrentServerInteractor.get()
val serverToken = currentServer?.let { multiServerRepository.get(currentServer) }
val settings = currentServer?.let { settingsRepository.get(currentServer) }
if (currentServer != null && serverToken != null && settings != null) {
tokenRepository.save(Token(serverToken.userId, serverToken.authToken))
}
}
CustomImageFormatConfigurator.addCustomDrawableFactories(draweeConfigBuilder)
private fun setupCrashlytics() {
val core = CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()
Fabric.with(this, Crashlytics.Builder().core(core).build())
}
Fresco.initialize(this, imagePipelineConfig, draweeConfigBuilder.build())
private fun setupFresco() {
Fresco.initialize(this, imagePipelineConfig, draweeConfig)
}
private fun setupTimber() {
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
} else {
Timber.plant(CrashlyticsTree())
}
}
......
package chat.rocket.android.authentication.di
import android.content.Context
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.MultiServerTokenRepository
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
......@@ -17,10 +13,4 @@ class AuthenticationModule {
@Provides
@PerActivity
fun provideAuthenticationNavigator(activity: AuthenticationActivity, context: Context) = AuthenticationNavigator(activity, context)
@Provides
@PerActivity
fun provideMultiServerTokenRepository(repository: LocalRepository, moshi: Moshi): MultiServerTokenRepository {
return SharedPreferencesMultiServerTokenRepository(repository, moshi)
}
}
\ No newline at end of file
......@@ -3,6 +3,7 @@ package chat.rocket.android.authentication.infraestructure
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.LocalRepository.Companion.TOKEN_KEY
import chat.rocket.android.server.domain.MultiServerTokenRepository
import com.squareup.moshi.Moshi
......@@ -28,6 +29,7 @@ class SharedPreferencesMultiServerTokenRepository(private val repository: LocalR
repository.save("$TOKEN_KEY$server", adapter.toJson(token))
}
}
const val TOKEN_KEY = "token_"
\ No newline at end of file
override fun clear(server: String) {
repository.clear("$TOKEN_KEY$server")
}
}
\ No newline at end of file
......@@ -17,7 +17,7 @@ import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
......@@ -92,8 +92,8 @@ class LoginFragment : Fragment(), LoginView {
override fun showOauthView(value: Boolean) {
if (value) {
social_accounts_container.setVisibility(true)
button_fab.setVisibility(true)
social_accounts_container.setVisible(true)
button_fab.setVisible(true)
// We need to setup the layout to hide and show the oauth interface when the soft keyboard is shown
// (means that the user touched the text_username_or_email or text_password EditText to fill that respective fields).
......@@ -102,8 +102,8 @@ class LoginFragment : Fragment(), LoginView {
isGlobalLayoutListenerSetUp = true
}
} else {
social_accounts_container.setVisibility(false)
button_fab.setVisibility(false)
social_accounts_container.setVisible(false)
button_fab.setVisible(false)
}
}
......@@ -143,7 +143,7 @@ class LoginFragment : Fragment(), LoginView {
button_gitlab.isEnabled = true
}
override fun showSignUpView(value: Boolean) = text_new_to_rocket_chat.setVisibility(value)
override fun showSignUpView(value: Boolean) = text_new_to_rocket_chat.setVisible(value)
override fun alertWrongUsernameOrEmail() {
AnimationHelper.vibrateSmartPhone(appContext)
......@@ -159,11 +159,11 @@ class LoginFragment : Fragment(), LoginView {
override fun showLoading() {
enableUserInput(false)
view_loading.setVisibility(true)
view_loading.setVisible(true)
}
override fun hideLoading() {
view_loading.setVisibility(false)
view_loading.setVisible(false)
enableUserInput(true)
}
......@@ -189,7 +189,7 @@ class LoginFragment : Fragment(), LoginView {
}
private fun showLoginButton(value: Boolean) {
button_log_in.setVisibility(value)
button_log_in.setVisible(value)
}
private fun setupSignUpListener() {
......
......@@ -48,11 +48,11 @@ class ServerFragment : Fragment(), ServerView {
override fun showLoading() {
enableUserInput(false)
view_loading.setVisibility(true)
view_loading.setVisible(true)
}
override fun hideLoading() {
view_loading.setVisibility(false)
view_loading.setVisible(false)
enableUserInput(true)
}
......
......@@ -14,7 +14,7 @@ import chat.rocket.android.authentication.signup.presentation.SignupView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
......@@ -26,9 +26,9 @@ class SignupFragment : Fragment(), SignupView {
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(constraint_layout.rootView)) {
text_new_user_agreement.setVisibility(false)
text_new_user_agreement.setVisible(false)
} else {
text_new_user_agreement.setVisibility(true)
text_new_user_agreement.setVisible(true)
}
}
......@@ -92,11 +92,11 @@ class SignupFragment : Fragment(), SignupView {
override fun showLoading() {
enableUserInput(false)
view_loading.setVisibility(true)
view_loading.setVisible(true)
}
override fun hideLoading() {
view_loading.setVisibility(false)
view_loading.setVisible(false)
enableUserInput(true)
}
......
......@@ -14,7 +14,7 @@ import chat.rocket.android.R
import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter
import chat.rocket.android.authentication.twofactor.presentation.TwoFAView
import chat.rocket.android.helper.AnimationHelper
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_two_fa.*
......@@ -75,11 +75,11 @@ class TwoFAFragment : Fragment(), TwoFAView {
override fun showLoading() {
enableUserInput(false)
view_loading.setVisibility(true)
view_loading.setVisible(true)
}
override fun hideLoading() {
view_loading.setVisibility(false)
view_loading.setVisible(false)
enableUserInput(true)
}
......
......@@ -4,14 +4,17 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.launchUI
import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.connect
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.unsubscibre
import chat.rocket.core.internal.rest.messages
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.model.Message
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
......@@ -19,23 +22,29 @@ import javax.inject.Inject
class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory) {
factory: RocketChatClientFactory,
private val mapper: MessageViewModelMapper) {
private val client = factory.create(serverInteractor.get()!!)
private val roomMessages = ArrayList<Message>()
private var subId: String? = null
fun loadMessages(chatRoomId: String, chatRoomType: String, offset: Int = 0) {
private val stateChannel = Channel<State>()
fun loadMessages(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
launchUI(strategy) {
view.showLoading()
try {
val messages = client.messages(chatRoomId, BaseRoom.RoomType.valueOf(chatRoomType), offset.toLong(), 30).result
if (messages != null) {
synchronized(roomMessages) {
roomMessages.addAll(messages)
}
view.showMessages(messages, serverInteractor.get()!!)
} else {
view.showGenericErrorMessage()
val messages = client.messages(chatRoomId, roomTypeOf(chatRoomType), offset, 30).result
synchronized(roomMessages) {
roomMessages.addAll(messages)
}
val messagesViewModels = mapper.mapToViewModelList(messages, settings)
view.showMessages(messagesViewModels, serverInteractor.get()!!)
// Subscribe after getting the first page of messages from REST
if (offset == 0L) {
subscribeMessages(chatRoomId)
}
} catch (ex: Exception) {
ex.printStackTrace()
......@@ -71,16 +80,50 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
fun subscribeMessages(roomId: String) {
client.addStateChannel(stateChannel)
launch(CommonPool + strategy.jobs) {
for (status in stateChannel) {
Timber.d("Changing status to: $status")
when (status) {
State.Authenticating -> Timber.d("Authenticating")
State.Connected -> {
Timber.d("Connected")
subId = client.subscribeRoomMessages(roomId) {
Timber.d("subscribe messages for $roomId: $it")
}
}
}
}
Timber.d("Done on statusChannel")
}
when (client.state) {
State.Connected -> {
Timber.d("Already connected")
subId = client.subscribeRoomMessages(roomId) {
Timber.d("subscribe messages for $roomId: $it")
}
}
else -> client.connect()
}
launchUI(strategy) {
listenMessages(roomId)
}
// TODO - when we have a proper service, we won't need to take care of connection, just
// subscribe and listen...
/*launchUI(strategy) {
subId = client.subscribeRoomMessages(roomId) {
Timber.d("subscribe messages for $roomId: $it")
}
listenMessages(roomId)
}
}*/
}
fun unsubscribeMessages() {
launch(CommonPool) {
client.removeStateChannel(stateChannel)
subId?.let { subscriptionId ->
client.unsubscibre(subscriptionId)
}
......@@ -101,16 +144,17 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private fun updateMessage(streamedMessage: Message) {
launchUI(strategy) {
val viewModelStreamedMessage = mapper.mapToViewModel(streamedMessage, settings)
synchronized(roomMessages) {
val index = roomMessages.indexOfFirst { msg -> msg.id == streamedMessage.id }
if (index != -1) {
Timber.d("Updatind message at $index")
roomMessages[index] = streamedMessage
view.dispatchUpdateMessage(index, streamedMessage)
view.dispatchUpdateMessage(index, viewModelStreamedMessage)
} else {
Timber.d("Adding new message")
roomMessages.add(0, streamedMessage)
view.showNewMessage(streamedMessage)
view.showNewMessage(viewModelStreamedMessage)
}
}
}
......
package chat.rocket.android.chatroom.ui
import DateTimeHelper
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.chatroom.viewmodel.AttachmentType
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.textContent
import chat.rocket.android.util.setVisible
import chat.rocket.common.util.ifNull
import chat.rocket.core.model.Message
import com.facebook.drawee.view.SimpleDraweeView
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
import kotlinx.android.synthetic.main.message_attachment.view.*
class ChatRoomAdapter(private val context: Context,
private val serverUrl: String) : RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
class ChatRoomAdapter(private val serverUrl: String) : RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {
init {
setHasStableIds(true)
}
val dataSet = ArrayList<Message>()
val dataSet = ArrayList<MessageViewModel>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = ViewHolder(parent.inflate(R.layout.item_message))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(parent.inflate(R.layout.item_message), serverUrl)
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(dataSet[position])
......@@ -39,18 +39,18 @@ class ChatRoomAdapter(private val context: Context,
override fun getItemViewType(position: Int): Int = position
fun addDataSet(dataSet: List<Message>) {
fun addDataSet(dataSet: List<MessageViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet.addAll(previousDataSetSize, dataSet)
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
fun addItem(message: Message) {
fun addItem(message: MessageViewModel) {
dataSet.add(0, message)
notifyItemInserted(0)
}
fun updateItem(index: Int, message: Message) {
fun updateItem(index: Int, message: MessageViewModel) {
dataSet[index] = message
notifyItemChanged(index)
}
......@@ -59,33 +59,68 @@ class ChatRoomAdapter(private val context: Context,
return dataSet[position].id.hashCode().toLong()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
class ViewHolder(itemView: View, val serverUrl: String) : RecyclerView.ViewHolder(itemView) {
fun bind(message: Message) = with(itemView) {
fun bind(message: MessageViewModel) = with(itemView) {
bindUserAvatar(message, image_avatar, image_unknown_avatar)
bindUserName(message, text_user_name)
bindTime(message, text_message_time)
bindContent(message, text_content)
}
private fun bindUserAvatar(message: Message, drawee: SimpleDraweeView, imageUnknownAvatar: ImageView) = message.sender?.username.let {
drawee.setImageURI(UrlHelper.getAvatarUrl(serverUrl, it.toString()))
}.ifNull {
imageUnknownAvatar.setVisibility(true)
}
text_user_name.text = message.sender
text_message_time.text = message.time
text_content.text = message.content
private fun bindUserName(message: Message, textView: TextView) = message.sender?.username.let {
textView.textContent = it.toString()
}.ifNull {
textView.textContent = context.getString(R.string.msg_unknown)
bindAttachment(message, message_attachment, image_attachment, audio_video_attachment,
file_name)
}
private fun bindTime(message: Message, textView: TextView) {
textView.textContent = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(message.timestamp))
private fun bindAttachment(message: MessageViewModel,
attachment_container: View,
image_attachment: SimpleDraweeView,
audio_video_attachment: View,
file_name: TextView) {
with(message) {
if (attachmentUrl == null || attachmentType == null) {
attachment_container.setVisible(false)
return
}
var imageVisible = false
var videoVisible = false
attachment_container.setVisible(true)
when (message.attachmentType) {
is AttachmentType.Image -> {
imageVisible = true
image_attachment.setImageURI(message.attachmentUrl)
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
ImageViewer.Builder(view.context, listOf(message.attachmentUrl))
.setStartPosition(0)
.show()
}
}
is AttachmentType.Video,
is AttachmentType.Audio -> {
videoVisible = true
audio_video_attachment.setOnClickListener { view ->
message.attachmentUrl?.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
image_attachment.setVisible(imageVisible)
audio_video_attachment.setVisible(videoVisible)
file_name.text = message.attachmentTitle
}
}
private fun bindContent(message: Message, textView: TextView) {
textView.textContent = message.message
private fun bindUserAvatar(message: MessageViewModel, drawee: SimpleDraweeView, imageUnknownAvatar: ImageView) = message.getAvatarUrl(serverUrl).let {
drawee.setImageURI(it.toString())
drawee.setVisible(true)
imageUnknownAvatar.setVisible(false)
}.ifNull {
drawee.setVisible(false)
imageUnknownAvatar.setVisible(true)
}
}
}
\ No newline at end of file
......@@ -14,7 +14,7 @@ import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.presentation.ChatRoomView
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import chat.rocket.core.model.Message
import dagger.android.support.AndroidSupportInjection
......@@ -67,7 +67,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.loadMessages(chatRoomId, chatRoomType)
presenter.subscribeMessages(chatRoomId)
setupComposer()
}
......@@ -87,7 +86,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
if (dataSet.size >= 30) {
recycler_view.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView?) {
presenter.loadMessages(chatRoomId, chatRoomType, page * 30)
presenter.loadMessages(chatRoomId, chatRoomType, page * 30L)
}
})
}
......@@ -124,9 +123,9 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
adapter.updateItem(index, message)
}
override fun showLoading() = view_loading.setVisibility(true)
override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisibility(false)
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
......@@ -134,8 +133,8 @@ class ChatRoomFragment : Fragment(), ChatRoomView {
private fun setupComposer() {
if (isChatRoomReadOnly) {
text_room_is_read_only.setVisibility(true)
top_container.setVisibility(false)
text_room_is_read_only.setVisible(true)
top_container.setVisible(false)
} else {
text_send.setOnClickListener { sendMessage(text_message.textContent) }
}
......
package chat.rocket.android.chatroom.viewmodel
import DateTimeHelper
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.SITE_URL
import chat.rocket.android.server.domain.USE_REALNAME
import chat.rocket.common.model.Token
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType.*
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.AudioAttachment
import chat.rocket.core.model.attachment.FileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import okhttp3.HttpUrl
data class MessageViewModel(val context: Context,
private val token: Token?,
private val message: Message,
private val settings: Map<String, Value<Any>>?) {
val id: String = message.id
val time: CharSequence
val sender: CharSequence
val content: CharSequence
var attachmentUrl: String? = null
var attachmentTitle: CharSequence? = null
var attachmentType: AttachmentType? = null
init {
sender = getSenderName()
content = getContent(context)
time = getTime()
message.attachments?.let {
if (it.isEmpty() || it[0] == null) return@let
val attachment = it[0] as FileAttachment
val baseUrl = settings?.get(SITE_URL)
baseUrl?.let {
attachmentUrl = attachmentUrl("${baseUrl.value}${attachment.url}")
attachmentTitle = attachment.title
attachmentType = when (attachment) {
is ImageAttachment -> AttachmentType.Image
is VideoAttachment -> AttachmentType.Video
is AudioAttachment -> AttachmentType.Audio
else -> null
}
}
}
}
fun getAvatarUrl(serverUrl: String): String? {
return message.sender?.username.let {
return@let UrlHelper.getAvatarUrl(serverUrl, it.toString())
}
}
fun getTime() = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(message.timestamp))
fun getSenderName(): CharSequence {
val useRealName = settings?.get(USE_REALNAME)?.value as Boolean
val username = message.sender?.username
val realName = message.sender?.name
val senderName = if (useRealName) realName else username
return senderName ?: username.toString()
}
fun getContent(context: Context): CharSequence {
val contentMessage: CharSequence
when (message.type) {
//TODO: Add implementation for Welcome type.
is MessageRemoved -> contentMessage = getSystemMessage(context.getString(R.string.message_removed))
is UserJoined -> contentMessage = getSystemMessage(context.getString(R.string.message_user_joined_channel))
is UserLeft -> contentMessage = getSystemMessage(context.getString(R.string.message_user_left))
is UserAdded -> contentMessage = getSystemMessage(
context.getString(R.string.message_user_added_by, message.message, message.sender?.username))
is RoomNameChanged -> contentMessage = getSystemMessage(
context.getString(R.string.message_room_name_changed, message.message, message.sender?.username))
is UserRemoved -> contentMessage = getSystemMessage(
context.getString(R.string.message_user_removed_by, message.message, message.sender?.username))
else -> contentMessage = getNormalMessage()
}
return contentMessage
}
private fun getNormalMessage() = message.message
private fun getSystemMessage(content: String): CharSequence {
val spannableMsg = SpannableString(content)
spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length,
0)
spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length,
0)
val username = message.sender?.username
val message = message.message
val usernameTextStartIndex = if (username != null) content.indexOf(username) else -1
val usernameTextEndIndex = if (username != null) usernameTextStartIndex + username.length else -1
val messageTextStartIndex = if (message.isNotEmpty()) content.indexOf(message) else -1
val messageTextEndIndex = messageTextStartIndex + message.length
if (usernameTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), usernameTextStartIndex, usernameTextEndIndex,
0)
}
if (messageTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), messageTextStartIndex, messageTextEndIndex,
0)
}
return spannableMsg
}
private fun attachmentUrl(url: String): String {
var response = url
val httpUrl = HttpUrl.parse(url)
httpUrl?.let {
response = it.newBuilder().apply {
addQueryParameter("rc_uid", token?.userId)
addQueryParameter("rc_token", token?.authToken)
}.build().toString()
}
return response
}
}
sealed class AttachmentType {
object Image : AttachmentType()
object Video : AttachmentType()
object Audio : AttachmentType()
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import android.content.Context
import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message
import chat.rocket.core.model.Value
import javax.inject.Inject
class MessageViewModelMapper @Inject constructor(private val context: Context, private val tokenRepository: TokenRepository) {
suspend fun mapToViewModel(message: Message, settings: Map<String, Value<Any>>?) = MessageViewModel(context, tokenRepository.get(), message, settings)
suspend fun mapToViewModelList(messageList: List<Message>, settings: Map<String, Value<Any>>?): List<MessageViewModel> {
return messageList.map { MessageViewModel(context, tokenRepository.get(), it, settings) }
}
}
\ No newline at end of file
package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SaveChatRoomsInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.*
import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.channels.Channel
import timber.log.Timber
import javax.inject.Inject
class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val strategy: CancelStrategy,
private val navigator: ChatRoomsNavigator,
serverInteractor: GetCurrentServerInteractor,
private val serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val localRepository: LocalRepository,
factory: RocketChatClientFactory) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
private val currentServer = serverInteractor.get()!!
private var reloadJob: Deferred<List<ChatRoom>?>? = null
private var reloadJob: Deferred<List<ChatRoom>>? = null
private val stateChannel = Channel<State>()
fun loadChatRooms() {
launchUI(strategy) {
view.showLoading()
try {
val chatRooms = getChatRooms()
if (chatRooms != null) {
view.updateChatRooms(chatRooms)
subscribeRoomUpdates()
} else {
view.showNoChatRoomsToDisplay()
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
view.updateChatRooms(loadRooms())
subscribeRoomUpdates()
view.hideLoading()
}
}
fun loadChatRoom(chatRoom: ChatRoom) {
navigator.toChatRoom(chatRoom.id,
chatRoom.name,
chatRoom.type.name,
chatRoom.readonly ?: false)
}
fun loadChatRoom(chatRoom: ChatRoom) = navigator.toChatRoom(chatRoom.id, chatRoom.name,
chatRoom.type.toString(), chatRoom.readonly ?: false)
/**
* Gets a [ChatRoom] list filtered by name from local repository.
*
* @param name The Chat Room name to get.
* Gets a [ChatRoom] list from local repository.
* ChatRooms returned are filtered by name.
*/
fun chatRoomsByName(name: String) {
val currentServer = serverInteractor.get()!!
launchUI(strategy) {
val roomList = getChatRoomsInteractor.getByName(currentServer, name)
view.updateChatRooms(roomList)
}
}
private suspend fun getChatRooms(): List<ChatRoom>? {
private suspend fun loadRooms(): List<ChatRoom> {
val chatRooms = client.chatRooms().update
if (chatRooms != null) {
val sortedOpenChatRooms = sortOpenChatRooms(chatRooms)
saveChatRoomsInteractor.save(currentServer, sortedOpenChatRooms)
return sortedOpenChatRooms
}
return null
val sortedRooms = sortRooms(chatRooms)
saveChatRoomsInteractor.save(currentServer, sortedRooms)
return sortedRooms
}
private fun sortOpenChatRooms(chatRooms: List<ChatRoom>): List<ChatRoom> {
private fun sortRooms(chatRooms: List<ChatRoom>): List<ChatRoom> {
val openChatRooms = getOpenChatRooms(chatRooms)
return sortChatRooms(openChatRooms)
}
private fun updateRooms() {
launch {
view.updateChatRooms(getChatRoomsInteractor.get(currentServer))
}
}
private fun getOpenChatRooms(chatRooms: List<ChatRoom>): List<ChatRoom> {
return chatRooms.filter(ChatRoom::open)
}
......@@ -94,16 +86,12 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
chatRoom.lastMessage?.timestamp
}
}
private fun updateChatRooms() {
launch {
view.updateChatRooms(getChatRoomsInteractor.get(currentServer))
}
}
// TODO - Temporary stuff, remove when adding DB support
private suspend fun subscribeRoomUpdates() {
client.addStateChannel(stateChannel)
launch(CommonPool + strategy.jobs) {
for (status in client.statusChannel) {
for (status in stateChannel) {
Timber.d("Changing status to: $status")
when (status) {
State.Authenticating -> Timber.d("Authenticating")
......@@ -159,7 +147,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
}
}
updateChatRooms()
updateRooms()
}
}
......@@ -179,7 +167,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
}
}
updateChatRooms()
updateRooms()
}
}
......@@ -190,7 +178,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
reloadJob = async(CommonPool + strategy.jobs) {
delay(1000)
Timber.d("reloading rooms after wait")
getChatRooms()
loadRooms()
}
reloadJob?.await()
}
......@@ -221,7 +209,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
client)
removeRoom(room.id, chatRooms)
chatRooms.add(newRoom)
saveChatRoomsInteractor.save(currentServer, sortOpenChatRooms(chatRooms))
saveChatRoomsInteractor.save(currentServer, sortRooms(chatRooms))
}
}
......@@ -251,7 +239,7 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
client)
removeRoom(subscription.roomId, chatRooms)
chatRooms.add(newRoom)
saveChatRoomsInteractor.save(currentServer, sortOpenChatRooms(chatRooms))
saveChatRoomsInteractor.save(currentServer, sortRooms(chatRooms))
}
}
......@@ -261,8 +249,39 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
synchronized(this) {
chatRooms.removeAll { chatRoom -> chatRoom.id == id }
}
saveChatRoomsInteractor.save(currentServer, sortOpenChatRooms(chatRooms))
saveChatRoomsInteractor.save(currentServer, sortRooms(chatRooms))
}
fun disconnect() {
client.removeStateChannel(stateChannel)
client.disconnect()
}
/**
* Logout from current server.
*/
fun logout() {
launchUI(strategy) {
try {
clearTokens()
client.logout()
//TODO: Add the code to unsubscribe to all subscriptions.
client.disconnect()
view.onLogout()
} catch (e: RocketChatException) {
Timber.e(e)
view.showMessage(e.message!!)
}
}
}
fun disconnect() = client.disconnect()
private suspend fun clearTokens() {
serverInteractor.clear()
val pushToken = localRepository.get(LocalRepository.KEY_PUSH_TOKEN)
if (pushToken != null) {
client.unregisterPushToken(pushToken)
localRepository.clear(LocalRepository.KEY_PUSH_TOKEN)
}
localRepository.clearAllFromServer(currentServer)
}
}
\ No newline at end of file
......@@ -17,4 +17,9 @@ interface ChatRoomsView : LoadingView, MessageView {
* Shows no chat rooms to display.
*/
fun showNoChatRoomsToDisplay()
/**
* User has successfully logged out from the current server.
**/
fun onLogout()
}
\ No newline at end of file
......@@ -11,9 +11,9 @@ import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.setVisible
import chat.rocket.android.util.textContent
import chat.rocket.common.model.BaseRoom.RoomType
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.*
......@@ -50,14 +50,14 @@ class ChatRoomsAdapter(private val context: Context,
private fun bindAvatar(chatRoom: ChatRoom, avatarLayout: View, drawee: SimpleDraweeView, imageView: ImageView) {
val chatRoomName = chatRoom.name
if (chatRoom.type == RoomType.ONE_TO_ONE) {
if (chatRoom.type is RoomType.DirectMessage) {
drawee.setImageURI(UrlHelper.getAvatarUrl(chatRoom.client.url, chatRoomName))
imageView.setVisibility(false)
avatarLayout.setVisibility(true)
imageView.setVisible(false)
avatarLayout.setVisible(true)
} else {
imageView.setImageDrawable(DrawableHelper.getTextDrawable(chatRoomName))
avatarLayout.setVisibility(false)
imageView.setVisibility(true)
avatarLayout.setVisible(false)
imageView.setVisible(true)
}
}
......@@ -99,11 +99,11 @@ class ChatRoomsAdapter(private val context: Context,
when {
totalUnreadMessage in 1..99 -> {
textView.textContent = totalUnreadMessage.toString()
textView.setVisibility(true)
textView.setVisible(true)
}
totalUnreadMessage > 99 -> {
textView.textContent = context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
textView.setVisibility(true)
textView.setVisible(true)
}
}
}
......
package chat.rocket.android.chatrooms.ui
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
......@@ -10,14 +11,15 @@ import android.support.v7.widget.SearchView
import android.view.*
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.util.inflate
import chat.rocket.android.util.setVisibility
import chat.rocket.android.util.setVisible
import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
......@@ -71,6 +73,13 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
})
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.action_logout -> presenter.logout()
}
return true
}
override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) {
activity.apply {
launch(UI) {
......@@ -89,12 +98,23 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override fun showLoading() = view_loading.setVisibility(true)
override fun hideLoading() = view_loading.setVisibility(false)
override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(message: String) = Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun onLogout() {
activity?.apply {
finish()
val intent = Intent(this, AuthenticationActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
}
private fun setupToolbar() {
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.title_chats)
}
......
......@@ -6,11 +6,16 @@ import android.content.Context
import android.content.SharedPreferences
import chat.rocket.android.BuildConfig
import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.app.utils.CustomImageFormatConfigurator
import chat.rocket.android.authentication.infraestructure.MemoryTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.helper.FrescoAuthInterceptor
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.server.domain.ChatRoomsRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository
import chat.rocket.android.server.infraestructure.ServerDao
......@@ -21,12 +26,19 @@ import chat.rocket.android.util.TimberLogger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient
import chat.rocket.core.TokenRepository
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig
import com.facebook.imagepipeline.listener.RequestListener
import com.facebook.imagepipeline.listener.RequestLoggingListener
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import javax.inject.Singleton
@Module
......@@ -71,11 +83,14 @@ class AppModule {
@Provides
@Singleton
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
val interceptor = HttpLoggingInterceptor()
val interceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger { message ->
Timber.d(message)
})
if (BuildConfig.DEBUG) {
interceptor.level = HttpLoggingInterceptor.Level.BODY
} else {
interceptor.level = HttpLoggingInterceptor.Level.HEADERS
// TODO - change to HEADERS on production...
interceptor.level = HttpLoggingInterceptor.Level.BODY
}
return interceptor
......@@ -89,6 +104,46 @@ class AppModule {
}.build()
}
@Provides
@ForFresco
@Singleton
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository): Interceptor {
return FrescoAuthInterceptor(tokenRepository)
}
@Provides
@ForFresco
@Singleton
fun provideFrescoOkHttpClient(okHttpClient: OkHttpClient, @ForFresco authInterceptor: Interceptor): OkHttpClient {
return okHttpClient.newBuilder().apply {
//addInterceptor(authInterceptor)
}.build()
}
@Provides
@Singleton
fun provideImagePipelineConfig(context: Context, @ForFresco okHttpClient: OkHttpClient): ImagePipelineConfig {
val listeners = HashSet<RequestListener>()
listeners.add(RequestLoggingListener())
return OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient)
.setImageDecoderConfig(CustomImageFormatConfigurator.createImageDecoderConfig())
.setRequestListeners(listeners)
.setDownsampleEnabled(true)
//.experiment().setBitmapPrepareToDraw(true).experiment()
.experiment().setPartialImageCachingEnabled(true).build()
}
@Provides
@Singleton
fun provideDraweeConfig(): DraweeConfig {
val draweeConfigBuilder = DraweeConfig.newBuilder()
CustomImageFormatConfigurator.addCustomDrawableFactories(draweeConfigBuilder)
return draweeConfigBuilder.build()
}
@Provides
@Singleton
fun provideTokenRepository(): TokenRepository {
......@@ -135,4 +190,10 @@ class AppModule {
fun provideMoshi(): Moshi {
return Moshi.Builder().add(AppJsonAdapterFactory.INSTANCE).build()
}
@Provides
@Singleton
fun provideMultiServerTokenRepository(repository: LocalRepository, moshi: Moshi): MultiServerTokenRepository {
return SharedPreferencesMultiServerTokenRepository(repository, moshi)
}
}
\ No newline at end of file
package chat.rocket.android.dagger.qualifier
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ForFresco
\ No newline at end of file
package chat.rocket.android.helper
import com.crashlytics.android.Crashlytics
import timber.log.Timber
class CrashlyticsTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String?, throwable: Throwable?) {
Crashlytics.log(priority, tag, message)
if (throwable != null) {
Crashlytics.logException(throwable)
}
}
}
\ No newline at end of file
package chat.rocket.android.helper
import chat.rocket.core.TokenRepository
import okhttp3.Interceptor
import okhttp3.Response
class FrescoAuthInterceptor(private val tokenRepository: TokenRepository) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenRepository.get()
var request = chain.request()
token?.let {
val url = request.url().newBuilder().apply {
addQueryParameter("rc_uid", token.userId)
addQueryParameter("rc_token", token.authToken)
}.build()
request = request.newBuilder().apply {
url(url)
}.build()
}
return chain.proceed(request)
}
}
\ No newline at end of file
......@@ -3,10 +3,16 @@ package chat.rocket.android.infrastructure
interface LocalRepository {
companion object {
val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
const val TOKEN_KEY = "token_"
const val SETTINGS_KEY = "settings_"
}
fun save(key: String, value: String?)
fun get(key: String): String?
fun clear(key: String)
fun clearAllFromServer(server: String)
}
\ No newline at end of file
......@@ -11,4 +11,14 @@ class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : L
override fun get(key: String): String? {
return preferences.getString(key, null)
}
override fun clear(key: String) {
preferences.edit().remove(key).apply()
}
override fun clearAllFromServer(server: String) {
clear(LocalRepository.KEY_PUSH_TOKEN)
clear(LocalRepository.TOKEN_KEY + server)
clear(LocalRepository.SETTINGS_KEY + server)
}
}
\ No newline at end of file
......@@ -3,4 +3,5 @@ package chat.rocket.android.server.domain
interface CurrentServerRepository {
fun save(url: String)
fun get(): String?
fun clear()
}
\ No newline at end of file
......@@ -4,4 +4,8 @@ import javax.inject.Inject
class GetCurrentServerInteractor @Inject constructor(private val repository: CurrentServerRepository) {
fun get(): String? = repository.get()
fun clear() {
repository.clear()
}
}
\ No newline at end of file
......@@ -6,4 +6,6 @@ interface MultiServerTokenRepository {
fun get(server: String): TokenModel?
fun save(server: String, token: TokenModel)
fun clear(server: String)
}
\ No newline at end of file
package chat.rocket.android.server.infraestructure
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.LocalRepository.Companion.SETTINGS_KEY
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.core.internal.SettingsAdapter
import chat.rocket.core.model.Value
......@@ -21,8 +22,4 @@ class SharedPreferencesSettingsRepository(private val localRespository: LocalRep
return null
}
companion object {
private const val SETTINGS_KEY = "settings_"
}
}
\ No newline at end of file
......@@ -16,4 +16,8 @@ class SharedPrefsCurrentServerRepository(private val preferences: SharedPreferen
companion object {
private const val CURRENT_SERVER_KEY = "current_server"
}
override fun clear() {
preferences.edit().remove(CURRENT_SERVER_KEY).apply()
}
}
\ No newline at end of file
......@@ -18,8 +18,8 @@ fun String.ifEmpty(value: String): String {
return this
}
fun View.setVisibility(value: Boolean) {
visibility = if (value) {
fun View.setVisible(visible: Boolean) {
visibility = if (visible) {
View.VISIBLE
} else {
View.GONE
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="80dp"
android:height="45dp"
android:viewportWidth="80.0"
android:viewportHeight="45.0">
<path
android:pathData="M0,0h80v45h-80z"
android:strokeColor="#00000000"
android:fillColor="#C3D1DA"
android:strokeWidth="1"/>
<path
android:pathData="M43.99,16.75C44.34,16.75 44.64,16.87 44.88,17.12C45.13,17.36 45.25,17.66 45.25,18.01L45.25,26.74C45.25,27.09 45.13,27.39 44.88,27.63C44.64,27.88 44.34,28 43.99,28L35.26,28C34.91,28 34.61,27.88 34.37,27.63C34.12,27.39 34,27.09 34,26.74L34,18.01C34,17.66 34.12,17.36 34.37,17.12C34.61,16.87 34.91,16.75 35.26,16.75L43.99,16.75ZM43.99,26.74L43.99,18.01L35.26,18.01L35.26,26.74L43.99,26.74ZM40.86,22.55L43.05,25.51L36.2,25.51L37.9,23.28L39.13,24.78L40.86,22.55Z"
android:strokeColor="#00000000"
android:fillColor="#5D8298"
android:strokeWidth="1"/>
</vector>
......@@ -4,10 +4,10 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginBottom="6dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="10dp">
android:layout_marginTop="6dp">
<include
android:id="@+id/layout_avatar"
......@@ -55,13 +55,13 @@
app:layout_constraintTop_toBottomOf="@+id/top_container"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<!-- TODO implement -->
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_attachment"
<!-- TODO - Use separate adapter items for messages and attachments. -->
<include
android:id="@+id/message_attachment"
layout="@layout/message_attachment"
android:layout_width="0dp"
android:layout_height="150dp"
android:layout_marginTop="5dp"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintLeft_toLeftOf="@id/top_container"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_content" />
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:id="@+id/attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_attachment"
android:layout_width="match_parent"
android:layout_height="150dp"
fresco:actualImageScaleType="fitStart"
fresco:placeholderImage="@drawable/image_dummy"
tools:visibility="visible"
android:visibility="gone"/>
<FrameLayout
android:id="@+id/audio_video_attachment"
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="@color/black"
android:visibility="gone"
tools:visibility="gone">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/exo_controls_play"/>
</FrameLayout>
<TextView
android:id="@+id/file_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Filename.png"/>
</LinearLayout>
\ No newline at end of file
......@@ -7,6 +7,11 @@
android:icon="@drawable/ic_search_white_24px"
android:title="@string/action_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always|collapseActionView" />
app:showAsAction="ifRoom|collapseActionView" />
<item android:id="@+id/action_logout"
android:title="@string/action_logout"
app:showAsAction="never" />
</menu>
\ No newline at end of file
......@@ -16,6 +16,7 @@
<string name="action_privacy_policy">Política de Privacidade</string>
<string name="action_search">Pesquisar</string>
<string name="action_update">Atualizar</string>
<string name="action_logout">Sair</string>
<!-- Regular information messages -->
<string name="msg_no_internet_connection">Sem conexão à internet</string>
......
......@@ -17,6 +17,7 @@
<string name="action_privacy_policy">Privacy Policy</string>
<string name="action_search">Search</string>
<string name="action_update">Update</string>
<string name="action_logout">Log Out</string>
<!-- Regular information messages -->
<string name="msg_no_internet_connection">No internet connection</string>
......
......@@ -5,6 +5,7 @@ buildscript {
repositories {
google()
jcenter()
maven { url 'https://maven.fabric.io/public' }
mavenCentral()
}
......@@ -13,6 +14,7 @@ buildscript {
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.1.2'
classpath 'io.fabric.tools:gradle:1.+'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
......
......@@ -5,7 +5,7 @@ ext {
targetSdk : 27,
buildTools : '27.0.3',
kotlin : '1.2.21',
coroutine : '0.21',
coroutine : '0.22',
dokka : '0.9.15',
// Main dependencies
......@@ -73,6 +73,7 @@ ext {
rxBinding : "com.jakewharton.rxbinding2:rxbinding-kotlin:${versions.rxBinding}",
fresco : "com.facebook.fresco:fresco:${versions.fresco}",
frescoOkHttp : "com.facebook.fresco:imagepipeline-okhttp3:${versions.fresco}",
frescoAnimatedGif : "com.facebook.fresco:animated-gif:${versions.fresco}",
frescoWebP : "com.facebook.fresco:webpsupport:${versions.fresco}",
frescoAnimatedWebP : "com.facebook.fresco:animated-webp:${versions.fresco}",
......
package chat.rocket.android.player
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.View
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.DefaultRenderersFactory
......@@ -22,10 +25,12 @@ class PlayerActivity : AppCompatActivity() {
private var playWhenReady = true
private var currentWindow = 0
private var playbackPosition = 0L
private lateinit var videoUrl: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
videoUrl = intent.getStringExtra(URL_KEY)
}
override fun onStart() {
......@@ -65,8 +70,9 @@ class PlayerActivity : AppCompatActivity() {
player.seekTo(currentWindow, playbackPosition)
isPlayerInitialized = true
}
val uri = Uri.parse("http://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4")
val uri = Uri.parse(videoUrl)
val mediaSource = buildMediaSource(uri)
Log.d("PlayerActivity", "Player with: " + videoUrl)
player.prepare(mediaSource, true, false)
}
......@@ -86,4 +92,13 @@ class PlayerActivity : AppCompatActivity() {
// Read the docs for detailed explanation: https://developer.android.com/training/basics/firstapp/starting-activity.html and https://developer.android.com/design/patterns/fullscreen.html
player_view.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
}
companion object {
const private val URL_KEY = "URL_KEY"
fun play(context: Context, url: String) {
context.startActivity(Intent(context, PlayerActivity::class.java).apply {
putExtra(URL_KEY, url)
})
}
}
}
\ No newline at end of file
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