Commit aeb17466 authored by Lucio Maciel's avatar Lucio Maciel

Merge branch 'develop' into feature/db-chatrooms

parents aee2ac64 b9b18b2c
......@@ -12,8 +12,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 2020
versionName "2.2.0"
versionCode 2022
versionName "2.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......@@ -73,6 +73,7 @@ dependencies {
kapt libraries.daggerAndroidApt
implementation libraries.playServicesGcm
implementation libraries.playServicesAuth
implementation libraries.room
kapt libraries.roomProcessor
......
......@@ -12,7 +12,18 @@ object DateTimeHelper {
private val lastWeek = today.minusWeeks(1)
/**
* Returns a date from a [LocalDateTime] or the textual representation if the [LocalDateTime] has a max period of a week from the current date.
* Returns a [LocalDateTime] from a [Long].
*
* @param long The [Long]
* @return The [LocalDateTime] from a [Long].
*/
fun getLocalDateTime(long: Long): LocalDateTime {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(long), ZoneId.systemDefault())
}
/**
* Returns a date from a [LocalDateTime] or the textual representation if the [LocalDateTime]
* has a max period of a week from the current date.
*
* @param localDateTime The [LocalDateTime].
* @param context The context.
......@@ -45,13 +56,18 @@ object DateTimeHelper {
}
/**
* Returns a [LocalDateTime] from a [Long].
* Returns a date time from a [LocalDateTime].
*
* @param long The [Long]
* @return The [LocalDateTime] from a [Long].
* @param localDateTime The [LocalDateTime].
* @return The time from a [LocalDateTime].
*/
fun getLocalDateTime(long: Long): LocalDateTime {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(long), ZoneId.systemDefault())
fun getDateTime(localDateTime: LocalDateTime): String {
return formatLocalDateTime(localDateTime)
}
private fun formatLocalDateTime(localDateTime: LocalDateTime): String {
val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
return localDateTime.format(formatter).toString()
}
private fun formatLocalDate(localDate: LocalDate): String {
......
......@@ -17,6 +17,7 @@ import chat.rocket.common.model.Token
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.*
import com.google.android.gms.auth.api.credentials.Credential
import kotlinx.coroutines.experimental.delay
import timber.log.Timber
import java.util.concurrent.TimeUnit
......@@ -54,6 +55,7 @@ class LoginPresenter @Inject constructor(
private lateinit var credentialSecret: String
private lateinit var deepLinkUserId: String
private lateinit var deepLinkToken: String
private var loginCredentials: Credential? = null
fun setupView() {
setupConnectionInfo(currentServer)
......@@ -149,13 +151,20 @@ class LoginPresenter @Inject constructor(
client.settingsOauth().services
}
if (services.isNotEmpty()) {
val state = "{\"loginStyle\":\"popup\",\"credentialToken\":\"${generateRandomString(40)}\",\"isCordova\":true}".encodeToBase64()
val state =
"{\"loginStyle\":\"popup\",\"credentialToken\":\"${generateRandomString(40)}\",\"isCordova\":true}".encodeToBase64()
var totalSocialAccountsEnabled = 0
if (settings.isFacebookAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_FACEBOOK)
if (clientId != null) {
view.setupFacebookButtonListener(OauthHelper.getFacebookOauthUrl(clientId, currentServer, state), state)
view.setupFacebookButtonListener(
OauthHelper.getFacebookOauthUrl(
clientId,
currentServer,
state
), state
)
view.enableLoginByFacebook()
totalSocialAccountsEnabled++
}
......@@ -163,7 +172,12 @@ class LoginPresenter @Inject constructor(
if (settings.isGithubAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_GITHUB)
if (clientId != null) {
view.setupGithubButtonListener(OauthHelper.getGithubOauthUrl(clientId, state), state)
view.setupGithubButtonListener(
OauthHelper.getGithubOauthUrl(
clientId,
state
), state
)
view.enableLoginByGithub()
totalSocialAccountsEnabled++
}
......@@ -171,7 +185,13 @@ class LoginPresenter @Inject constructor(
if (settings.isGoogleAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_GOOGLE)
if (clientId != null) {
view.setupGoogleButtonListener(OauthHelper.getGoogleOauthUrl(clientId, currentServer, state), state)
view.setupGoogleButtonListener(
OauthHelper.getGoogleOauthUrl(
clientId,
currentServer,
state
), state
)
view.enableLoginByGoogle()
totalSocialAccountsEnabled++
}
......@@ -179,7 +199,13 @@ class LoginPresenter @Inject constructor(
if (settings.isLinkedinAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_LINKEDIN)
if (clientId != null) {
view.setupLinkedinButtonListener(OauthHelper.getLinkedinOauthUrl(clientId, currentServer, state), state)
view.setupLinkedinButtonListener(
OauthHelper.getLinkedinOauthUrl(
clientId,
currentServer,
state
), state
)
view.enableLoginByLinkedin()
totalSocialAccountsEnabled++
}
......@@ -303,6 +329,12 @@ class LoginPresenter @Inject constructor(
saveAccount(username)
saveToken(token)
registerPushToken()
if (loginType == TYPE_LOGIN_USER_EMAIL) {
loginCredentials = Credential.Builder(usernameOrEmail)
.setPassword(password)
.build()
view.saveSmartLockCredentials(loginCredentials)
}
navigator.toChatList()
} else if (loginType == TYPE_LOGIN_OAUTH) {
navigator.toRegisterUsername(token.userId, token.authToken)
......@@ -333,7 +365,7 @@ class LoginPresenter @Inject constructor(
}.toString()
}
private fun getCustomOauthServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> {
private fun getCustomOauthServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> {
return listMap.filter { map -> map["custom"] == true }
}
......
......@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import com.google.android.gms.auth.api.credentials.Credential
interface LoginView : LoadingView, MessageView {
......@@ -223,4 +224,9 @@ interface LoginView : LoadingView, MessageView {
* Alerts the user about a wrong inputted password.
*/
fun alertWrongPassword()
/**
* Save credentials via google smart lock
*/
fun saveSmartLockCredentials(loginCredential: Credential?)
}
\ No newline at end of file
......@@ -2,7 +2,9 @@ package chat.rocket.android.authentication.login.ui
import DrawableHelper
import android.app.Activity
import android.app.PendingIntent
import android.content.Intent
import android.content.IntentSender
import android.graphics.PorterDuff
import android.os.Build
import android.os.Bundle
......@@ -12,12 +14,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.*
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.fragment.app.FragmentActivity
import chat.rocket.android.R
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.login.presentation.LoginPresenter
......@@ -31,22 +31,37 @@ import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_SECRET
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN
import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent
import chat.rocket.common.util.ifNull
import com.google.android.gms.auth.api.Auth
import com.google.android.gms.auth.api.credentials.*
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.GoogleApiClient
import com.google.android.gms.common.api.ResolvingResultCallbacks
import com.google.android.gms.common.api.Status
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import timber.log.Timber
import javax.inject.Inject
internal const val REQUEST_CODE_FOR_CAS = 1
internal const val REQUEST_CODE_FOR_OAUTH = 2
internal const val MULTIPLE_CREDENTIALS_READ = 3
internal const val NO_CREDENTIALS_EXIST = 4
internal const val SAVE_CREDENTIALS = 5
lateinit var googleApiClient: GoogleApiClient
class LoginFragment : Fragment(), LoginView {
class LoginFragment : Fragment(), LoginView, GoogleApiClient.ConnectionCallbacks {
@Inject
lateinit var presenter: LoginPresenter
private var isOauthViewEnable = false
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
areLoginOptionsNeeded()
}
private var isOauthSuccessful = false
private var isGlobalLayoutListenerSetUp = false
private var deepLinkInfo: LoginDeepLinkInfo? = null
private var credentialsToBeSaved: Credential? = null
companion object {
private const val DEEP_LINK_INFO = "DeepLinkInfo"
......@@ -58,9 +73,17 @@ class LoginFragment : Fragment(), LoginView {
}
}
override fun onConnected(bundle: Bundle?) {
saveSmartLockCredentials(credentialsToBeSaved)
}
override fun onConnectionSuspended(errorCode: Int) {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
buildGoogleApiClient()
deepLinkInfo = arguments?.getParcelable(DEEP_LINK_INFO)
}
......@@ -95,18 +118,155 @@ class LoginFragment : Fragment(), LoginView {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
data?.apply {
presenter.authenticateWithCas(getStringExtra(INTENT_CAS_TOKEN))
if (data != null) {
when (requestCode) {
REQUEST_CODE_FOR_CAS -> data.apply {
presenter.authenticateWithCas(getStringExtra(INTENT_CAS_TOKEN))
}
REQUEST_CODE_FOR_OAUTH -> {
isOauthSuccessful = true
data.apply {
presenter.authenticateWithOauth(
getStringExtra(INTENT_OAUTH_CREDENTIAL_TOKEN),
getStringExtra(INTENT_OAUTH_CREDENTIAL_SECRET)
)
}
}
MULTIPLE_CREDENTIALS_READ -> {
val loginCredentials: Credential =
data.getParcelableExtra(Credential.EXTRA_KEY)
handleCredential(loginCredentials)
}
NO_CREDENTIALS_EXIST -> {
//use the hints to autofill sign in forms to reduce the info to be filled
val loginCredentials: Credential =
data.getParcelableExtra(Credential.EXTRA_KEY)
val email = loginCredentials.id
val password = loginCredentials.password
text_username_or_email.setText(email)
text_password.setText(password)
}
SAVE_CREDENTIALS -> Toast.makeText(
context,
getString(R.string.message_credentials_saved_successfully),
Toast.LENGTH_SHORT
).show()
}
} else if (requestCode == REQUEST_CODE_FOR_OAUTH) {
data?.apply {
presenter.authenticateWithOauth(
getStringExtra(INTENT_OAUTH_CREDENTIAL_TOKEN),
getStringExtra(INTENT_OAUTH_CREDENTIAL_SECRET)
}
}
//cancel button pressed by the user in case of reading from smart lock
else if (resultCode == Activity.RESULT_CANCELED && requestCode == REQUEST_CODE_FOR_OAUTH) {
Timber.d("Returned from oauth")
}
}
override fun onDestroy() {
super.onDestroy()
googleApiClient.let {
activity?.let { it1 -> it.stopAutoManage(it1) }
it.disconnect()
}
}
private fun buildGoogleApiClient() {
googleApiClient = GoogleApiClient.Builder(context!!)
.enableAutoManage(activity as FragmentActivity, {
Timber.e("ERROR: Connection to client failed")
})
.addConnectionCallbacks(this)
.addApi(Auth.CREDENTIALS_API)
.build()
}
override fun onStart() {
super.onStart()
if (!isOauthSuccessful) {
requestCredentials()
}
}
private fun requestCredentials() {
val request: CredentialRequest = CredentialRequest.Builder()
.setPasswordLoginSupported(true)
.build()
Auth.CredentialsApi.request(googleApiClient, request)
.setResultCallback { credentialRequestResult ->
val status = credentialRequestResult.status
when {
status.isSuccess -> handleCredential(credentialRequestResult.credential)
(status.statusCode == CommonStatusCodes.RESOLUTION_REQUIRED) -> resolveResult(
status,
MULTIPLE_CREDENTIALS_READ
)
(status.statusCode == CommonStatusCodes.SIGN_IN_REQUIRED) -> {
val hintRequest: HintRequest = HintRequest.Builder()
.setHintPickerConfig(
CredentialPickerConfig.Builder()
.setShowCancelButton(true)
.build()
)
.setEmailAddressIdentifierSupported(true)
.setAccountTypes(IdentityProviders.GOOGLE)
.build()
val intent: PendingIntent =
Auth.CredentialsApi.getHintPickerIntent(googleApiClient, hintRequest)
try {
startIntentSenderForResult(
intent.intentSender,
NO_CREDENTIALS_EXIST,
null,
0,
0,
0,
null
)
} catch (e: IntentSender.SendIntentException) {
Timber.e("ERROR: Could not start hint picker Intent")
}
}
else -> Timber.d("ERROR: nothing happening")
}
}
}
private fun handleCredential(loginCredentials: Credential) {
if (loginCredentials.accountType == null) {
presenter.authenticateWithUserAndPassword(
loginCredentials.id,
loginCredentials.password.toString()
)
}
}
private fun resolveResult(status: Status, requestCode: Int) {
try {
status.startResolutionForResult(activity, requestCode)
} catch (e: IntentSender.SendIntentException) {
Timber.e("Failed to send Credentials intent")
}
}
override fun saveSmartLockCredentials(loginCredential: Credential?) {
credentialsToBeSaved = loginCredential
if (credentialsToBeSaved == null) {
return
}
activity?.let {
Auth.CredentialsApi.save(googleApiClient, credentialsToBeSaved).setResultCallback(
object : ResolvingResultCallbacks<Status>(it, SAVE_CREDENTIALS) {
override fun onSuccess(status: Status) {
Timber.d("credentials save:SUCCESS:$status")
credentialsToBeSaved = null
}
override fun onUnresolvableFailure(status: Status) {
Timber.e("credentials save:FAILURE:$status")
credentialsToBeSaved = null
}
})
}
}
......
......@@ -32,6 +32,7 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
}
fun connect(server: String) {
//code that leads to login screen (smart lock will be implemented after this)
connectToServer(server) {
navigator.toLogin()
}
......@@ -64,6 +65,7 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
}
fun deepLink(deepLinkInfo: LoginDeepLinkInfo) {
//code that leads to login screen (smart lock will be implemented after this)
connectToServer(deepLinkInfo.url) {
navigator.toLogin(deepLinkInfo)
}
......
......@@ -6,12 +6,7 @@ import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.privacyPolicyUrl
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.extensions.termsOfServiceUrl
import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
......@@ -20,17 +15,20 @@ import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.signup
import chat.rocket.core.model.Myself
import com.google.android.gms.auth.api.credentials.Credential
import javax.inject.Inject
class SignupPresenter @Inject constructor(private val view: SignupView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor) {
class SignupPresenter @Inject constructor(
private val view: SignupView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor
) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
......@@ -66,6 +64,10 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me)
registerPushToken()
val loginCredentials = Credential.Builder(email)
.setPassword(password)
.build()
view.saveSmartLockCredentials(loginCredentials)
navigator.toChatList()
} catch (exception: RocketChatException) {
exception.message?.let {
......
......@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.signup.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import com.google.android.gms.auth.api.credentials.Credential
interface SignupView : LoadingView, MessageView {
......@@ -24,4 +25,9 @@ interface SignupView : LoadingView, MessageView {
* Alerts the user about a blank email.
*/
fun alertBlankEmail()
/**
* Save credentials via google smart lock
*/
fun saveSmartLockCredentials(loginCredential: Credential)
}
\ No newline at end of file
package chat.rocket.android.authentication.signup.ui
import DrawableHelper
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.text.style.ClickableSpan
import android.view.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.login.ui.googleApiClient
import chat.rocket.android.authentication.signup.presentation.SignupPresenter
import chat.rocket.android.authentication.signup.presentation.SignupView
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.*
import com.google.android.gms.auth.api.Auth
import com.google.android.gms.auth.api.credentials.Credential
import com.google.android.gms.common.api.ResolvingResultCallbacks
import com.google.android.gms.common.api.Status
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
import timber.log.Timber
import javax.inject.Inject
internal const val SAVE_CREDENTIALS = 1
class SignupFragment : Fragment(), SignupView {
@Inject lateinit var presenter: SignupPresenter
@Inject
lateinit var presenter: SignupPresenter
private lateinit var credentialsToBeSaved: Credential
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)) {
bottom_container.setVisible(false)
......@@ -39,7 +56,11 @@ class SignupFragment : Fragment(), SignupView {
AndroidSupportInjection.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_authentication_sign_up, container, false)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_authentication_sign_up, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -53,7 +74,12 @@ class SignupFragment : Fragment(), SignupView {
setUpNewUserAgreementListener()
button_sign_up.setOnClickListener {
presenter.signup(text_username.textContent, text_username.textContent, text_password.textContent, text_email.textContent)
presenter.signup(
text_username.textContent,
text_username.textContent,
text_password.textContent,
text_email.textContent
)
}
}
......@@ -94,6 +120,44 @@ class SignupFragment : Fragment(), SignupView {
}
}
override fun saveSmartLockCredentials(loginCredential: Credential) {
credentialsToBeSaved = loginCredential
googleApiClient.let {
if (it.isConnected) {
saveCredentials()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == SAVE_CREDENTIALS) {
if (resultCode == RESULT_OK) {
Toast.makeText(
context,
getString(R.string.message_credentials_saved_successfully),
Toast.LENGTH_SHORT
).show()
} else {
Timber.e("ERROR: Cancelled by user")
}
}
}
private fun saveCredentials() {
activity?.let {
Auth.CredentialsApi.save(googleApiClient, credentialsToBeSaved).setResultCallback(
object : ResolvingResultCallbacks<Status>(it, SAVE_CREDENTIALS) {
override fun onSuccess(status: Status) {
Timber.d("save:SUCCESS:$status")
}
override fun onUnresolvableFailure(status: Status) {
Timber.e("save:FAILURE:$status")
}
})
}
}
override fun showLoading() {
ui {
enableUserInput(false)
......@@ -126,7 +190,8 @@ class SignupFragment : Fragment(), SignupView {
private fun tintEditTextDrawableStart() {
ui {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, it)
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)
......@@ -134,14 +199,22 @@ class SignupFragment : Fragment(), SignupView {
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_password, text_email), drawables)
DrawableHelper.compoundDrawables(
arrayOf(
text_name,
text_username,
text_password,
text_email
), drawables
)
}
}
private fun setUpNewUserAgreementListener() {
val termsOfService = getString(R.string.action_terms_of_service)
val privacyPolicy = getString(R.string.action_privacy_policy)
val newUserAgreement = String.format(getString(R.string.msg_new_user_agreement), termsOfService, privacyPolicy)
val newUserAgreement =
String.format(getString(R.string.msg_new_user_agreement), termsOfService, privacyPolicy)
text_new_user_agreement.text = newUserAgreement
......@@ -157,7 +230,11 @@ class SignupFragment : Fragment(), SignupView {
}
}
TextHelper.addLink(text_new_user_agreement, arrayOf(termsOfService, privacyPolicy), arrayOf(termsOfServiceListener, privacyPolicyListener))
TextHelper.addLink(
text_new_user_agreement,
arrayOf(termsOfService, privacyPolicy),
arrayOf(termsOfServiceListener, privacyPolicyListener)
)
}
private fun enableUserInput(value: Boolean) {
......
......@@ -21,8 +21,10 @@ import kotlinx.coroutines.experimental.launch
import javax.inject.Inject
class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject lateinit var presenter: AuthenticationPresenter
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var presenter: AuthenticationPresenter
val job = Job()
override fun onCreate(savedInstanceState: Bundle?) {
......@@ -43,6 +45,14 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
if (currentFragment != null) {
currentFragment.onActivityResult(requestCode, resultCode, data)
}
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
......
......@@ -14,13 +14,12 @@ import timber.log.Timber
import java.security.InvalidParameterException
class ChatRoomAdapter(
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
private val roomType: String? = null,
private val roomName: String? = null,
private val presenter: ChatRoomPresenter? = null,
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseViewModel<*>>()
init {
......@@ -65,6 +64,12 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_file_attachment)
GenericFileAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.MESSAGE_REPLY -> {
val view = parent.inflate(R.layout.item_message_reply)
MessageReplyViewHolder(view, actionsListener, reactionListener) { roomName, permalink ->
presenter?.openDirectMessage(roomName, permalink)
}
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
......@@ -98,15 +103,26 @@ class ChatRoomAdapter(
}
when (holder) {
is MessageViewHolder -> holder.bind(dataSet[position] as MessageViewModel)
is ImageAttachmentViewHolder -> holder.bind(dataSet[position] as ImageAttachmentViewModel)
is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel)
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)
is ColorAttachmentViewHolder -> holder.bind(dataSet[position] as ColorAttachmentViewModel)
is GenericFileAttachmentViewHolder -> holder.bind(dataSet[position] as GenericFileAttachmentViewModel)
is MessageViewHolder ->
holder.bind(dataSet[position] as MessageViewModel)
is ImageAttachmentViewHolder ->
holder.bind(dataSet[position] as ImageAttachmentViewModel)
is AudioAttachmentViewHolder ->
holder.bind(dataSet[position] as AudioAttachmentViewModel)
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)
is ColorAttachmentViewHolder ->
holder.bind(dataSet[position] as ColorAttachmentViewModel)
is GenericFileAttachmentViewHolder ->
holder.bind(dataSet[position] as GenericFileAttachmentViewModel)
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyViewModel)
}
}
......@@ -147,7 +163,7 @@ class ChatRoomAdapter(
}
fun updateItem(message: BaseViewModel<*>) {
var index = dataSet.indexOfLast { it.messageId == message.messageId }
val index = dataSet.indexOfLast { it.messageId == message.messageId }
val indexOfNext = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index")
if (index > -1) {
......@@ -188,10 +204,14 @@ class ChatRoomAdapter(
message.apply {
when (item.itemId) {
R.id.action_message_reply -> {
presenter?.citeMessage(roomType, id, true)
if (roomName != null && roomType != null) {
presenter?.citeMessage(roomName, roomType, id, true)
}
}
R.id.action_message_quote -> {
presenter?.citeMessage(roomType, id, false)
if (roomName != null && roomType != null) {
presenter?.citeMessage(roomName, roomType, id, false)
}
}
R.id.action_message_copy -> {
presenter?.copyMessage(id)
......
......@@ -3,6 +3,7 @@ package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.net.Uri
import android.view.View
import androidx.core.net.toUri
import chat.rocket.android.chatroom.viewmodel.GenericFileAttachmentViewModel
import chat.rocket.android.util.extensions.content
import chat.rocket.android.widget.emoji.EmojiReactionListener
......@@ -25,7 +26,7 @@ class GenericFileAttachmentViewHolder(itemView: View,
text_file_name.content = data.attachmentTitle
text_file_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.attachmentUrl)))
it.context.startActivity(Intent(Intent.ACTION_VIEW, data.attachmentUrl.toUri()))
}
}
}
......
package chat.rocket.android.chatroom.adapter
import android.Manifest
import android.app.Activity
import android.graphics.Color
import android.graphics.Typeface
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Environment
import com.google.android.material.appbar.AppBarLayout
import androidx.appcompat.widget.Toolbar
import android.text.TextUtils
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.setPadding
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import chat.rocket.android.helper.AndroidPermissionsHelper
import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.widget.emoji.EmojiReactionListener
import com.facebook.binaryresource.FileBinaryResource
import com.facebook.cache.common.CacheKey
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imageformat.ImageFormatChecker
import com.facebook.imagepipeline.cache.DefaultCacheKeyFactory
import com.facebook.imagepipeline.core.ImagePipelineFactory
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.*
import timber.log.Timber
import java.io.File
class ImageAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
private var cacheKey: CacheKey? = null
class ImageAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
......@@ -58,123 +28,13 @@ class ImageAttachmentViewHolder(itemView: View,
}.build()
image_attachment.controller = controller
file_name.text = data.attachmentTitle
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
// TODO - We should definitely write our own ImageViewer
var imageViewer: ImageViewer? = null
val request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(data.attachmentUrl))
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.DISK_CACHE)
.build()
cacheKey = DefaultCacheKeyFactory.getInstance()
.getEncodedCacheKey(request, null)
val pad = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_padding)
val lparams = AppBarLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
val toolbar = Toolbar(context).also {
it.inflateMenu(R.menu.image_actions)
it.overflowIcon?.setTint(Color.WHITE)
it.setOnMenuItemClickListener {
return@setOnMenuItemClickListener when (it.itemId) {
R.id.action_save_image -> saveImage()
else -> super.onMenuItemClick(it)
}
}
val titleSize = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_title)
val titleTextView = TextView(context).also {
it.text = data.attachmentTitle
it.setTextColor(Color.WHITE)
it.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize.toFloat())
it.ellipsize = TextUtils.TruncateAt.END
it.setSingleLine()
it.typeface = Typeface.DEFAULT_BOLD
it.setPadding(pad)
}
val backArrowView = ImageView(context).also {
it.setImageResource(R.drawable.ic_arrow_back_white_24dp)
it.setOnClickListener { imageViewer?.onDismiss() }
it.setPadding(0, pad ,pad, pad)
}
val layoutParams = AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.WRAP_CONTENT,
AppBarLayout.LayoutParams.WRAP_CONTENT
)
it.addView(backArrowView, layoutParams)
it.addView(titleTextView, layoutParams)
}
val appBarLayout = AppBarLayout(context).also {
it.layoutParams = lparams
it.setBackgroundColor(Color.BLACK)
it.addView(toolbar, AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
))
}
val builder = ImageViewer.createPipelineDraweeControllerBuilder()
.setImageRequest(request)
.setAutoPlayAnimations(true)
imageViewer = ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
.setOverlayView(appBarLayout)
.setStartPosition(0)
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
.show()
image_attachment.setOnClickListener {
ImageHelper.openImage(
it.context,
data.attachmentUrl,
data.attachmentTitle.toString()
)
}
}
}
private fun saveImage(): Boolean {
if (!canWriteToExternalStorage()) {
checkWritingPermission()
return false
}
if (ImagePipelineFactory.getInstance().mainFileCache.hasKey(cacheKey)) {
val context = itemView.context
val resource = ImagePipelineFactory.getInstance().mainFileCache.getResource(cacheKey)
val cachedFile = (resource as FileBinaryResource).file
val imageFormat = ImageFormatChecker.getImageFormat(resource.openStream())
val imageDir = "${Environment.DIRECTORY_PICTURES}/Rocket.Chat Images/"
val imagePath = Environment.getExternalStoragePublicDirectory(imageDir)
val imageFile = File(imagePath, "${cachedFile.nameWithoutExtension}.${imageFormat.fileExtension}")
imagePath.mkdirs()
imageFile.createNewFile()
try {
cachedFile.copyTo(imageFile, true)
MediaScannerConnection.scanFile(context, arrayOf(imageFile.absolutePath), null) { path, uri ->
Timber.i("Scanned $path:")
Timber.i("-> uri=$uri")
}
} catch (ex: Exception) {
Timber.e(ex)
val message = context.getString(R.string.msg_image_saved_failed)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} finally {
val message = context.getString(R.string.msg_image_saved_successfully)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
return true
}
private fun canWriteToExternalStorage(): Boolean {
return AndroidPermissionsHelper.checkPermission(itemView.context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
private fun checkWritingPermission() {
val context = itemView.context
if (context is ContextThemeWrapper && context.baseContext is Activity) {
val activity = context.baseContext as Activity
AndroidPermissionsHelper.requestPermission(activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
AndroidPermissionsHelper.WRITE_EXTERNAL_STORAGE_CODE)
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageReplyViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message_reply.view.*
class MessageReplyViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
private val replyCallback: (roomName: String, permalink: String) -> Unit
) : BaseViewHolder<MessageReplyViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(itemView)
}
}
override fun bindViews(data: MessageReplyViewModel) {
with(itemView) {
button_message_reply.setOnClickListener {
with(data.rawData) {
replyCallback.invoke(roomName, permalink)
}
}
}
}
}
\ No newline at end of file
......@@ -37,7 +37,7 @@ class MessageViewHolder(
if (data.isTemporary) Color.GRAY else Color.BLACK
)
data.message.let {
text_edit_indicator.isVisible = it.isSystemMessage() && it.editedBy != null
text_edit_indicator.isVisible = !it.isSystemMessage() && it.editedBy != null
image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false
}
}
......
package chat.rocket.android.chatroom.domain
data class MessageReply(
val roomName: String,
val permalink: String
)
\ No newline at end of file
......@@ -2,21 +2,33 @@ package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.members.ui.newInstance
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack
class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
fun toMembersList(chatRoomId: String, chatRoomType: String) {
fun toMembersList(chatRoomId: String) {
activity.addFragmentBackStack("MembersFragment", R.id.fragment_container) {
newInstance(chatRoomId, chatRoomType)
chat.rocket.android.members.ui.newInstance(chatRoomId)
}
}
fun toPinnedMessageList(chatRoomId: String, chatRoomType: String) {
activity.addFragmentBackStack("PinnedMessages", R.id.fragment_container){
chat.rocket.android.pinnedmessages.ui.newInstance(chatRoomId,chatRoomType)
fun toPinnedMessageList(chatRoomId: String) {
activity.addFragmentBackStack("PinnedMessages", R.id.fragment_container) {
chat.rocket.android.pinnedmessages.ui.newInstance(chatRoomId)
}
}
fun toFavoriteMessageList(chatRoomId: String) {
activity.addFragmentBackStack("FavoriteMessages", R.id.fragment_container) {
chat.rocket.android.favoritemessages.ui.newInstance(chatRoomId)
}
}
fun toFileList(chatRoomId: String) {
activity.addFragmentBackStack("Files", R.id.fragment_container) {
chat.rocket.android.files.ui.newInstance(chatRoomId)
}
}
......@@ -24,4 +36,17 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
activity.startActivity(activity.changeServerIntent())
activity.finish()
}
fun toDirectMessage(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean,
isChatRoomCreator: Boolean,
chatRoomMessage: String) {
activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
}
\ No newline at end of file
......@@ -8,6 +8,7 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewMod
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
interface ChatRoomView : LoadingView, MessageView {
......@@ -122,9 +123,9 @@ interface ChatRoomView : LoadingView, MessageView {
/**
* This user has joined the chat callback.
*
* @param canPost Whether the user can post a message or not.
* @param userCanPost Whether the user can post a message or not.
*/
fun onJoined(canPost: Boolean)
fun onJoined(userCanPost: Boolean)
fun showReactionsPopup(messageId: String)
......@@ -135,5 +136,14 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>)
fun onRoomChanged(canPost: Boolean)
/**
* Communicate whether it's a broadcast channel and if current user can post to it.
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
/**
* Open a DM with the user in the given [chatRoom] and pass the [permalink] for the message
* to reply.
*/
fun openDirectMessage(chatRoom: ChatRoom, permalink: String)
}
\ No newline at end of file
......@@ -60,13 +60,22 @@ class MessageService : JobService() {
)
messageRepository.save(message.copy(isTemporary = false))
Timber.d("Sent scheduled message given by id: ${message.id}")
} catch (ex: RocketChatException) {
} catch (ex: Exception) {
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))
// TODO - remove the generic message when we implement :userId:/message subscription
if (ex is IllegalStateException) {
Timber.d(ex, "Probably a read-only problem...")
// TODO: For now we are only going to reschedule when api is fixed.
messageRepository.removeById(message.id)
jobFinished(params, false)
} else {
// some other error
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)
}
jobFinished(params, true)
}
}
}
......
......@@ -6,6 +6,8 @@ import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.appcompat.app.AppCompatActivity
import android.text.SpannableStringBuilder
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.server.domain.GetCurrentServerInteractor
......@@ -28,7 +30,8 @@ fun Context.chatRoomIntent(
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean = true,
isChatRoomOwner: Boolean = false
isChatRoomCreator: Boolean = false,
chatRoomMessage: String? = null
): Intent {
return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
......@@ -37,7 +40,8 @@ fun Context.chatRoomIntent(
putExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putExtra(INTENT_CHAT_IS_SUBSCRIBED, isChatRoomSubscribed)
putExtra(INTENT_CHAT_ROOM_IS_OWNER, isChatRoomOwner)
putExtra(INTENT_CHAT_ROOM_IS_CREATOR, isChatRoomCreator)
putExtra(INTENT_CHAT_ROOM_MESSAGE, chatRoomMessage)
}
}
......@@ -45,25 +49,22 @@ private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
private const val INTENT_CHAT_ROOM_IS_READ_ONLY = "chat_room_is_read_only"
private const val INTENT_CHAT_ROOM_IS_OWNER = "chat_room_is_owner"
private const val INTENT_CHAT_ROOM_IS_CREATOR = "chat_room_is_creator"
private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val INTENT_CHAT_IS_SUBSCRIBED = "is_chat_room_subscribed"
private const val INTENT_CHAT_ROOM_MESSAGE = "chat_room_message"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
// TODO - workaround for now... We will move to a single activity
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var navigator: ChatRoomNavigator
@Inject lateinit var managerFactory: ConnectionManagerFactory
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private var isChatRoomSubscribed: Boolean = true
private var isChatRoomOwner: Boolean = false
private var chatRoomLastSeen: Long = -1L
@Inject
lateinit var serverInteractor: GetCurrentServerInteractor
@Inject
lateinit var navigator: ChatRoomNavigator
@Inject
lateinit var managerFactory: ConnectionManagerFactory
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
......@@ -79,31 +80,33 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
return
}
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
val chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
chatRoomName = intent.getStringExtra(INTENT_CHAT_ROOM_NAME)
val chatRoomName = intent.getStringExtra(INTENT_CHAT_ROOM_NAME)
requireNotNull(chatRoomName) { "no chat_room_name provided in Intent extras" }
chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
val chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true)
requireNotNull(isChatRoomReadOnly) { "no chat_room_is_read_only provided in Intent extras" }
val isChatRoomReadOnly = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true)
isChatRoomOwner = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_OWNER, false)
requireNotNull(isChatRoomOwner) { "no chat_room_is_owner provided in Intent extras" }
val isChatRoomCreator = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_CREATOR, false)
setupToolbar()
val chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
val isChatRoomSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true)
chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
val chatRoomMessage = intent.getStringExtra(INTENT_CHAT_ROOM_MESSAGE)
isChatRoomSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true)
setupToolbar()
if (supportFragmentManager.findFragmentByTag(TAG_CHAT_ROOM_FRAGMENT) == null) {
addFragment(TAG_CHAT_ROOM_FRAGMENT, R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
isChatRoomSubscribed, isChatRoomOwner)
newInstance(
chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage
)
}
}
}
......@@ -116,47 +119,38 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector
}
fun showRoomTypeIcon(showRoomTypeIcon: Boolean) {
if (showRoomTypeIcon) {
val roomType = roomTypeOf(chatRoomType)
val drawable = when (roomType) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, this)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, this)
}
is RoomType.DirectMessage -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, this)
}
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.white)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
}
} else {
text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
toolbar.setNavigationOnClickListener { finishActivity() }
}
fun showToolbarTitle(title: String) {
text_room_name.textContent = title
}
showRoomTypeIcon(true)
fun showToolbarChatRoomIcon(chatRoomType: String) {
val drawable = when (roomTypeOf(chatRoomType)) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_hashtag_black_12dp, this)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_12_dp, this)
}
else -> null
}
toolbar.setNavigationOnClickListener {
finishActivity()
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.colorWhite)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
}
}
fun setupToolbarTitle(toolbarTitle: String) {
text_room_name.textContent = toolbarTitle
fun hideToolbarChatRoomIcon() {
text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
private fun finishActivity() {
......
......@@ -26,7 +26,7 @@ class ActionListAdapter(
holder.itemView.setOnClickListener {
callback?.onMenuItemClick(item)
}
val deleteTextColor = holder.itemView.context.resources.getColor(R.color.red)
val deleteTextColor = holder.itemView.context.resources.getColor(R.color.colorRed)
val color = if (item.itemId == R.id.action_message_delete) {
deleteTextColor
} else {
......
......@@ -24,7 +24,8 @@ interface BaseViewModel<out T> {
MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8),
GENERIC_FILE_ATTACHMENT(9)
GENERIC_FILE_ATTACHMENT(9),
MESSAGE_REPLY(10)
}
}
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.android.chatroom.domain.MessageReply
import chat.rocket.core.model.Message
data class MessageReplyViewModel(
override val rawData: MessageReply,
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>?,
override var preview: Message?,
override var isTemporary: Boolean = false,
override val message: Message
) : BaseViewModel<MessageReply> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_REPLY.viewType
override val layoutId: Int
get() = R.layout.item_message_reply
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.core.model.ChatRoomRole
data class RoomViewModel(
val roles: List<ChatRoomRole>,
val isBroadcast: Boolean = false,
val isRoom: Boolean = false
)
\ No newline at end of file
......@@ -13,16 +13,32 @@ import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.text.scale
import chat.rocket.android.R
import chat.rocket.android.chatroom.domain.MessageReply
import chat.rocket.android.helper.MessageHelper
import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.baseUrl
import chat.rocket.android.server.domain.useRealName
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.ChatRoom
import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.*
import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.AudioAttachment
import chat.rocket.core.model.attachment.AuthorAttachment
import chat.rocket.core.model.attachment.ColorAttachment
import chat.rocket.core.model.attachment.FileAttachment
import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.MessageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool
......@@ -34,6 +50,8 @@ import javax.inject.Inject
class ViewModelMapper @Inject constructor(
private val context: Context,
private val parser: MessageParser,
private val roomsInteractor: ChatRoomsInteractor,
private val messageHelper: MessageHelper,
tokenRepository: TokenRepository,
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor,
......@@ -47,21 +65,24 @@ class ViewModelMapper @Inject constructor(
private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
private val secondaryTextColor = ContextCompat.getColor(context, R.color.colorSecondaryText)
suspend fun map(message: Message): List<BaseViewModel<*>> {
return translate(message)
suspend fun map(message: Message, roomViewModel: RoomViewModel = RoomViewModel(
roles = emptyList(), isBroadcast = true)): List<BaseViewModel<*>> {
return translate(message, roomViewModel)
}
suspend fun map(messages: List<Message>): List<BaseViewModel<*>> = withContext(CommonPool) {
suspend fun map(messages: List<Message>, roomViewModel: RoomViewModel = RoomViewModel(
roles = emptyList(), isBroadcast = true)): List<BaseViewModel<*>> = withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>(messages.size)
messages.forEach {
list.addAll(translate(it))
list.addAll(translate(it, roomViewModel))
}
return@withContext list
}
private suspend fun translate(message: Message): List<BaseViewModel<*>> = withContext(CommonPool) {
private suspend fun translate(message: Message, roomViewModel: RoomViewModel)
: List<BaseViewModel<*>> = withContext(CommonPool) {
val list = ArrayList<BaseViewModel<*>>()
message.urls?.forEach {
......@@ -86,9 +107,40 @@ class ViewModelMapper @Inject constructor(
list[i].nextDownStreamMessage = next
}
if (isBroadcastReplyAvailable(roomViewModel, message)) {
roomsInteractor.getById(currentServer, message.roomId)?.let { chatRoom ->
val replyViewModel = mapMessageReply(message, chatRoom)
list.first().nextDownStreamMessage = replyViewModel
list.add(0, replyViewModel)
}
}
return@withContext list
}
private fun isBroadcastReplyAvailable(roomViewModel: RoomViewModel, message: Message): Boolean {
val senderUsername = message.sender?.username
return roomViewModel.isRoom && roomViewModel.isBroadcast &&
!message.isSystemMessage() &&
senderUsername != currentUsername
}
private fun mapMessageReply(message: Message, chatRoom: ChatRoom): MessageReplyViewModel {
val name = message.sender?.name
val roomName = if (settings.useRealName() && name != null) name else message.sender?.username
?: ""
val permalink = messageHelper.createPermalink(message, chatRoom)
return MessageReplyViewModel(
messageId = message.id,
isTemporary = false,
reactions = emptyList(),
message = message,
preview = mapMessagePreview(message),
rawData = MessageReply(roomName = roomName, permalink = permalink),
nextDownStreamMessage = null
)
}
private fun mapUrl(message: Message, url: Url): BaseViewModel<*>? {
if (url.ignoreParse || url.meta == null) return null
......@@ -313,7 +365,6 @@ class ViewModelMapper @Inject constructor(
}
private fun getSystemMessage(message: Message): CharSequence {
println(message)
val content = when (message.type) {
//TODO: Add implementation for Welcome type.
is MessageType.MessageRemoved -> context.getString(R.string.message_removed)
......
......@@ -14,9 +14,9 @@ import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class RoomViewHolder(itemView: View, private val listener: (String) -> Unit) : ViewHolder<RoomItemHolder>(itemView) {
private val resources: Resources = itemView.resources
private val channelUnread: Drawable = resources.getDrawable(R.drawable.ic_hashtag_unread_12dp)
private val channelUnread: Drawable = resources.getDrawable(R.drawable.ic_hashtag_black_12dp)
private val channel: Drawable = resources.getDrawable(R.drawable.ic_hashtag_12dp)
private val groupUnread: Drawable = resources.getDrawable(R.drawable.ic_lock_unread_12_dp)
private val groupUnread: Drawable = resources.getDrawable(R.drawable.ic_lock_black_12_dp)
private val group: Drawable = resources.getDrawable(R.drawable.ic_lock_12_dp)
private val online: Drawable = resources.getDrawable(R.drawable.ic_status_online_12dp)
private val away: Drawable = resources.getDrawable(R.drawable.ic_status_away_12dp)
......
......@@ -11,7 +11,7 @@ import chat.rocket.android.helper.UserHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.server.domain.GetActiveUsersInteractor
import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.JobSchedulerInteractor
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor
......@@ -60,7 +60,7 @@ class ChatRoomsPresenter @Inject constructor(
private val navigator: MainNavigator,
@Named("currentServer") private val currentServer: String,
private val manager: ConnectionManager,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val chatRoomsInteractor: ChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val saveActiveUsersInteractor: SaveActiveUsersInteractor,
private val getActiveUsersInteractor: GetActiveUsersInteractor,
......@@ -164,7 +164,7 @@ class ChatRoomsPresenter @Inject constructor(
fun chatRoomsByName(name: String) {
launchUI(strategy) {
try {
val roomList = getChatRoomsInteractor.getAllByName(currentServer, name)
val roomList = chatRoomsInteractor.getAllByName(currentServer, name)
if (roomList.isEmpty()) {
val (users, rooms) = retryIO("spotlight($name)") {
client.spotlight(name)
......@@ -271,7 +271,7 @@ class ChatRoomsPresenter @Inject constructor(
fun updateSortedChatRooms() {
launchUI(strategy) {
val roomList = getChatRoomsInteractor.getAll(currentServer)
val roomList = chatRoomsInteractor.getAll(currentServer)
view.updateChatRooms(sortRooms(roomList))
}
}
......@@ -472,7 +472,7 @@ class ChatRoomsPresenter @Inject constructor(
// Update a ChatRoom with a Room information
private fun updateRoom(room: Room) {
Timber.d("Updating Room: ${room.id} - ${room.name}")
val chatRooms = getChatRoomsInteractor.getAll(currentServer).toMutableList()
val chatRooms = chatRoomsInteractor.getAll(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == room.id }
chatRoom?.apply {
val newRoom = ChatRoom(
......@@ -513,7 +513,7 @@ class ChatRoomsPresenter @Inject constructor(
// Update a ChatRoom with a Subscription information
private fun updateSubscription(subscription: Subscription) {
Timber.d("Updating subscription: ${subscription.id} - ${subscription.name}")
val chatRooms = getChatRoomsInteractor.getAll(currentServer).toMutableList()
val chatRooms = chatRoomsInteractor.getAll(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId }
chatRoom?.apply {
val newRoom = ChatRoom(
......@@ -553,7 +553,7 @@ class ChatRoomsPresenter @Inject constructor(
private fun removeRoom(
id: String,
chatRooms: MutableList<ChatRoom> = getChatRoomsInteractor.getAll(currentServer).toMutableList()
chatRooms: MutableList<ChatRoom> = chatRoomsInteractor.getAll(currentServer).toMutableList()
) {
Timber.d("Removing ROOM: $id")
synchronized(this) {
......@@ -594,7 +594,7 @@ class ChatRoomsPresenter @Inject constructor(
val username = user_.username
val status = user_.status
if (username != null && status != null) {
getChatRoomsInteractor.getByName(currentServer, username)?.let {
chatRoomsInteractor.getByName(currentServer, username)?.let {
val newRoom = ChatRoom(
id = it.id,
subscriptionId = it.id,
......@@ -622,10 +622,10 @@ class ChatRoomsPresenter @Inject constructor(
broadcast = it.broadcast
)
getChatRoomsInteractor.remove(currentServer, it)
getChatRoomsInteractor.add(currentServer, newRoom)
chatRoomsInteractor.remove(currentServer, it)
chatRoomsInteractor.add(currentServer, newRoom)
launchUI(strategy) {
view.updateChatRooms(sortRooms(getChatRoomsInteractor.getAll(currentServer)))
view.updateChatRooms(sortRooms(chatRoomsInteractor.getAll(currentServer)))
}
}
}
......@@ -635,7 +635,7 @@ class ChatRoomsPresenter @Inject constructor(
Timber.i("Updating ChatRooms")
launch(strategy.jobs) {
val chatRoomsWithPreview = getChatRoomsWithPreviews(
getChatRoomsInteractor.getAll(currentServer)
chatRoomsInteractor.getAll(currentServer)
)
val chatRoomsWithStatus = getChatRoomWithStatus(chatRoomsWithPreview)
view.updateChatRooms(chatRoomsWithStatus)
......
......@@ -92,11 +92,11 @@ class ChatRoomsAdapter(
private fun bindIcon(chatRoom: ChatRoom, imageView: ImageView) {
val drawable = when (chatRoom.type) {
is RoomType.Channel -> DrawableHelper.getDrawableFromId(
R.drawable.ic_hashtag_unread_12dp,
R.drawable.ic_hashtag_black_12dp,
context
)
is RoomType.PrivateGroup -> DrawableHelper.getDrawableFromId(
R.drawable.ic_lock_12_dp,
R.drawable.ic_lock_black_12_dp,
context
)
is RoomType.DirectMessage -> DrawableHelper.getUserStatusDrawable(
......
......@@ -50,7 +50,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
lateinit var presenter: ChatRoomsPresenter
@Inject
lateinit var factory: ChatRoomsViewModelFactory
@Inject
lateinit var dbManager: DatabaseManager // TODO - remove when moving ChatRoom screen to DB
......
......@@ -10,13 +10,15 @@ import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider
import chat.rocket.android.chatroom.di.ChatRoomModule
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider
import chat.rocket.android.chatroom.di.FavoriteMessagesFragmentProvider
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatrooms.di.ChatRoomsFragmentProvider
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.files.di.FilesFragmentProvider
import chat.rocket.android.main.di.MainModule
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.members.di.MembersFragmentProvider
import chat.rocket.android.pinnedmessages.di.PinnedMessagesFragmentProvider
import chat.rocket.android.profile.di.ProfileFragmentProvider
import chat.rocket.android.server.di.ChangeServerModule
import chat.rocket.android.server.ui.ChangeServerActivity
......@@ -29,28 +31,38 @@ import dagger.android.ContributesAndroidInjector
abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(modules = [AuthenticationModule::class,
ServerFragmentProvider::class,
LoginFragmentProvider::class,
RegisterUsernameFragmentProvider::class,
ResetPasswordFragmentProvider::class,
SignupFragmentProvider::class,
TwoFAFragmentProvider::class
])
@ContributesAndroidInjector(
modules = [AuthenticationModule::class,
ServerFragmentProvider::class,
LoginFragmentProvider::class,
RegisterUsernameFragmentProvider::class,
ResetPasswordFragmentProvider::class,
SignupFragmentProvider::class,
TwoFAFragmentProvider::class
]
)
abstract fun bindAuthenticationActivity(): AuthenticationActivity
@PerActivity
@ContributesAndroidInjector(modules = [MainModule::class,
ChatRoomsFragmentProvider::class,
ProfileFragmentProvider::class
])
@ContributesAndroidInjector(
modules = [MainModule::class,
ChatRoomsFragmentProvider::class,
ProfileFragmentProvider::class
]
)
abstract fun bindMainActivity(): MainActivity
@PerActivity
@ContributesAndroidInjector(modules = [ChatRoomModule::class,
ChatRoomFragmentProvider::class,
MembersFragmentProvider::class,
PinnedMessagesFragmentProvider::class])
@ContributesAndroidInjector(
modules = [
ChatRoomModule::class,
ChatRoomFragmentProvider::class,
MembersFragmentProvider::class,
PinnedMessagesFragmentProvider::class,
FavoriteMessagesFragmentProvider::class,
FilesFragmentProvider::class
]
)
abstract fun bindChatRoomActivity(): ChatRoomActivity
@PerActivity
......
package chat.rocket.android.chatroom.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesView
import chat.rocket.android.favoritemessages.ui.FavoriteMessagesFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class FavoriteMessagesFragmentModule {
@Provides
fun provideLifecycleOwner(frag: FavoriteMessagesFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
@Provides
fun provideFavoriteMessagesView(frag: FavoriteMessagesFragment): FavoriteMessagesView {
return frag
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.di
import chat.rocket.android.favoritemessages.ui.FavoriteMessagesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class FavoriteMessagesFragmentProvider {
@ContributesAndroidInjector(modules = [FavoriteMessagesFragmentModule::class])
abstract fun provideFavoriteMessageFragment(): FavoriteMessagesFragment
}
\ No newline at end of file
package chat.rocket.android.favoritemessages.presentation
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getFavoriteMessages
import timber.log.Timber
import javax.inject.Inject
class FavoriteMessagesPresenter @Inject constructor(
private val view: FavoriteMessagesView,
private val strategy: CancelStrategy,
private val roomsInteractor: ChatRoomsInteractor,
private val mapper: ViewModelMapper,
val serverInteractor: GetCurrentServerInteractor,
val factory: RocketChatClientFactory
) {
private val serverUrl = serverInteractor.get()!!
private val client = factory.create(serverUrl)
private var offset: Int = 0
/**
* Loads all favorite messages for room. the given room id.
*
* @param roomId The id of the room to get its favorite messages.
*/
fun loadFavoriteMessages(roomId: String) {
launchUI(strategy) {
try {
view.showLoading()
roomsInteractor.getById(serverUrl, roomId)?.let {
val favoriteMessages = client.getFavoriteMessages(roomId, it.type, offset)
val messageList = mapper.map(favoriteMessages.result)
view.showFavoriteMessages(messageList)
offset += 1 * 30
}.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server.")
}
} catch (exception: RocketChatException) {
Timber.e(exception)
} finally {
view.hideLoading()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.favoritemessages.presentation
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface FavoriteMessagesView : MessageView, LoadingView {
/**
* Shows the list of favorite messages for the current room.
*
* @param favoriteMessages The list of favorite messages to show.
*/
fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>)
}
\ No newline at end of file
package chat.rocket.android.favoritemessages.ui
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesPresenter
import chat.rocket.android.favoritemessages.presentation.FavoriteMessagesView
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.inflate
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_favorite_messages.*
import javax.inject.Inject
fun newInstance(chatRoomId: String): Fragment {
return FavoriteMessagesFragment().apply {
arguments = Bundle(1).apply {
putString(INTENT_CHAT_ROOM_ID, chatRoomId)
}
}
}
private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
private lateinit var chatRoomId: String
private lateinit var adapter: ChatRoomAdapter
@Inject
lateinit var presenter: FavoriteMessagesPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(INTENT_CHAT_ROOM_ID)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_favorite_messages)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
presenter.loadFavoriteMessages(chatRoomId)
}
override fun showFavoriteMessages(favoriteMessages: List<BaseViewModel<*>>) {
ui {
if (recycler_view.adapter == null) {
adapter = ChatRoomAdapter(enableActions = false)
recycler_view.adapter = adapter
val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
recycler_view.layoutManager = linearLayoutManager
recycler_view.itemAnimator = DefaultItemAnimator()
if (favoriteMessages.size > 10) {
recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView?
) {
presenter.loadFavoriteMessages(chatRoomId)
}
})
}
no_messages_view.isVisible = favoriteMessages.isEmpty()
}
adapter.appendData(favoriteMessages)
}
}
override fun showMessage(resId: Int) {
ui { showToast(resId) }
}
override fun showMessage(message: String) {
ui { showToast(message) }
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showLoading() {
ui { view_loading.isVisible = true }
}
override fun hideLoading() {
ui { view_loading.isVisible = false }
}
private fun setupToolbar() {
(activity as ChatRoomActivity).let {
it.showToolbarTitle(getString(R.string.title_favorite_messages))
it.hideToolbarChatRoomIcon()
}
}
}
\ No newline at end of file
package chat.rocket.android.files.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.files.viewmodel.FileViewModel
import chat.rocket.android.util.extensions.inflate
import kotlinx.android.synthetic.main.item_generic_attachment.view.*
class FilesAdapter(private val listener: (FileViewModel) -> Unit) :
RecyclerView.Adapter<FilesAdapter.ViewHolder>() {
private var dataSet: List<FileViewModel> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FilesAdapter.ViewHolder =
ViewHolder(parent.inflate(R.layout.item_generic_attachment))
override fun onBindViewHolder(holder: FilesAdapter.ViewHolder, position: Int) =
holder.bind(dataSet[position], listener)
override fun getItemCount(): Int = dataSet.size
fun prependData(dataSet: List<FileViewModel>) {
this.dataSet = dataSet
notifyItemRangeInserted(0, dataSet.size)
}
fun appendData(dataSet: List<FileViewModel>) {
val previousDataSetSize = this.dataSet.size
this.dataSet += dataSet
notifyItemRangeInserted(previousDataSetSize, dataSet.size)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(fileViewModel: FileViewModel, listener: (FileViewModel) -> Unit) {
with(itemView) {
when {
fileViewModel.isImage -> {
image_file_thumbnail.setImageURI(fileViewModel.url)
image_file_media_thumbnail.isVisible = false
image_file_thumbnail.isVisible = true
}
fileViewModel.isMedia -> {
image_file_media_thumbnail.setImageDrawable(
context.resources.getDrawable(
R.drawable.ic_play_arrow_black_24dp, null
)
)
image_file_thumbnail.isVisible = false
image_file_media_thumbnail.isVisible = true
}
else -> {
image_file_media_thumbnail.setImageDrawable(
context.resources.getDrawable(
R.drawable.ic_insert_drive_file_black_24dp, null
)
)
image_file_thumbnail.isVisible = false
image_file_media_thumbnail.isVisible = true
}
}
text_file_name.text = fileViewModel.name
text_uploader.text = fileViewModel.uploader
text_upload_date.text = fileViewModel.uploadDate
setOnClickListener { listener(fileViewModel) }
}
}
}
}
\ No newline at end of file
package chat.rocket.android.files.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.files.presentation.FilesView
import chat.rocket.android.files.ui.FilesFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class FilesFragmentModule {
@Provides
fun provideLifecycleOwner(frag: FilesFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
@Provides
fun provideFilesView(frag: FilesFragment): FilesView {
return frag
}
}
\ No newline at end of file
package chat.rocket.android.files.di
import chat.rocket.android.files.ui.FilesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class FilesFragmentProvider {
@ContributesAndroidInjector(modules = [FilesFragmentModule::class])
abstract fun provideFilesFragment(): FilesFragment
}
\ No newline at end of file
package chat.rocket.android.files.presentation
import androidx.core.net.toUri
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.files.viewmodel.FileViewModel
import chat.rocket.android.files.viewmodel.FileViewModelMapper
import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getFiles
import timber.log.Timber
import javax.inject.Inject
class FilesPresenter @Inject constructor(
private val view: FilesView,
private val strategy: CancelStrategy,
private val roomsInteractor: ChatRoomsInteractor,
private val mapper: FileViewModelMapper,
val serverInteractor: GetCurrentServerInteractor,
val factory: RocketChatClientFactory
) {
private val serverUrl = serverInteractor.get()!!
private val client = factory.create(serverUrl)
private var offset: Int = 0
/**
* Load all files for the given room id.
*
* @param roomId The id of the room to get files from.
*/
fun loadFiles(roomId: String) {
launchUI(strategy) {
try {
view.showLoading()
roomsInteractor.getById(serverUrl, roomId)?.let {
val files = client.getFiles(roomId, it.type, offset)
val filesViewModel = mapper.mapToViewModelList(files.result)
view.showFiles(filesViewModel, files.total)
offset += 1 * 30
}.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server.")
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
Timber.e(exception)
} finally {
view.hideLoading()
}
}
}
fun openFile(fileViewModel: FileViewModel) {
when {
fileViewModel.isImage -> fileViewModel.url?.let {
view.openImage(it, fileViewModel.name ?: "")
}
fileViewModel.isMedia -> fileViewModel.url?.let {
view.playMedia(it)
}
else -> fileViewModel.url?.let {
view.openDocument(it.toUri())
}
}
}
}
\ No newline at end of file
package chat.rocket.android.files.presentation
import android.net.Uri
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.files.viewmodel.FileViewModel
interface FilesView : MessageView, LoadingView {
/**
* Show list of files for the current room.
*
* @param dataSet The data set to show.
* @param total The total number of files.
*/
fun showFiles(dataSet: List<FileViewModel>, total: Long)
/**
* Plays a media file (audio/video).
*
* @param url The file url to play its media.
*/
fun playMedia(url: String)
/**
* Opens an image file
*
* @param url The file url to open its image.
* @param name The file name.
*/
fun openImage(url: String, name: String)
/**
* Opens a document file (.pdf, .txt and so on).
*
* @param uri The file uri to open its document.
*/
fun openDocument(uri: Uri)
}
\ No newline at end of file
package chat.rocket.android.files.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.files.adapter.FilesAdapter
import chat.rocket.android.files.presentation.FilesPresenter
import chat.rocket.android.files.presentation.FilesView
import chat.rocket.android.files.viewmodel.FileViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.widget.DividerItemDecoration
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_files.*
import javax.inject.Inject
fun newInstance(chatRoomId: String): Fragment {
return FilesFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
}
}
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
class FilesFragment : Fragment(), FilesView {
@Inject
lateinit var presenter: FilesPresenter
private val adapter: FilesAdapter =
FilesAdapter { fileViewModel -> presenter.openFile(fileViewModel) }
private val linearLayoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
private lateinit var chatRoomId: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_files)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
presenter.loadFiles(chatRoomId)
}
override fun showFiles(dataSet: List<FileViewModel>, total: Long) {
setupToolbar(total)
if (adapter.itemCount == 0) {
adapter.prependData(dataSet)
if (dataSet.size >= 30) {
recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView?
) {
presenter.loadFiles(chatRoomId)
}
})
}
} else {
adapter.appendData(dataSet)
}
}
override fun playMedia(url: String) {
ui {
activity?.let {
PlayerActivity.play(it, url)
}
}
}
override fun openImage(url: String, name: String) {
ui {
activity?.let {
ImageHelper.openImage(it, url, name)
}
}
}
override fun openDocument(uri: Uri) {
ui {
startActivity(Intent(Intent.ACTION_VIEW, uri))
}
}
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
override fun showLoading() {
ui { view_loading.isVisible = true }
}
override fun hideLoading() {
ui { view_loading.isVisible = false }
}
private fun setupRecyclerView() {
ui {
recycler_view.layoutManager = linearLayoutManager
recycler_view.addItemDecoration(DividerItemDecoration(it))
recycler_view.adapter = adapter
}
}
private fun setupToolbar(totalFiles: Long) {
(activity as ChatRoomActivity).let {
it.showToolbarTitle(getString(R.string.title_files_total, totalFiles))
it.hideToolbarChatRoomIcon()
}
}
}
\ No newline at end of file
package chat.rocket.android.files.viewmodel
import DateTimeHelper
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.fileUrl
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.GenericAttachment
class FileViewModel(
private val genericAttachment: GenericAttachment,
private val settings: Map<String, Value<Any>>,
private val tokenRepository: TokenRepository,
private val baseUrl: String
) {
val name: String?
val uploader: String?
val uploadDate: String?
val url: String?
val isMedia: Boolean
val isImage: Boolean
init {
name = getFileName()
uploader = getUserDisplayName()
uploadDate = getFileUploadDate()
url = getFileUrl()
isMedia = isFileMedia()
isImage = isFileImage()
}
private fun getFileName(): String? {
return genericAttachment.name
}
private fun getUserDisplayName(): String {
val username = "@${genericAttachment.user?.username}"
val realName = genericAttachment.user?.name
val uploaderName = if (settings.useRealName()) realName else username
return uploaderName ?: username
}
private fun getFileUploadDate(): String {
return DateTimeHelper.getDateTime(
DateTimeHelper.getLocalDateTime(genericAttachment.uploadedAt)
)
}
private fun getFileUrl(): String? {
val token = tokenRepository.get(baseUrl)
if (token != null) {
genericAttachment.path?.let {
return baseUrl.fileUrl(it, token)
}
}
return ""
}
private fun isFileMedia(): Boolean {
genericAttachment.type?.let {
return it.contains("audio") || it.contains("video")
}
return false
}
private fun isFileImage(): Boolean {
genericAttachment.type?.let {
return it.contains("image")
}
return false
}
}
\ No newline at end of file
package chat.rocket.android.files.viewmodel
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.baseUrl
import chat.rocket.core.model.Value
import chat.rocket.core.model.attachment.GenericAttachment
import javax.inject.Inject
class FileViewModelMapper @Inject constructor(
serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor,
private val tokenRepository: TokenRepository
) {
private var settings: Map<String, Value<Any>> =
getSettingsInteractor.get(serverInteractor.get()!!)
private val baseUrl = settings.baseUrl()
fun mapToViewModelList(fileList: List<GenericAttachment>): List<FileViewModel> {
return fileList.map { FileViewModel(it, settings, tokenRepository, baseUrl) }
}
}
\ No newline at end of file
package chat.rocket.android.helper
import android.Manifest
import android.app.Activity
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.media.MediaScannerConnection
import android.os.Environment
import android.support.design.widget.AppBarLayout
import android.support.v7.widget.Toolbar
import android.text.TextUtils
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.net.toUri
import androidx.core.view.setPadding
import chat.rocket.android.R
import com.facebook.binaryresource.FileBinaryResource
import com.facebook.cache.common.CacheKey
import com.facebook.imageformat.ImageFormatChecker
import com.facebook.imagepipeline.cache.DefaultCacheKeyFactory
import com.facebook.imagepipeline.core.ImagePipelineFactory
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.stfalcon.frescoimageviewer.ImageViewer
import timber.log.Timber
import java.io.File
object ImageHelper {
private var cacheKey: CacheKey? = null
// TODO - implement a proper image viewer with a proper Transition
// TODO - We should definitely write our own ImageViewer
fun openImage(context: Context, imageUrl: String, imageName: String) {
var imageViewer: ImageViewer? = null
val request =
ImageRequestBuilder.newBuilderWithSource(imageUrl.toUri())
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.DISK_CACHE)
.build()
cacheKey = DefaultCacheKeyFactory.getInstance()
.getEncodedCacheKey(request, null)
val pad = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_padding)
val lparams = AppBarLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val toolbar = Toolbar(context).also {
it.inflateMenu(R.menu.image_actions)
it.overflowIcon?.setTint(Color.WHITE)
it.setOnMenuItemClickListener {
return@setOnMenuItemClickListener when (it.itemId) {
R.id.action_save_image -> saveImage(context)
else -> true
}
}
val titleSize = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_title)
val titleTextView = TextView(context).also {
it.text = imageName
it.setTextColor(Color.WHITE)
it.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize.toFloat())
it.ellipsize = TextUtils.TruncateAt.END
it.setSingleLine()
it.typeface = Typeface.DEFAULT_BOLD
it.setPadding(pad)
}
val backArrowView = ImageView(context).also {
it.setImageResource(R.drawable.ic_arrow_back_white_24dp)
it.setOnClickListener { imageViewer?.onDismiss() }
it.setPadding(0, pad, pad, pad)
}
val layoutParams = AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.WRAP_CONTENT,
AppBarLayout.LayoutParams.WRAP_CONTENT
)
it.addView(backArrowView, layoutParams)
it.addView(titleTextView, layoutParams)
}
val appBarLayout = AppBarLayout(context).also {
it.layoutParams = lparams
it.setBackgroundColor(Color.BLACK)
it.addView(
toolbar, AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
)
}
val builder = ImageViewer.createPipelineDraweeControllerBuilder()
.setImageRequest(request)
.setAutoPlayAnimations(true)
imageViewer = ImageViewer.Builder(context, listOf(imageUrl))
.setOverlayView(appBarLayout)
.setStartPosition(0)
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
.show()
}
private fun saveImage(context: Context): Boolean {
if (!canWriteToExternalStorage(context)) {
checkWritingPermission(context)
return false
}
if (ImagePipelineFactory.getInstance().mainFileCache.hasKey(cacheKey)) {
val resource = ImagePipelineFactory.getInstance().mainFileCache.getResource(cacheKey)
val cachedFile = (resource as FileBinaryResource).file
val imageFormat = ImageFormatChecker.getImageFormat(resource.openStream())
val imageDir = "${Environment.DIRECTORY_PICTURES}/Rocket.Chat Images/"
val imagePath = Environment.getExternalStoragePublicDirectory(imageDir)
val imageFile =
File(imagePath, "${cachedFile.nameWithoutExtension}.${imageFormat.fileExtension}")
imagePath.mkdirs()
imageFile.createNewFile()
try {
cachedFile.copyTo(imageFile, true)
MediaScannerConnection.scanFile(
context,
arrayOf(imageFile.absolutePath),
null
) { path, uri ->
Timber.i("Scanned $path:")
Timber.i("-> uri=$uri")
}
} catch (ex: Exception) {
Timber.e(ex)
val message = context.getString(R.string.msg_image_saved_failed)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} finally {
val message = context.getString(R.string.msg_image_saved_successfully)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
return true
}
private fun canWriteToExternalStorage(context: Context): Boolean {
return AndroidPermissionsHelper.checkPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
private fun checkWritingPermission(context: Context) {
if (context is ContextThemeWrapper && context.baseContext is Activity) {
val activity = context.baseContext as Activity
AndroidPermissionsHelper.requestPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
AndroidPermissionsHelper.WRITE_EXTERNAL_STORAGE_CODE
)
}
}
}
\ No newline at end of file
package chat.rocket.android.helper
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.useRealName
import chat.rocket.common.model.RoomType
import chat.rocket.core.model.ChatRoom
import chat.rocket.core.model.Message
import javax.inject.Inject
class MessageHelper @Inject constructor(
getSettingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor
) {
private val currentServer = serverInteractor.get()!!
private val settings: PublicSettings = getSettingsInteractor.get(currentServer)
fun createPermalink(message: Message, chatRoom: ChatRoom): String {
val type = when (chatRoom.type) {
is RoomType.PrivateGroup -> "group"
is RoomType.Channel -> "channel"
is RoomType.DirectMessage -> "direct"
is RoomType.Livechat -> "livechat"
else -> "custom"
}
val name = if (settings.useRealName()) chatRoom.fullName ?: chatRoom.name else chatRoom.name
return "[ ]($currentServer/$type/$name?msg=${message.id}) "
}
fun messageIdFromPermalink(permalink: String): String? {
PERMALINK_REGEX.find(permalink.trim())?.let {
if (it.groupValues.size == 5) {
return it.groupValues[MESSAGE_ID]
}
}
return null
}
fun roomNameFromPermalink(permalink: String): String? {
PERMALINK_REGEX.find(permalink.trim())?.let {
if (it.groupValues.size == 5) {
return it.groupValues[ROOM_NAME]
}
}
return null
}
fun roomTypeFromPermalink(permalink: String): String? {
PERMALINK_REGEX.find(permalink.trim())?.let {
if (it.groupValues.size == 5) {
val type = it.groupValues[ROOM_TYPE]
return when(type) {
"group" -> "p"
"channel" -> "c"
"direct" -> "d"
"livechat" -> "l"
else -> type
}
}
}
return null
}
companion object {
private const val ROOM_TYPE = 2
private const val ROOM_NAME = 3
private const val MESSAGE_ID = 4
val PERMALINK_REGEX = "(?:__|[*#])|\\[(.+?)\\]\\(.+?//.+?/(.+)/(.+)\\?.*=(.*)\\)".toRegex()
}
}
\ No newline at end of file
......@@ -93,7 +93,7 @@ class MessageParser @Inject constructor(
private val othersTextColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme)
private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.white, context.theme)
private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.colorWhite, context.theme)
private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val mentionPadding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
......
......@@ -48,7 +48,7 @@ class UserHelper @Inject constructor(
/**
* Return the username for the current logged [User].
*/
fun username(): String? = user()?.username
fun username(): String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY, null)
/**
* Whether current [User] is admin on the current server.
......
package chat.rocket.android.main.presentation
import android.content.Context
import chat.rocket.android.R
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.chatroom.ui.chatRoomIntent
......@@ -37,9 +36,9 @@ class MainNavigator(internal val activity: MainActivity) {
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean,
isChatRoomOwner: Boolean) {
isChatRoomCreator: Boolean) {
activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomOwner))
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomCreator))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
......
......@@ -105,11 +105,12 @@ class MainPresenter @Inject constructor(
disconnect()
removeAccountInteractor.remove(currentServer)
tokenRepository.remove(currentServer)
view.disableAutoSignIn()
navigator.toNewServer()
} catch (ex: Exception) {
Timber.d(ex, "Error cleaning up the session...")
}
view.disableAutoSignIn()
navigator.toNewServer()
}
}
......
......@@ -24,4 +24,9 @@ interface MainView : MessageView, VersionCheckView {
fun setupNavHeader(viewModel: NavHeaderViewModel, accounts: List<Account>)
fun closeServerSelection()
/**
* callback to disable auto sign in for google smart lock when the user logs out
*/
fun disableAutoSignIn()
}
\ No newline at end of file
package chat.rocket.android.main.ui
import DrawableHelper
import android.app.Activity
import android.app.AlertDialog
import android.os.Bundle
......@@ -11,8 +12,8 @@ import android.view.MenuItem
import android.view.View
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.main.adapter.Selector
import chat.rocket.android.main.adapter.AccountsAdapter
import chat.rocket.android.main.adapter.Selector
import chat.rocket.android.main.presentation.MainPresenter
import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
......@@ -22,6 +23,8 @@ import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.showToast
import chat.rocket.common.model.UserStatus
import com.google.android.gms.auth.api.Auth
import com.google.android.gms.common.api.GoogleApiClient
import com.google.android.gms.gcm.GoogleCloudMessaging
import com.google.android.gms.iid.InstanceID
import dagger.android.AndroidInjection
......@@ -37,22 +40,32 @@ import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupportFragmentInjector {
@Inject lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject lateinit var presenter: MainPresenter
class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupportFragmentInjector,
GoogleApiClient.ConnectionCallbacks {
@Inject
lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var presenter: MainPresenter
private var isFragmentAdded: Boolean = false
private var expanded = false
private lateinit var googleApiClient: GoogleApiClient
private val headerLayout by lazy { view_navigation.getHeaderView(0) }
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
buildGoogleApiClient()
launch(CommonPool) {
try {
val token = InstanceID.getInstance(this@MainActivity).getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
val token = InstanceID.getInstance(this@MainActivity).getToken(
getString(R.string.gcm_sender_id),
GoogleCloudMessaging.INSTANCE_ID_SCOPE,
null
)
Timber.d("GCM token: $token")
presenter.refreshToken(token)
} catch (ex: Exception) {
......@@ -66,6 +79,39 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
setupNavigationView()
}
override fun onConnected(bundle: Bundle?) {
}
override fun onConnectionSuspended(errorCode: Int) {
}
private fun buildGoogleApiClient() {
googleApiClient = GoogleApiClient.Builder(this)
.enableAutoManage(this, {
Timber.d("ERROR: connection to client failed")
})
.addConnectionCallbacks(this)
.addApi(Auth.CREDENTIALS_API)
.build()
}
override fun onStart() {
super.onStart()
googleApiClient.let {
if (it.isConnected) {
Timber.d("Google api client connected successfully")
}
}
}
override fun disableAutoSignIn() {
googleApiClient.let {
if (it.isConnected) {
Auth.CredentialsApi.disableAutoSignIn(googleApiClient)
}
}
}
override fun onResume() {
super.onResume()
if (!isFragmentAdded) {
......@@ -119,19 +165,29 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
override fun alertNotRecommendedVersion() {
AlertDialog.Builder(this)
.setMessage(getString(R.string.msg_ver_not_recommended, BuildConfig.RECOMMENDED_SERVER_VERSION))
.setPositiveButton(R.string.msg_ok, null)
.create()
.show()
.setMessage(
getString(
R.string.msg_ver_not_recommended,
BuildConfig.RECOMMENDED_SERVER_VERSION
)
)
.setPositiveButton(R.string.msg_ok, null)
.create()
.show()
}
override fun blockAndAlertNotRequiredVersion() {
AlertDialog.Builder(this)
.setMessage(getString(R.string.msg_ver_not_minimum, BuildConfig.REQUIRED_SERVER_VERSION))
.setOnDismissListener { presenter.logout() }
.setPositiveButton(R.string.msg_ok, null)
.create()
.show()
.setMessage(
getString(
R.string.msg_ver_not_minimum,
BuildConfig.REQUIRED_SERVER_VERSION
)
)
.setOnDismissListener { presenter.logout() }
.setPositiveButton(R.string.msg_ok, null)
.create()
.show()
}
private fun setupAccountsList(header: View, accounts: List<Account>) {
......@@ -176,7 +232,8 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
override fun activityInjector(): AndroidInjector<Activity> = activityDispatchingAndroidInjector
override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector
override fun supportFragmentInjector(): AndroidInjector<Fragment> =
fragmentDispatchingAndroidInjector
private fun setupToolbar() {
setSupportActionBar(toolbar)
......
......@@ -3,39 +3,44 @@ package chat.rocket.android.members.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.members.viewmodel.MemberViewModel
import chat.rocket.android.members.viewmodel.MemberViewModelMapper
import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.getMembers
import timber.log.Timber
import javax.inject.Inject
class MembersPresenter @Inject constructor(
private val view: MembersView,
private val navigator: MembersNavigator,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory,
private val mapper: MemberViewModelMapper
private val roomsInteractor: ChatRoomsInteractor,
private val mapper: MemberViewModelMapper,
val serverInteractor: GetCurrentServerInteractor,
val factory: RocketChatClientFactory
) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl)
private var offset: Long = 0
fun loadChatRoomsMembers(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
fun loadChatRoomsMembers(roomId: String) {
launchUI(strategy) {
try {
view.showLoading()
val members = retryIO("getMembers($chatRoomId, $chatRoomType, $offset)") {
client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 60)
roomsInteractor.getById(serverUrl, roomId)?.let {
val members = client.getMembers(it.id, it.type, offset, 60)
val memberViewModels = mapper.mapToViewModelList(members.result)
view.showMembers(memberViewModels, members.total)
offset += 1 * 60L
}.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server")
}
val memberViewModels = mapper.mapToViewModelList(members.result)
view.showMembers(memberViewModels, members.total)
} catch (ex: RocketChatException) {
ex.message?.let {
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
......
......@@ -6,7 +6,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
......@@ -25,25 +24,23 @@ import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_members.*
import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment {
fun newInstance(chatRoomId: String): Fragment {
return MembersFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
}
}
}
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
class MembersFragment : Fragment(), MembersView {
@Inject lateinit var presenter: MembersPresenter
private val adapter: MembersAdapter = MembersAdapter { memberViewModel -> presenter.toMemberDetails(memberViewModel) }
@Inject
lateinit var presenter: MembersPresenter
private val adapter: MembersAdapter =
MembersAdapter { memberViewModel -> presenter.toMemberDetails(memberViewModel) }
private val linearLayoutManager = LinearLayoutManager(context)
private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -52,21 +49,21 @@ class MembersFragment : Fragment(), MembersView {
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_members)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_members)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as AppCompatActivity).supportActionBar?.title = ""
setupRecyclerView()
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType)
presenter.loadChatRoomsMembers(chatRoomId)
}
override fun showMembers(dataSet: List<MemberViewModel>, total: Long) {
......@@ -75,29 +72,23 @@ class MembersFragment : Fragment(), MembersView {
if (adapter.itemCount == 0) {
adapter.prependData(dataSet)
if (dataSet.size >= 59) { // TODO Check why the API retorns the specified count -1
recycler_view.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView) {
presenter.loadChatRoomsMembers(chatRoomId, chatRoomType, page * 60L)
recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView
) {
presenter.loadChatRoomsMembers(chatRoomId)
}
})
}
} else {
adapter.appendData(dataSet)
}
if (it is ChatRoomActivity) {
it.showRoomTypeIcon(false)
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
(activity as ChatRoomActivity).showRoomTypeIcon(true)
return super.onOptionsItemSelected(item)
}
return super.onOptionsItemSelected(item)
}
override fun showLoading() {
ui { view_loading.setVisible(true) }
}
......@@ -129,6 +120,9 @@ class MembersFragment : Fragment(), MembersView {
}
private fun setupToolbar(totalMembers: Long) {
(activity as ChatRoomActivity?)?.setupToolbarTitle(getString(R.string.title_members, totalMembers))
(activity as ChatRoomActivity).let {
it.showToolbarTitle(getString(R.string.title_members, totalMembers))
it.hideToolbarChatRoomIcon()
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.di
package chat.rocket.android.pinnedmessages.di
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentModule
import chat.rocket.android.pinnedmessages.ui.PinnedMessagesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
......
......@@ -2,7 +2,7 @@ package chat.rocket.android.pinnedmessages.presentation
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.ChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
......@@ -16,13 +16,14 @@ import javax.inject.Inject
class PinnedMessagesPresenter @Inject constructor(
private val view: PinnedMessagesView,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor,
private val roomsInteractor: ChatRoomsInteractor,
private val mapper: ViewModelMapper,
factory: RocketChatClientFactory
val serverInteractor: GetCurrentServerInteractor,
val factory: RocketChatClientFactory
) {
private val client = factory.create(serverInteractor.get()!!)
private var pinnedMessagesListOffset: Int = 0
private val serverUrl = serverInteractor.get()!!
private val client = factory.create(serverUrl)
private var offset: Int = 0
/**
* Load all pinned messages for the given room id.
......@@ -32,21 +33,20 @@ class PinnedMessagesPresenter @Inject constructor(
fun loadPinnedMessages(roomId: String) {
launchUI(strategy) {
try {
val serverUrl = serverInteractor.get()!!
val chatRoom = roomsInteractor.getById(serverUrl, roomId)
chatRoom?.let { room ->
view.showLoading()
val pinnedMessages =
client.getPinnedMessages(roomId, room.type, pinnedMessagesListOffset)
pinnedMessagesListOffset = pinnedMessages.offset.toInt()
val messageList = mapper.map(pinnedMessages.result.filterNot { it.isSystemMessage() })
view.showLoading()
roomsInteractor.getById(serverUrl, roomId)?.let {
val pinnedMessages = client.getPinnedMessages(roomId, it.type, offset)
val messageList =
mapper.map(pinnedMessages.result.filterNot { it.isSystemMessage() })
view.showPinnedMessages(messageList)
view.hideLoading()
offset += 1 * 30
}.ifNull {
Timber.e("Couldn't find a room with id: $roomId at current server.")
}
} catch (e: RocketChatException) {
Timber.e(e)
} catch (exception: RocketChatException) {
Timber.e(exception)
} finally {
view.hideLoading()
}
}
}
......
......@@ -8,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.ui.ChatRoomActivity
......@@ -16,29 +17,25 @@ import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.pinnedmessages.presentation.PinnedMessagesPresenter
import chat.rocket.android.pinnedmessages.presentation.PinnedMessagesView
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 dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_pinned_messages.*
import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String) : Fragment {
fun newInstance(chatRoomId: String): Fragment {
return PinnedMessagesFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
}
}
}
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
private const val BUNDLE_CHAT_ROOM_TYPE = "chat_room_type"
class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
private lateinit var chatRoomId: String
private lateinit var chatRoomType: String
private lateinit var adapter: ChatRoomAdapter
@Inject
lateinit var presenter: PinnedMessagesPresenter
......@@ -48,41 +45,49 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null){
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
}else{
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_pinned_messages)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_pinned_messages)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
presenter.loadPinnedMessages(chatRoomId)
}
override fun showPinnedMessages(pinnedMessages: List<BaseViewModel<*>>) {
ui {
if (recycler_view_pinned.adapter == null){
adapter = ChatRoomAdapter(chatRoomType,"",null,false)
if (recycler_view_pinned.adapter == null) {
adapter = ChatRoomAdapter(enableActions = false)
recycler_view_pinned.adapter = adapter
val linearLayoutManager = LinearLayoutManager(context)
recycler_view_pinned.layoutManager = linearLayoutManager
recycler_view_pinned.itemAnimator = DefaultItemAnimator()
if (pinnedMessages.size > 10){
recycler_view_pinned.addOnScrollListener(object : EndlessRecyclerViewScrollListener(linearLayoutManager){
override fun onLoadMore(page: Int, totalItemsCount: Int, recyclerView: RecyclerView) {
if (pinnedMessages.size > 10) {
recycler_view_pinned.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView
) {
presenter.loadPinnedMessages(chatRoomId)
}
})
}
togglePinView(pinnedMessages.size)
pin_view.isVisible = pinnedMessages.isEmpty()
}
adapter.appendData(pinnedMessages)
}
......@@ -103,22 +108,17 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showLoading() {
ui{ view_loading.setVisible(true) }
ui { view_loading.isVisible = true }
}
override fun hideLoading() {
ui { view_loading.setVisible(false) }
ui { view_loading.isVisible = false }
}
private fun setupToolbar() {
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_pinned_messages))
}
private fun togglePinView(size : Int){
if (size == 0){
pin_view.setVisible(true)
}else{
pin_view.setVisible(false)
(activity as ChatRoomActivity).let {
it.showToolbarTitle(getString(R.string.title_pinned_messages))
it.hideToolbarChatRoomIcon()
}
}
}
\ No newline at end of file
......@@ -5,7 +5,7 @@ import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
class ChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
/**
* Get all [ChatRoom].
......@@ -41,8 +41,7 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo
* @return The [ChatRoom] object or null if we couldn't find any.
*/
suspend fun getById(serverUrl: String, roomId: String): ChatRoom? = withContext(CommonPool) {
val allChatRooms = repository.get(serverUrl)
return@withContext allChatRooms.first {
return@withContext repository.get(serverUrl).find {
it.id == roomId
}
}
......@@ -55,7 +54,7 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo
* @return The [ChatRoom] object or null if we couldn't find any.
*/
fun getByName(serverUrl: String, name: String): ChatRoom? {
return getAll(serverUrl).toMutableList().find { chatRoom -> chatRoom.name == name }
return getAll(serverUrl).firstOrNull { it.name == name || it.fullName == name }
}
/**
......
......@@ -103,5 +103,5 @@ fun PublicSettings.uploadMaxFileSize(): Int {
return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE
}
fun PublicSettings.baseUrl(): String? = this[SITE_URL]?.value as String?
fun PublicSettings.baseUrl(): String = this[SITE_URL]?.value as String
fun PublicSettings.siteName(): String? = this[SITE_NAME]?.value as String?
\ No newline at end of file
......@@ -2,6 +2,7 @@ package chat.rocket.android.util.extensions
import android.graphics.Color
import android.util.Patterns
import chat.rocket.common.model.Token
import timber.log.Timber
fun String.removeTrailingSlash(): String {
......@@ -17,7 +18,11 @@ fun String.sanitize(): String {
return tmp.removeTrailingSlash()
}
fun String.avatarUrl(avatar: String, isGroupOrChannel: Boolean = false, format: String = "jpeg"): String {
fun String.avatarUrl(
avatar: String,
isGroupOrChannel: Boolean = false,
format: String = "jpeg"
): String {
return if (isGroupOrChannel) {
"${removeTrailingSlash()}/avatar/%23${avatar.removeTrailingSlash()}?format=$format"
} else {
......@@ -25,6 +30,14 @@ fun String.avatarUrl(avatar: String, isGroupOrChannel: Boolean = false, format:
}
}
fun String.fileUrl(path: String, token: Token): String {
return (this + path + "?rc_uid=${token.userId}" + "&rc_token=${token.authToken}").safeUrl()
}
fun String.safeUrl(): String {
return this.replace(" ", "%20").replace("\\", "")
}
fun String.serverLogoUrl(favicon: String) = "${removeTrailingSlash()}/$favicon"
fun String.casUrl(serverUrl: String, token: String) =
......
......@@ -40,7 +40,6 @@ class OauthWebViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
......@@ -81,7 +80,8 @@ class OauthWebViewActivity : AppCompatActivity() {
domStorageEnabled = true
// TODO Remove this workaround that is required to make Google OAuth to work. We should use Custom Tabs instead. See https://github.com/RocketChat/Rocket.Chat.Android/issues/968
if (webPageUrl.contains("google")) {
userAgentString = "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/43.0.2357.65 Mobile Safari/535.19"
userAgentString =
"Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/43.0.2357.65 Mobile Safari/535.19"
}
}
web_view.webViewClient = object : WebViewClient() {
......@@ -114,8 +114,18 @@ class OauthWebViewActivity : AppCompatActivity() {
private fun getCredentialSecret(json: JSONObject): String =
json.optString(JSON_CREDENTIAL_SECRET)
private fun closeView(activityResult: Int = Activity.RESULT_CANCELED, credentialToken: String? = null, credentialSecret: String? = null) {
setResult(activityResult, Intent().putExtra(INTENT_OAUTH_CREDENTIAL_TOKEN, credentialToken).putExtra(INTENT_OAUTH_CREDENTIAL_SECRET, credentialSecret))
private fun closeView(
activityResult: Int = Activity.RESULT_CANCELED,
credentialToken: String? = null,
credentialSecret: String? = null
) {
setResult(
activityResult,
Intent().putExtra(INTENT_OAUTH_CREDENTIAL_TOKEN, credentialToken).putExtra(
INTENT_OAUTH_CREDENTIAL_SECRET,
credentialSecret
)
)
finish()
overridePendingTransition(R.anim.hold, R.anim.slide_down)
}
......
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="13dp"
android:height="16dp"
android:viewportWidth="13.0"
android:viewportHeight="16.0">
<path
android:pathData="M6.626,11.495L4.505,11.495L3.714,16L1.703,16L2.495,11.495L0,11.495L0,9.604L2.824,9.604L3.374,6.484L0.824,6.484L0.824,4.571L3.714,4.571L4.516,0L6.516,0L5.714,4.571L7.846,4.571L8.648,0L10.659,0L9.857,4.571L12.264,4.571L12.264,6.484L9.516,6.484L8.967,9.604L11.429,9.604L11.429,11.495L8.637,11.495L7.846,16L5.835,16L6.626,11.495ZM4.835,9.604L6.956,9.604L7.505,6.484L5.374,6.484L4.835,9.604Z"
android:strokeColor="#00000000"
android:fillType="evenOdd"
android:fillColor="#000000"
android:strokeWidth="1"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:fillColor="#FF000000"
android:pathData="M2.4,0h1.2v12h-1.2z" />
<path
android:fillColor="#FF000000"
android:pathData="M0,2.4h12v1.2h-12z" />
<path
android:fillColor="#FF000000"
android:pathData="M0,8.4h12v1.2h-12z" />
<path
android:fillColor="#FF000000"
android:pathData="M8.4,0h1.2v12h-1.2z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M18,10v-4c0,-3.313 -2.687,-6 -6,-6s-6,2.687 -6,6v4h-3v14h18v-14h-3zM8,6c0,-2.206 1.794,-4 4,-4s4,1.794 4,4v4h-8v-4zM19,22h-14v-10h14v10z" />
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:pathData="M1.5,5.5h9v6h-9z"
android:strokeColor="#FF000000"
android:strokeWidth="1" />
<path
android:pathData="M2.5,5.5L9.5,5.5L9.5,4C9.5,2.067 7.933,0.5 6,0.5C4.067,0.5 2.5,2.067 2.5,4L2.5,5.5Z"
android:strokeColor="#FF000000"
android:strokeWidth="1" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M22.548,9l0.452,-2h-5.364l1.364,-6h-2l-1.364,6h-5l1.364,-6h-2l-1.364,6h-6.184l-0.452,2h6.182l-1.364,6h-5.36l-0.458,2h5.364l-1.364,6h2l1.364,-6h5l-1.364,6h2l1.364,-6h6.185l0.451,-2h-6.182l1.364,-6h5.366zM13.818,15h-5l1.364,-6h5l-1.364,6z" />
</vector>
android:pathData="M8,5v14l11,-7z" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportHeight="20.0"
android:viewportWidth="20.0">
<path
android:fillColor="#FF000000"
android:pathData="M13.6,13.47c-0.91,0.953 -2.191,1.545 -3.61,1.545 -2.756,0 -4.99,-2.234 -4.99,-4.99 0,-0.009 0,-0.018 0,-0.026v0.001c0,-2.761 2.239,-5 5,-5 1.131,0 2.175,0.376 3.013,1.009l-0.013,-0.009v-1h2v6.5c0,0.828 0.672,1.5 1.5,1.5s1.5,-0.672 1.5,-1.5v0,-1.5c-0.003,-4.416 -3.584,-7.994 -8,-7.994 -4.418,0 -8,3.582 -8,8s3.582,8 8,8c1.305,0 2.537,-0.312 3.625,-0.867l-0.045,0.021 0.9,1.79c-1.305,0.668 -2.847,1.06 -4.48,1.06 -5.523,0 -10,-4.477 -10,-10s4.477,-10 10,-10c5.519,0 9.994,4.472 10,9.99v0.001h-0.01v1.5c0,0.003 0,0.007 0,0.01 0,1.933 -1.567,3.5 -3.5,3.5 -1.202,0 -2.262,-0.606 -2.892,-1.528l-0.008,-0.012zM10,13c1.657,0 3,-1.343 3,-3s-1.343,-3 -3,-3v0c-1.657,0 -3,1.343 -3,3s1.343,3 3,3v0z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="4dp" />
<stroke android:color="#1D74F5" android:width="2dp" />
</shape>
\ No newline at end of file
......@@ -42,7 +42,7 @@
android:layout_height="match_parent"
android:layout_marginTop="@dimen/nav_header_height"
android:alpha="0"
android:background="@color/white"
android:background="@color/colorWhite"
android:elevation="20dp"
android:visibility="gone" />
</FrameLayout>
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="@style/AppTheme"
tools:context=".chatroom.ui.PinnedMessagesActivity">
<include
android:id="@+id/layout_app_bar"
layout="@layout/app_bar_chat_room" />
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/layout_app_bar" />
</RelativeLayout>
\ No newline at end of file
......@@ -21,7 +21,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:indicatorColor="@color/black"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" />
</RelativeLayout>
\ No newline at end of file
......@@ -16,35 +16,17 @@
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbar_content_container"
<TextView
android:id="@+id/text_room_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- TODO implement -->
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_room_avatar"
android:layout_width="30dp"
android:layout_height="30dp"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:roundAsCircle="true" />
<TextView
android:id="@+id/text_room_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:drawablePadding="@dimen/text_view_drawable_padding"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Developers" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:drawablePadding="@dimen/text_view_drawable_padding"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/colorWhite"
android:textSize="18sp"
android:textStyle="bold"
tools:text="general" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
\ No newline at end of file
......@@ -25,7 +25,7 @@
android:layout_alignParentStart="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/white"
android:textColor="@color/colorWhite"
android:textSize="18sp"
android:textStyle="bold"
tools:text="@string/title_password" />
......
......@@ -4,7 +4,7 @@
android:id="@+id/emoji_keyboard_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@color/white">
android:background="@color/colorWhite">
<View
android:id="@+id/divider"
......
......@@ -4,7 +4,7 @@
android:id="@+id/picker_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:background="@color/colorWhite"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
......@@ -22,6 +22,6 @@
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:background="@color/white" />
android:background="@color/colorWhite" />
</LinearLayout>
\ No newline at end of file
......@@ -214,7 +214,7 @@
android:layout_height="wrap_content"
android:src="@drawable/ic_expand_more_black_24dp"
android:theme="@style/Theme.AppCompat"
android:tint="@color/white"
android:tint="@color/colorWhite"
android:visibility="gone"
app:backgroundTint="@color/colorAccent"
app:elevation="@dimen/fab_elevation"
......
......@@ -12,7 +12,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:indicatorColor="@color/black"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
......@@ -136,7 +136,7 @@
android:elevation="4dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/white"
android:textColor="@color/colorWhite"
android:visibility="gone"
tools:alpha="1"
tools:text="connected"
......
......@@ -17,7 +17,7 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"
app:indicatorColor="@color/black"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" />
<TextView
......@@ -39,7 +39,7 @@
android:elevation="4dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/white"
android:textColor="@color/colorWhite"
android:visibility="gone"
tools:alpha="1"
tools:text="connected"
......
<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".favoritemessages.ui.FavoriteMessagesFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView
android:id="@+id/image_star"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_action_message_star_24dp"
android:tint="@color/icon_grey"
app:layout_constraintBottom_toTopOf="@+id/text_no_favorite_messages"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/text_no_favorite_messages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/no_favorite_messages"
android:textColor="@color/colorSecondaryText"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/text_no_favorite_messages_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image_star" />
<TextView
android:id="@+id/text_no_favorite_messages_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/no_favorite_description"
android:textAlignment="center"
android:textColor="@color/colorSecondaryTextLight"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_no_favorite_messages" />
<android.support.constraint.Group
android:id="@+id/no_messages_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="text_no_favorite_messages_description,image_star,text_no_favorite_messages"
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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".files.ui.FilesFragment">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="@+id/recycler_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/image_file"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_insert_drive_file_black_24dp"
android:tint="@color/icon_grey"
app:layout_constraintBottom_toTopOf="@+id/text_no_file"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/text_no_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/msg_no_files"
android:textColor="@color/colorSecondaryText"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/text_all_files_appear_here"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image_file" />
<TextView
android:id="@+id/text_all_files_appear_here"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/msg_all_files_appear_here"
android:textColor="@color/colorSecondaryTextLight"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_no_file" />
<android.support.constraint.Group
android:id="@+id/group_no_file"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="image_file,text_no_file,text_all_files_appear_here"
tools:visibility="visible" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -30,7 +30,7 @@
style="@style/TextAppearance.AppCompat.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textColor="@color/colorWhite"
tools:text="Ronald Perkins" />
<TextView
......@@ -39,7 +39,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="@color/white"
android:textColor="@color/colorWhite"
tools:text="\@ronaldPerkins" />
</LinearLayout>
......
......@@ -18,7 +18,7 @@
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center"
app:indicatorColor="@color/black"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
......@@ -48,7 +48,7 @@
android:layout_height="wrap_content"
tools:visibility="visible"
android:visibility="gone"
app:indicatorColor="@color/black"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
......
......@@ -3,18 +3,14 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:context=".pinnedmessages.ui.PinnedMessagesFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_pinned"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
android:scrollbars="vertical" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
......@@ -24,7 +20,7 @@
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:visibility="gone"
app:indicatorColor="@color/black"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator"
app:layout_constraintBottom_toBottomOf="@+id/recycler_view_pinned"
app:layout_constraintEnd_toEndOf="parent"
......@@ -38,46 +34,46 @@
android:layout_height="100dp"
android:src="@drawable/ic_action_message_pin_24dp"
android:tint="@color/icon_grey"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_pin_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_pin_title"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/tv_pin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
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:textColor="@color/colorSecondaryText"
android:textSize="20sp"
android:layout_marginTop="24dp"
android:textStyle="bold"
android:textColor="@color/colorSecondaryText"/>
app:layout_constraintBottom_toTopOf="@id/tv_pin_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_pin_icon" />
<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:text="@string/no_pinned_description"
android:textAlignment="center"
android:textColor="@color/colorSecondaryTextLight"
android:textSize="16sp"
android:textColor="@color/colorSecondaryTextLight"/>
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_pin_title" />
<androidx.constraintlayout.widget.Group
android:id="@+id/pin_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="tv_pin_description,iv_pin_icon,tv_pin_title"
android:visibility="gone"
app:constraint_referenced_ids="tv_pin_description,iv_pin_icon,tv_pin_title"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
......@@ -69,7 +69,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:indicatorColor="@color/black"
app:indicatorColor="@color/colorBlack"
app:indicatorName="BallPulseIndicator" />
</RelativeLayout>
\ No newline at end of file
......@@ -29,7 +29,6 @@
app:layout_constraintTop_toTopOf="@+id/text_chat_name"
tools:src="@drawable/ic_hashtag_unread_12dp" />
<TextView
android:id="@+id/text_last_message"
android:layout_width="0dp"
......@@ -53,7 +52,7 @@
style="@style/ChatRoom.Name.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginStart="@dimen/text_view_drawable_padding"
android:ellipsize="end"
android:lines="1"
android:maxLines="1"
......
......@@ -25,6 +25,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:textDirection="locale"
tools:text="This is a very, very, very long filename, to test how the layout will work on very very very long filenames.pdf" />
<include
......
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingBottom="@dimen/member_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/member_item_top_and_bottom_padding">
<LinearLayout
android:id="@+id/image_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_file_thumbnail"
android:layout_width="80dp"
android:layout_height="70dp"
android:visibility="gone"
app:roundedCornerRadius="3dp" />
<ImageView
android:id="@+id/image_file_media_thumbnail"
android:layout_width="80dp"
android:layout_height="70dp"
android:visibility="gone" />
</LinearLayout>
<TextView
android:id="@+id/text_file_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/colorPrimaryText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_container"
app:layout_constraintTop_toTopOf="@+id/image_container"
tools:text="File.mp3" />
<TextView
android:id="@+id/text_uploader"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/colorSecondaryText"
app:layout_constraintBottom_toTopOf="@+id/text_upload_date"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_container"
tools:text="\@filipe.brito" />
<TextView
android:id="@+id/text_upload_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textColor="@color/colorSecondaryTextLight"
app:layout_constraintBottom_toBottomOf="@+id/image_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_container"
tools:text="Ma 22, 2018 6:42 PM" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -13,21 +13,13 @@
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding">
<include
android:id="@+id/layout_avatar"
layout="@layout/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif" />
<LinearLayout
android:id="@+id/new_messages_notif"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
......@@ -38,13 +30,13 @@
android:layout_gravity="center"
android:layout_marginEnd="4dp"
android:layout_weight="1"
android:background="@color/red" />
android:background="@color/colorRed" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/msg_unread_messages"
android:textColor="@color/red" />
android:textColor="@color/colorRed" />
<View
android:layout_width="0dp"
......@@ -52,16 +44,25 @@
android:layout_gravity="center"
android:layout_marginStart="4dp"
android:layout_weight="1"
android:background="@color/red" />
android:background="@color/colorRed" />
</LinearLayout>
<include
android:id="@+id/layout_avatar"
layout="@layout/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="5dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif" />
<LinearLayout
android:id="@+id/top_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="horizontal"
app:layout_constraintLeft_toRightOf="@+id/layout_avatar"
app:layout_constraintStart_toEndOf="@+id/layout_avatar"
app:layout_constraintTop_toBottomOf="@+id/new_messages_notif">
<TextView
......@@ -108,9 +109,10 @@
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginTop="5dp"
app:layout_constraintLeft_toLeftOf="@+id/top_container"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="@+id/top_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_container"
android:textDirection="locale"
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!" />
<include
......
......@@ -23,31 +23,27 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/top_container"
android:layout_width="0dp"
<TextView
android:id="@+id/text_sender"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="horizontal"
app:layout_constraintLeft_toRightOf="@+id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/new_messages_notif">
<TextView
android:id="@+id/text_sender"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorPrimary"
tools:text="Ronald Perkins" />
android:textColor="@color/colorPrimary"
tools:text="Ronald Perkins"
app:layout_constraintStart_toEndOf="@+id/quote_bar"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="8dp" />
<TextView
android:id="@+id/text_message_time"
style="@style/Timestamp.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
tools:text="11:45 PM" />
</LinearLayout>
<TextView
android:id="@+id/text_message_time"
style="@style/Timestamp.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
tools:text="11:45 PM"
app:layout_constraintStart_toEndOf="@+id/text_sender"
app:layout_constraintTop_toTopOf="@+id/text_sender"
app:layout_constraintBottom_toBottomOf="@+id/text_sender"/>
<TextView
android:id="@+id/text_content"
......@@ -57,8 +53,8 @@
android:ellipsize="end"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/top_container"
app:layout_constraintTop_toBottomOf="@+id/top_container"
app:layout_constraintStart_toStartOf="@+id/text_sender"
app:layout_constraintTop_toBottomOf="@+id/text_sender"
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!" />
<include
......
<?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"
android:id="@+id/message_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">
<Button
android:id="@+id/button_message_reply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:layout_marginTop="5dp"
android:layout_marginStart="56dp"
android:background="@drawable/message_reply_button_bg"
android:text="@string/action_msg_reply"
android:textAllCaps="false"
android:textColor="#1D74F5"
android:textSize="14sp"
app:layout_constraintLeft_toLeftOf="parent" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@+id/button_message_reply"
app:layout_constraintStart_toStartOf="@+id/button_message_reply"
app:layout_constraintTop_toBottomOf="@+id/button_message_reply" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -26,7 +26,7 @@
android:layout_marginStart="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/white"
android:textColor="@color/colorWhite"
android:textSize="14sp"
android:typeface="normal"
app:layout_constraintBottom_toBottomOf="parent"
......
......@@ -22,7 +22,7 @@
android:id="@+id/audio_video_attachment"
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="@color/black"
android:background="@color/colorBlack"
android:visibility="gone"
tools:visibility="visible">
......
......@@ -21,10 +21,10 @@
android:id="@+id/text_room_is_read_only"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@color/white"
android:background="@color/colorWhite"
android:gravity="center"
android:text="@string/msg_this_room_is_read_only"
android:textColor="@color/black"
android:textColor="@color/colorBlack"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@+id/divider" />
......@@ -32,7 +32,7 @@
android:id="@+id/button_join_chat"
android:layout_width="match_parent"
android:layout_height="45dp"
android:background="@color/white"
android:background="@color/colorWhite"
android:text="@string/action_join_chat"
android:textColor="@color/colorAccent"
android:visibility="gone"
......
......@@ -19,7 +19,7 @@
android:theme="@style/Theme.AppCompat"
android:tint="@color/gray_material"
android:visibility="invisible"
app:backgroundTint="@color/white"
app:backgroundTint="@color/colorWhite"
app:fabSize="mini"
app:layout_anchor="@id/recycler_view"
app:layout_anchorGravity="bottom|end" />
......
......@@ -13,6 +13,8 @@
android:id="@+id/image_preview"
android:layout_width="70dp"
android:layout_height="50dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:actualImageScaleType="centerCrop" />
<TextView
......@@ -23,6 +25,7 @@
android:textColor="@color/colorSecondaryText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_preview"
android:textDirection="locale"
tools:text="www.uol.com.br" />
<TextView
......@@ -33,6 +36,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_host"
android:textDirection="locale"
tools:text="Web page title" />
<TextView
......@@ -42,6 +46,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/text_host"
app:layout_constraintTop_toBottomOf="@id/text_title"
android:textDirection="locale"
tools:text="description" />
<include
......
......@@ -54,7 +54,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:textColor="@color/white"
android:textColor="@color/colorWhite"
app:layout_constraintBottom_toBottomOf="@+id/image_user_status"
app:layout_constraintEnd_toStartOf="@+id/image_account_expand"
app:layout_constraintStart_toEndOf="@+id/image_user_status"
......@@ -69,7 +69,7 @@
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@color/white"
android:textColor="@color/colorWhite"
app:layout_constraintEnd_toStartOf="@+id/image_account_expand"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_user_name"
......@@ -80,7 +80,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_expand_more_24dp"
android:tint="@color/white"
android:tint="@color/colorWhite"
app:layout_constraintBottom_toBottomOf="@+id/text_server_url"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment