Unverified Commit 18f2ed9a authored by Filipe de Lima Brito's avatar Filipe de Lima Brito Committed by GitHub

Merge pull request #2168 from RocketChat/beta

[RELEASE] Merge BETA into MASTER 
parents b342f3cd afe67756
......@@ -43,8 +43,8 @@ cd Rocket.Chat.Android/app
## Bug report & Feature request
Are you having a technical issue trying to compile the app, or setting up Push Notifications? Please use our Community Support channel for that: https://forums.rocket.chat/c/community-support. The issues are only suppose to be used for bugs, improvements and features in the native Android application.
Are you having a technical issue trying to compile the app, or setting up Push Notifications? Please use our Community Support channel for that: https://forums.rocket.chat/c/community-support. The issues are only supposed to be used for bugs, improvements, and features in the native Android application.
## Coding Style
Please follow the official [Kotlin coding convections](https://kotlinlang.org/docs/reference/coding-conventions.html) when contributing.
Please follow the official [Kotlin coding conventions](https://kotlinlang.org/docs/reference/coding-conventions.html) when contributing.
......@@ -2,7 +2,9 @@ def taskRequests = getGradle().getStartParameter().getTaskRequests().toString()
def isPlay = !(taskRequests.contains("Foss") || taskRequests.contains("foss"))
apply plugin: 'com.android.application'
if (isPlay) { apply plugin: 'io.fabric' }
if (isPlay) {
apply plugin: 'io.fabric'
}
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
......@@ -16,13 +18,12 @@ android {
applicationId "chat.rocket.android"
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
versionCode 2057
versionName "3.2.0"
versionCode 2059
versionName "3.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
def gitSha = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim()
def buildTime = new GregorianCalendar().format("MM-dd-yyyy' 'h:mm:ss a z")
buildConfigField "String", "GIT_SHA", "\"${gitSha}\""
javaCompileOptions {
......@@ -30,6 +31,17 @@ android {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
// For Jitsi
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// For Jitsi
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
signingConfigs {
......@@ -73,7 +85,7 @@ android {
dimension "type"
}
// only foss
// only FOSS
foss {
dimension "type"
}
......@@ -83,6 +95,10 @@ android {
exclude 'META-INF/core.kotlin_module'
exclude 'META-INF/main.kotlin_module'
}
lintOptions {
lintConfig file("src/main/res/xml/lint.xml")
}
}
dependencies {
......@@ -96,7 +112,7 @@ dependencies {
implementation project(':suggestions')
implementation libraries.kotlin
implementation libraries.coroutines
implementation libraries.coroutinesCore
implementation libraries.coroutinesAndroid
implementation libraries.appCompat
......@@ -123,6 +139,8 @@ dependencies {
implementation libraries.viewmodelKtx
implementation libraries.workmanager
implementation libraries.livedataKtx
implementation libraries.rxKotlin
implementation libraries.rxAndroid
......@@ -133,25 +151,25 @@ dependencies {
implementation libraries.timber
implementation libraries.threeTenABP
kapt libraries.kotshiCompiler
implementation libraries.kotshiApi
implementation libraries.fresco
api libraries.frescoOkHttp
implementation libraries.frescoAnimatedGif
implementation libraries.frescoWebP
implementation libraries.frescoAnimatedWebP
implementation libraries.glide
implementation libraries.glideTransformations
kapt libraries.kotshiCompiler
implementation libraries.kotshiApi
implementation libraries.frescoImageViewer
implementation libraries.markwon
implementation libraries.aVLoadingIndicatorView
implementation libraries.livedataKtx
implementation libraries.glide
implementation libraries.glideTransformations
implementation(libraries.jitsi) { transitive = true }
implementation 'com.google.code.findbugs:jsr305:3.0.2'
......@@ -159,8 +177,8 @@ dependencies {
playImplementation libraries.fcm
playImplementation libraries.firebaseAnalytics
playImplementation libraries.playServicesAuth
playImplementation('com.crashlytics.sdk.android:crashlytics:2.9.5@aar') { transitive = true }
playImplementation('com.crashlytics.sdk.android:answers:1.4.3@aar') { transitive = true }
playImplementation('com.crashlytics.sdk.android:crashlytics:2.9.8@aar') { transitive = true }
playImplementation('com.crashlytics.sdk.android:answers:1.4.6@aar') { transitive = true }
testImplementation libraries.junit
testImplementation libraries.truth
......@@ -168,12 +186,6 @@ dependencies {
androidTestImplementation libraries.espressoIntents
}
kotlin {
experimental {
coroutines "enable"
}
}
androidExtensions {
experimental = true
}
......@@ -181,8 +193,8 @@ androidExtensions {
// FIXME - build and install the sdk into the app/libs directory
// We were having some issues with the kapt generated files from the sdk when importing as a module
def sdk_location=project.properties['sdk_location'] ?: ""
task compileSdk(type:Exec) {
def sdk_location = project.properties['sdk_location'] ?: ""
task compileSdk(type: Exec) {
if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
commandLine 'cmd', '/c', 'build-sdk.sh', sdk_location
} else {
......
......@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:name=".app.RocketChatApplication"
......@@ -73,6 +75,11 @@
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".videoconference.ui.VideoConferenceActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".chatroom.ui.ChatRoomActivity"
android:theme="@style/AppTheme"
......
......@@ -34,12 +34,16 @@ class AboutFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
setupViews()
analyticsManager.logScreenView(ScreenViewEvent.About)
}
override fun onResume() {
super.onResume()
setupToolbar()
}
private fun setupViews() {
text_version_name.text = BuildConfig.VERSION_NAME
text_build_number.text = getString(
......
......@@ -71,4 +71,67 @@ interface Analytics {
* @param resetPasswordSucceeded True if successful reset password, false otherwise.
*/
fun logResetPassword(resetPasswordSucceeded: Boolean) {}
/**
* Logs the video conference event.
*
* @param event The [SubscriptionTypeEvent] to log.
* @param serverUrl The server URL to log.
*/
fun logVideoConference(event: SubscriptionTypeEvent, serverUrl: String) {}
/**
* Logs the add reaction message action.
*/
fun logMessageActionAddReaction() {}
/**
* Logs the replay message action.
*/
fun logMessageActionReply() {}
/**
* Logs the quote message action.
*/
fun logMessageActionQuote() {}
/**
* Logs the permalink message action.
*/
fun logMessageActionPermalink() {}
/**
* Logs the copy message action.
*/
fun logMessageActionCopy() {}
/**
* Logs the edit message action.
*/
fun logMessageActionEdit() {}
/**
* Logs the info message action.
*/
fun logMessageActionInfo() {}
/**
* Logs the star message action.
*/
fun logMessageActionStar() {}
/**
* Logs the pin message action.
*/
fun logMessageActionPin() {}
/**
* Logs the report message action.
*/
fun logMessageActionReport() {}
/**
* Logs the delete message action.
*/
fun logMessageActionDelete() {}
}
......@@ -76,4 +76,76 @@ class AnalyticsManager @Inject constructor(
analytics.forEach { it.logResetPassword(resetPasswordSucceeded) }
}
}
fun logVideoConference(event: SubscriptionTypeEvent) {
if (analyticsTrackingInteractor.get() && serverUrl != null) {
analytics.forEach { it.logVideoConference(event, serverUrl) }
}
}
fun logMessageActionAddReaction() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionAddReaction() }
}
}
fun logMessageActionReply() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionReply() }
}
}
fun logMessageActionQuote() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionQuote() }
}
}
fun logMessageActionPermalink() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionPermalink() }
}
}
fun logMessageActionCopy() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionCopy() }
}
}
fun logMessageActionEdit() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionEdit() }
}
}
fun logMessageActionInfo() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionInfo() }
}
}
fun logMessageActionStar() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionStar() }
}
}
fun logMessageActionPin() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionPin() }
}
}
fun logMessageActionReport() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionReport() }
}
}
fun logMessageActionDelete() {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMessageActionDelete() }
}
}
}
......@@ -3,15 +3,11 @@ package chat.rocket.android.app
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.UserStatus
import chat.rocket.core.internal.realtime.setTemporaryStatus
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class AppLifecycleObserver @Inject constructor(
......@@ -33,7 +29,7 @@ class AppLifecycleObserver @Inject constructor(
}
private fun changeTemporaryStatus(userStatus: UserStatus) {
launch {
GlobalScope.launch {
serverInteractor.get()?.let { currentServer ->
factory.create(currentServer).setTemporaryStatus(userStatus)
}
......
import android.content.Context
import chat.rocket.android.R
import org.threeten.bp.*
import org.threeten.bp.Instant
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalTime
import org.threeten.bp.Period
import org.threeten.bp.ZoneId
import org.threeten.bp.format.DateTimeFormatter
import org.threeten.bp.format.FormatStyle
import org.threeten.bp.format.TextStyle
......@@ -53,39 +58,39 @@ object DateTimeHelper {
}
}
/**
* Returns a time from a [LocalDateTime].
*
* @param localDateTime The [LocalDateTime].
* @return The time from a [LocalDateTime].
*/
fun getTime(localDateTime: LocalDateTime): String {
val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return localDateTime.toLocalTime().format(formatter).toString()
}
/**
* Returns a time from a [LocalDateTime].
*
* @param localDateTime The [LocalDateTime].
* @return The time from a [LocalDateTime].
*/
fun getTime(localDateTime: LocalDateTime): String {
val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return localDateTime.toLocalTime().format(formatter).toString()
}
/**
* Returns a date time from a [LocalDateTime].
*
* @param localDateTime The [LocalDateTime].
* @return The time from a [LocalDateTime].
*/
fun getDateTime(localDateTime: LocalDateTime): String {
return formatLocalDateTime(localDateTime)
}
/**
* Returns a date time from a [LocalDateTime].
*
* @param localDateTime The [LocalDateTime].
* @return The time from a [LocalDateTime].
*/
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 formatLocalDateTime(localDateTime: LocalDateTime): String {
val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)
return localDateTime.format(formatter).toString()
}
private fun formatLocalDate(localDate: LocalDate): String {
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
return localDate.format(formatter).toString()
}
private fun formatLocalDate(localDate: LocalDate): String {
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
return localDate.format(formatter).toString()
}
private fun formatLocalTime(localTime: LocalTime): String {
val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return localTime.format(formatter).toString()
}
private fun formatLocalTime(localTime: LocalTime): String {
val formatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return localTime.format(formatter).toString()
}
}
\ No newline at end of file
......@@ -91,7 +91,7 @@ object DrawableHelper {
}
/**
* Compounds a Drawable (to appear to the left of the text) into a TextView.
* Compounds a Drawable (to appear on the left side of a text) into a TextView.
*
* @param textView The TextView.
* @param drawable The Drawable.
......@@ -100,6 +100,16 @@ object DrawableHelper {
fun compoundDrawable(textView: TextView, drawable: Drawable) =
textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
/**
* Compounds a Drawable (to appear on the right side of a text) into a TextView.
*
* @param textView The TextView.
* @param drawable The Drawable.
* @see compoundDrawable
*/
fun compoundRightDrawable(textView: TextView, drawable: Drawable) =
textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
/**
* Returns the user status drawable.
*
......
......@@ -37,7 +37,8 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.HasBroadcastReceiverInjector
import dagger.android.HasServiceInjector
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject
......@@ -174,7 +175,7 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
EmojiRepository.init(this)
val currentServer = getCurrentServerInteractor.get()
currentServer?.let { server ->
launch {
GlobalScope.launch {
val client = factory.create(server)
EmojiRepository.setCurrentServerUrl(server)
val customEmojiList = mutableListOf<Emoji>()
......
......@@ -7,7 +7,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class AuthenticationModule {
......
......@@ -8,19 +8,16 @@ import chat.rocket.android.server.domain.MultiServerTokenRepository
import com.squareup.moshi.Moshi
@PerActivity
class SharedPreferencesMultiServerTokenRepository(private val repository: LocalRepository,
private val moshi: Moshi
class SharedPreferencesMultiServerTokenRepository(
private val repository: LocalRepository,
private val moshi: Moshi
) : MultiServerTokenRepository {
override fun get(server: String): TokenModel? {
val token = repository.get("$TOKEN_KEY$server")
val adapter = moshi.adapter<TokenModel>(TokenModel::class.java)
token?.let {
return adapter.fromJson(token)
}
return null
return token?.let { adapter.fromJson(it) }
}
override fun save(server: String, token: TokenModel) {
......
......@@ -85,20 +85,20 @@ class LoginPresenter @Inject constructor(
}
}
val myself = retryIO("me()") { client.me() }
if (myself.username != null) {
myself.username?.let { username ->
val user = User(
id = myself.id,
roles = myself.roles,
status = myself.status,
name = myself.name,
emails = myself.emails?.map { Email(it.address ?: "", it.verified) },
username = myself.username,
username = username,
utcOffset = myself.utcOffset
)
localRepository.saveCurrentUser(currentServer, user)
saveCurrentServer.save(currentServer)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, myself.username)
saveAccount(myself.username!!)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username)
saveAccount(username)
saveToken(token)
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
......@@ -133,7 +133,7 @@ class LoginPresenter @Inject constructor(
fun forgotPassword() = navigator.toForgotPassword()
private suspend fun saveAccount(username: String) {
private fun saveAccount(username: String) {
val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it)
}
......
......@@ -39,11 +39,9 @@ internal const val REQUEST_CODE_FOR_SIGN_IN_REQUIRED = 1
internal const val REQUEST_CODE_FOR_MULTIPLE_ACCOUNTS_RESOLUTION = 2
internal const val REQUEST_CODE_FOR_SAVE_RESOLUTION = 3
fun newInstance(serverName: String): Fragment {
return LoginFragment().apply {
arguments = Bundle(1).apply {
putString(SERVER_NAME, serverName)
}
fun newInstance(serverName: String): Fragment = LoginFragment().apply {
arguments = Bundle(1).apply {
putString(SERVER_NAME, serverName)
}
}
......@@ -59,9 +57,8 @@ class LoginFragment : Fragment(), LoginView {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
serverName = bundle.getString(SERVER_NAME)
arguments?.run {
serverName = getString(SERVER_NAME)
}
}
......@@ -93,7 +90,7 @@ class LoginFragment : Fragment(), LoginView {
text_username_or_email.setText(credential.first)
text_password.setText(credential.second)
}
REQUEST_CODE_FOR_SAVE_RESOLUTION -> showMessage(getString(R.string.message_credentials_saved_successfully))
REQUEST_CODE_FOR_SAVE_RESOLUTION -> showMessage(getString(R.string.msg_credentials_saved_successfully))
}
}
}
......@@ -150,7 +147,7 @@ class LoginFragment : Fragment(), LoginView {
override fun showGenericErrorMessage() = showMessage(R.string.msg_generic_error)
private fun setupOnClickListener() =
ui { _ ->
ui {
button_log_in.setOnClickListener {
presenter.authenticateWithUserAndPassword(
text_username_or_email.textContent,
......@@ -160,7 +157,7 @@ class LoginFragment : Fragment(), LoginView {
}
override fun showForgotPasswordView() {
ui { _ ->
ui {
button_forgot_your_password.isVisible = true
button_forgot_your_password.setOnClickListener { presenter.forgotPassword() }
......
......@@ -31,8 +31,7 @@ import chat.rocket.core.internal.rest.loginWithCas
import chat.rocket.core.internal.rest.loginWithOauth
import chat.rocket.core.internal.rest.loginWithSaml
import chat.rocket.core.internal.rest.me
import kotlinx.coroutines.experimental.delay
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.delay
import javax.inject.Inject
private const val TYPE_LOGIN_OAUTH = 1
......@@ -109,11 +108,11 @@ class LoginOptionsPresenter @Inject constructor(
when (loginType) {
TYPE_LOGIN_OAUTH -> client.loginWithOauth(credentialToken, credentialSecret)
TYPE_LOGIN_CAS -> {
delay(3, TimeUnit.SECONDS)
delay(3000)
client.loginWithCas(credentialToken)
}
TYPE_LOGIN_SAML -> {
delay(3, TimeUnit.SECONDS)
delay(3000)
client.loginWithSaml(credentialToken)
}
TYPE_LOGIN_DEEP_LINK -> {
......@@ -174,7 +173,7 @@ class LoginOptionsPresenter @Inject constructor(
settings = settingsInteractor.get(currentServer)
}
private suspend fun saveAccount(username: String) {
private fun saveAccount(username: String) {
val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it)
}
......
......@@ -88,36 +88,34 @@ fun newInstance(
isLoginFormEnabled: Boolean,
isNewAccountCreationEnabled: Boolean,
deepLinkInfo: LoginDeepLinkInfo? = null
): Fragment {
return LoginOptionsFragment().apply {
arguments = Bundle(23).apply {
putString(SERVER_NAME, serverName)
putString(STATE, state)
putString(FACEBOOK_OAUTH_URL, facebookOauthUrl)
putString(GITHUB_OAUTH_URL, githubOauthUrl)
putString(GOOGLE_OAUTH_URL, googleOauthUrl)
putString(LINKEDIN_OAUTH_URL, linkedinOauthUrl)
putString(GITLAB_OAUTH_URL, gitlabOauthUrl)
putString(WORDPRESS_OAUTH_URL, wordpressOauthUrl)
putString(CAS_LOGIN_URL, casLoginUrl)
putString(CAS_TOKEN, casToken)
putString(CAS_SERVICE_NAME, casServiceName)
putInt(CAS_SERVICE_NAME_TEXT_COLOR, casServiceNameTextColor)
putInt(CAS_SERVICE_BUTTON_COLOR, casServiceButtonColor)
putString(CUSTOM_OAUTH_URL, customOauthUrl)
putString(CUSTOM_OAUTH_SERVICE_NAME, customOauthServiceName)
putInt(CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR, customOauthServiceNameTextColor)
putInt(CUSTOM_OAUTH_SERVICE_BUTTON_COLOR, customOauthServiceButtonColor)
putString(SAML_URL, samlUrl)
putString(SAML_TOKEN, samlToken)
putString(SAML_SERVICE_NAME, samlServiceName)
putInt(SAML_SERVICE_NAME_TEXT_COLOR, samlServiceNameTextColor)
putInt(SAML_SERVICE_BUTTON_COLOR, samlServiceButtonColor)
putInt(TOTAL_SOCIAL_ACCOUNTS, totalSocialAccountsEnabled)
putBoolean(IS_LOGIN_FORM_ENABLED, isLoginFormEnabled)
putBoolean(IS_NEW_ACCOUNT_CREATION_ENABLED, isNewAccountCreationEnabled)
putParcelable(DEEP_LINK_INFO, deepLinkInfo)
}
): Fragment = LoginOptionsFragment().apply {
arguments = Bundle(23).apply {
putString(SERVER_NAME, serverName)
putString(STATE, state)
putString(FACEBOOK_OAUTH_URL, facebookOauthUrl)
putString(GITHUB_OAUTH_URL, githubOauthUrl)
putString(GOOGLE_OAUTH_URL, googleOauthUrl)
putString(LINKEDIN_OAUTH_URL, linkedinOauthUrl)
putString(GITLAB_OAUTH_URL, gitlabOauthUrl)
putString(WORDPRESS_OAUTH_URL, wordpressOauthUrl)
putString(CAS_LOGIN_URL, casLoginUrl)
putString(CAS_TOKEN, casToken)
putString(CAS_SERVICE_NAME, casServiceName)
putInt(CAS_SERVICE_NAME_TEXT_COLOR, casServiceNameTextColor)
putInt(CAS_SERVICE_BUTTON_COLOR, casServiceButtonColor)
putString(CUSTOM_OAUTH_URL, customOauthUrl)
putString(CUSTOM_OAUTH_SERVICE_NAME, customOauthServiceName)
putInt(CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR, customOauthServiceNameTextColor)
putInt(CUSTOM_OAUTH_SERVICE_BUTTON_COLOR, customOauthServiceButtonColor)
putString(SAML_URL, samlUrl)
putString(SAML_TOKEN, samlToken)
putString(SAML_SERVICE_NAME, samlServiceName)
putInt(SAML_SERVICE_NAME_TEXT_COLOR, samlServiceNameTextColor)
putInt(SAML_SERVICE_BUTTON_COLOR, samlServiceButtonColor)
putInt(TOTAL_SOCIAL_ACCOUNTS, totalSocialAccountsEnabled)
putBoolean(IS_LOGIN_FORM_ENABLED, isLoginFormEnabled)
putBoolean(IS_NEW_ACCOUNT_CREATION_ENABLED, isNewAccountCreationEnabled)
putParcelable(DEEP_LINK_INFO, deepLinkInfo)
}
}
......@@ -157,34 +155,33 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
serverName = bundle.getString(SERVER_NAME)
state = bundle.getString(STATE)
facebookOauthUrl = bundle.getString(FACEBOOK_OAUTH_URL)
githubOauthUrl = bundle.getString(GITHUB_OAUTH_URL)
googleOauthUrl = bundle.getString(GOOGLE_OAUTH_URL)
linkedinOauthUrl = bundle.getString(LINKEDIN_OAUTH_URL)
gitlabOauthUrl = bundle.getString(GITLAB_OAUTH_URL)
wordpressOauthUrl = bundle.getString(WORDPRESS_OAUTH_URL)
casLoginUrl = bundle.getString(CAS_LOGIN_URL)
casToken = bundle.getString(CAS_TOKEN)
casServiceName = bundle.getString(CAS_SERVICE_NAME)
casServiceNameTextColor = bundle.getInt(CAS_SERVICE_NAME_TEXT_COLOR)
casServiceButtonColor = bundle.getInt(CAS_SERVICE_BUTTON_COLOR)
customOauthUrl = bundle.getString(CUSTOM_OAUTH_URL)
customOauthServiceName = bundle.getString(CUSTOM_OAUTH_SERVICE_NAME)
customOauthServiceTextColor = bundle.getInt(CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR)
customOauthServiceButtonColor = bundle.getInt(CUSTOM_OAUTH_SERVICE_BUTTON_COLOR)
samlUrl = bundle.getString(SAML_URL)
samlToken = bundle.getString(SAML_TOKEN)
samlServiceName = bundle.getString(SAML_SERVICE_NAME)
samlServiceTextColor = bundle.getInt(SAML_SERVICE_NAME_TEXT_COLOR)
samlServiceButtonColor = bundle.getInt(SAML_SERVICE_BUTTON_COLOR)
totalSocialAccountsEnabled = bundle.getInt(TOTAL_SOCIAL_ACCOUNTS)
isLoginFormEnabled = bundle.getBoolean(IS_LOGIN_FORM_ENABLED)
isNewAccountCreationEnabled = bundle.getBoolean(IS_NEW_ACCOUNT_CREATION_ENABLED)
deepLinkInfo = bundle.getParcelable(DEEP_LINK_INFO)
arguments?.run {
serverName = getString(SERVER_NAME)
state = getString(STATE)
facebookOauthUrl = getString(FACEBOOK_OAUTH_URL)
githubOauthUrl = getString(GITHUB_OAUTH_URL)
googleOauthUrl = getString(GOOGLE_OAUTH_URL)
linkedinOauthUrl = getString(LINKEDIN_OAUTH_URL)
gitlabOauthUrl = getString(GITLAB_OAUTH_URL)
wordpressOauthUrl = getString(WORDPRESS_OAUTH_URL)
casLoginUrl = getString(CAS_LOGIN_URL)
casToken = getString(CAS_TOKEN)
casServiceName = getString(CAS_SERVICE_NAME)
casServiceNameTextColor = getInt(CAS_SERVICE_NAME_TEXT_COLOR)
casServiceButtonColor = getInt(CAS_SERVICE_BUTTON_COLOR)
customOauthUrl = getString(CUSTOM_OAUTH_URL)
customOauthServiceName = getString(CUSTOM_OAUTH_SERVICE_NAME)
customOauthServiceTextColor = getInt(CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR)
customOauthServiceButtonColor = getInt(CUSTOM_OAUTH_SERVICE_BUTTON_COLOR)
samlUrl = getString(SAML_URL)
samlToken = getString(SAML_TOKEN)
samlServiceName = getString(SAML_SERVICE_NAME)
samlServiceTextColor = getInt(SAML_SERVICE_NAME_TEXT_COLOR)
samlServiceButtonColor = getInt(SAML_SERVICE_BUTTON_COLOR)
totalSocialAccountsEnabled = getInt(TOTAL_SOCIAL_ACCOUNTS)
isLoginFormEnabled = getBoolean(IS_LOGIN_FORM_ENABLED)
isNewAccountCreationEnabled = getBoolean(IS_NEW_ACCOUNT_CREATION_ENABLED)
deepLinkInfo = getParcelable(DEEP_LINK_INFO)
}
}
......@@ -388,7 +385,7 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
}
override fun setupExpandAccountsView() {
ui { _ ->
ui {
expand_more_accounts_container.isVisible = true
var isAccountsCollapsed = true
button_expand_collapse_accounts.setOnClickListener {
......@@ -406,14 +403,14 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
}
override fun showLoginWithEmailButton() {
ui { _ ->
ui {
button_login_with_email.setOnClickListener { presenter.toLoginWithEmail() }
button_login_with_email.isVisible = true
}
}
override fun showCreateNewAccountButton() {
ui { _ ->
ui {
button_create_an_account.setOnClickListener { presenter.toCreateAccount() }
button_create_an_account.isVisible = true
}
......
......@@ -10,8 +10,8 @@ import chat.rocket.android.server.domain.SaveConnectingServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extension.launchUI
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class OnBoardingPresenter @Inject constructor(
......@@ -19,7 +19,7 @@ class OnBoardingPresenter @Inject constructor(
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveConnectingServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
val settingsInteractor: GetSettingsInteractor,
val factory: RocketChatClientFactory
......@@ -80,7 +80,7 @@ class OnBoardingPresenter @Inject constructor(
}
view.showLoading()
try {
withContext(DefaultDispatcher) {
withContext(Dispatchers.Default) {
setupConnectionInfo(serverUrl)
// preparing next fragment before showing it
......
......@@ -31,12 +31,10 @@ import javax.inject.Inject
private const val BUNDLE_USER_ID = "user_id"
private const val BUNDLE_AUTH_TOKEN = "auth_token"
fun newInstance(userId: String, authToken: String): Fragment {
return RegisterUsernameFragment().apply {
arguments = Bundle(2).apply {
putString(BUNDLE_USER_ID, userId)
putString(BUNDLE_AUTH_TOKEN, authToken)
}
fun newInstance(userId: String, authToken: String): Fragment = RegisterUsernameFragment().apply {
arguments = Bundle(2).apply {
putString(BUNDLE_USER_ID, userId)
putString(BUNDLE_AUTH_TOKEN, authToken)
}
}
......@@ -53,13 +51,10 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
userId = bundle.getString(BUNDLE_USER_ID)
authToken = bundle.getString(BUNDLE_AUTH_TOKEN)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
arguments?.run {
userId = getString(BUNDLE_USER_ID, "")
authToken = getString(BUNDLE_AUTH_TOKEN, "")
} ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
}
override fun onCreateView(
......
......@@ -12,8 +12,8 @@ import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.isValidUrl
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class ServerPresenter @Inject constructor(
......@@ -98,7 +98,7 @@ class ServerPresenter @Inject constructor(
}
view.showLoading()
try {
withContext(DefaultDispatcher) {
withContext(Dispatchers.Default) {
// preparing next fragment before showing it
refreshServerAccounts()
checkEnabledAccounts(serverUrl)
......
......@@ -63,7 +63,7 @@ class SignupFragment : Fragment(), SignupView {
if (resultCode == Activity.RESULT_OK) {
if (data != null) {
if (requestCode == SAVE_CREDENTIALS) {
showMessage(getString(R.string.message_credentials_saved_successfully))
showMessage(getString(R.string.msg_credentials_saved_successfully))
}
}
}
......@@ -75,7 +75,7 @@ class SignupFragment : Fragment(), SignupView {
}
private fun setupOnClickListener() =
ui { _ ->
ui {
button_register.setOnClickListener {
presenter.signup(
text_username.textContent,
......
......@@ -94,7 +94,7 @@ class TwoFAPresenter @Inject constructor(
}
}
private suspend fun saveAccount(me: Myself) {
private fun saveAccount(me: Myself) {
val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it)
}
......
......@@ -25,12 +25,10 @@ import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_authentication_two_fa.*
import javax.inject.Inject
fun newInstance(username: String, password: String): Fragment {
return TwoFAFragment().apply {
arguments = Bundle(2).apply {
putString(BUNDLE_USERNAME, username)
putString(BUNDLE_PASSWORD, password)
}
fun newInstance(username: String, password: String): Fragment = TwoFAFragment().apply {
arguments = Bundle(2).apply {
putString(BUNDLE_USERNAME, username)
putString(BUNDLE_PASSWORD, password)
}
}
......@@ -50,13 +48,10 @@ class TwoFAFragment : Fragment(), TwoFAView {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
username = bundle.getString(BUNDLE_USERNAME)
password = bundle.getString(BUNDLE_PASSWORD)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
arguments?.run {
username = getString(BUNDLE_USERNAME, "")
password = getString(BUNDLE_PASSWORD, "")
} ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
}
override fun onCreateView(
......
package chat.rocket.android.chatdetails.adapter
import android.content.Context
import DrawableHelper
import android.view.View
import android.widget.ImageView
import android.widget.TextView
......
......@@ -7,8 +7,10 @@ import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.favorite
import chat.rocket.core.internal.rest.getInfo
import chat.rocket.core.model.Room
import timber.log.Timber
......@@ -25,6 +27,31 @@ class ChatDetailsPresenter @Inject constructor(
private val manager = factory.create(currentServer)
private val client = manager.client
fun toggleFavoriteChatRoom(roomId: String, isFavorite: Boolean) {
launchUI(strategy) {
try {
// Note: If it is favorite then the user wants to remove the favorite - and vice versa.
retryIO("favorite($roomId, ${!isFavorite})") {
client.favorite(roomId, !isFavorite)
}
view.showFavoriteIcon(!isFavorite)
} catch (e: RocketChatException) {
Timber.e(
e,
"Error while trying to favorite or removing the favorite of a chat room."
)
e.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
}
fun toVideoConference(roomId: String, chatRoomType: String) =
navigator.toVideoConference(roomId, chatRoomType)
fun getDetails(chatRoomId: String, chatRoomType: String) {
launchUI(strategy) {
try {
......@@ -32,7 +59,7 @@ class ChatDetailsPresenter @Inject constructor(
client.getInfo(chatRoomId, null, roomTypeOf(chatRoomType))
}
view.displayDetails(roomToChatDetails(room))
} catch(exception: Exception) {
} catch (exception: Exception) {
Timber.e(exception)
exception.message?.let {
view.showMessage(it)
......
......@@ -5,5 +5,16 @@ import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface ChatDetailsView: MessageView {
/**
* Shows the corresponding favorite icon for a favorite or non-favorite chat room.
*
* @param isFavorite True if a chat room is favorite, false otherwise.
*/
fun showFavoriteIcon(isFavorite: Boolean)
/**
* Shows the details of a chat room.
*/
fun displayDetails(room: ChatDetails)
}
\ No newline at end of file
......@@ -11,9 +11,15 @@ import chat.rocket.android.util.extensions.inflate
class ChatDetailsAdapter: RecyclerView.Adapter<OptionViewHolder>() {
private val options: MutableList<Option> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OptionViewHolder = OptionViewHolder(parent.inflate(R.layout.item_detail_option))
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): OptionViewHolder = OptionViewHolder(parent.inflate(R.layout.item_detail_option))
override fun onBindViewHolder(holder: OptionViewHolder, position: Int) = holder.bindViews(OptionItemHolder(options[position]))
override fun onBindViewHolder(
holder: OptionViewHolder,
position: Int
) = holder.bindViews(OptionItemHolder(options[position]))
override fun getItemCount(): Int = options.size
......
......@@ -3,6 +3,8 @@ package chat.rocket.android.chatdetails.ui
import DrawableHelper
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
......@@ -17,6 +19,8 @@ import chat.rocket.android.chatdetails.presentation.ChatDetailsView
import chat.rocket.android.chatdetails.viewmodel.ChatDetailsViewModel
import chat.rocket.android.chatdetails.viewmodel.ChatDetailsViewModelFactory
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
......@@ -31,23 +35,28 @@ fun newInstance(
chatRoomId: String,
chatRoomType: String,
isSubscribed: Boolean,
isFavorite: Boolean,
disableMenu: Boolean
): ChatDetailsFragment {
return ChatDetailsFragment().apply {
arguments = Bundle(4).apply {
arguments = Bundle(5).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
putString(BUNDLE_CHAT_ROOM_TYPE, chatRoomType)
putBoolean(BUNDLE_IS_SUBSCRIBED, isSubscribed)
putBoolean(BUNDLE_IS_FAVORITE, isFavorite)
putBoolean(BUNDLE_DISABLE_MENU, disableMenu)
}
}
}
}
internal const val TAG_CHAT_DETAILS_FRAGMENT = "ChatDetailsFragment"
internal const val MENU_ACTION_FAVORITE_REMOVE_FAVORITE = 1
internal const val MENU_ACTION_VIDEO_CALL = 2
private const val BUNDLE_CHAT_ROOM_ID = "BUNDLE_CHAT_ROOM_ID"
private const val BUNDLE_CHAT_ROOM_TYPE = "BUNDLE_CHAT_ROOM_TYPE"
private const val BUNDLE_IS_SUBSCRIBED = "BUNDLE_IS_SUBSCRIBED"
private const val BUNDLE_IS_FAVORITE = "BUNDLE_IS_FAVORITE"
private const val BUNDLE_DISABLE_MENU = "BUNDLE_DISABLE_MENU"
class ChatDetailsFragment : Fragment(), ChatDetailsView {
......@@ -55,26 +64,32 @@ class ChatDetailsFragment : Fragment(), ChatDetailsView {
lateinit var presenter: ChatDetailsPresenter
@Inject
lateinit var factory: ChatDetailsViewModelFactory
@Inject
lateinit var serverUrl: CurrentServerRepository
@Inject
lateinit var settings: GetSettingsInteractor
private var adapter: ChatDetailsAdapter? = null
private lateinit var viewModel: ChatDetailsViewModel
private var chatRoomId: String? = null
private var chatRoomType: String? = null
internal lateinit var chatRoomId: String
internal lateinit var chatRoomType: String
private var isSubscribed: Boolean = true
internal var isFavorite: Boolean = false
private var disableMenu: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = bundle.getString(BUNDLE_CHAT_ROOM_TYPE)
isSubscribed = bundle.getBoolean(BUNDLE_IS_SUBSCRIBED)
disableMenu = bundle.getBoolean(BUNDLE_DISABLE_MENU)
} else {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
arguments?.run {
chatRoomId = getString(BUNDLE_CHAT_ROOM_ID)
chatRoomType = getString(BUNDLE_CHAT_ROOM_TYPE)
isSubscribed = getBoolean(BUNDLE_IS_SUBSCRIBED)
isFavorite = getBoolean(BUNDLE_IS_FAVORITE)
disableMenu = getBoolean(BUNDLE_DISABLE_MENU)
} ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
setHasOptionsMenu(true)
}
override fun onCreateView(
......@@ -91,11 +106,27 @@ class ChatDetailsFragment : Fragment(), ChatDetailsView {
getDetails()
}
override fun onPrepareOptionsMenu(menu: Menu) {
menu.clear()
setupMenu(menu)
super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
setOnMenuItemClickListener(item)
return true
}
override fun showFavoriteIcon(isFavorite: Boolean) {
this.isFavorite = isFavorite
activity?.invalidateOptionsMenu()
}
override fun displayDetails(room: ChatDetails) {
ui {
val text = room.name
name.text = text
bindImage(chatRoomType!!)
bindImage(chatRoomType)
content_topic.text =
if (room.topic.isNullOrEmpty()) getString(R.string.msg_no_topic) else room.topic
content_announcement.text =
......@@ -207,8 +238,8 @@ class ChatDetailsFragment : Fragment(), ChatDetailsView {
private fun setupToolbar() {
with((activity as ChatRoomActivity)) {
hideToolbarChatRoomIcon()
showToolbarTitle(getString(R.string.title_channel_details))
hideExpandMoreForToolbar()
setupToolbarTitle(getString(R.string.title_channel_details))
}
}
}
\ No newline at end of file
package chat.rocket.android.chatdetails.ui
import android.view.Menu
import android.view.MenuItem
import chat.rocket.android.R
import chat.rocket.android.server.domain.isJitsiEnabled
import chat.rocket.android.server.domain.isJitsiEnabledForChannels
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
internal fun ChatDetailsFragment.setupMenu(menu: Menu) {
serverUrl.get()?.let {
with(settings.get(it)) {
if (isJitsiEnabled()) {
if (roomTypeOf(chatRoomType) !is RoomType.DirectMessage && !isJitsiEnabledForChannels()) {
return
}
menu.add(
Menu.NONE,
MENU_ACTION_VIDEO_CALL,
Menu.NONE,
R.string.msg_video_call
).setIcon(R.drawable.ic_video_white_24dp).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
}
}
if (isFavorite) {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_REMOVE_FAVORITE,
Menu.NONE,
R.string.action_remove_favorite
).setIcon(R.drawable.ic_star_yellow_24dp).setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} else {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_REMOVE_FAVORITE,
Menu.NONE,
R.string.action_favorite
).setIcon(R.drawable.ic_star_border_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
}
internal fun ChatDetailsFragment.setOnMenuItemClickListener(item: MenuItem) {
if (item.itemId == MENU_ACTION_FAVORITE_REMOVE_FAVORITE) {
presenter.toggleFavoriteChatRoom(chatRoomId, isFavorite)
} else if (item.itemId == MENU_ACTION_VIDEO_CALL) {
presenter.toVideoConference(chatRoomId, chatRoomType)
}
}
......@@ -5,9 +5,10 @@ import androidx.lifecycle.ViewModelProvider
import chat.rocket.android.db.ChatRoomDao
import javax.inject.Inject
class ChatDetailsViewModelFactory @Inject constructor(private val chatRoomDao: ChatRoomDao) : ViewModelProvider.NewInstanceFactory() {
class ChatDetailsViewModelFactory @Inject constructor(
private val chatRoomDao: ChatRoomDao
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>) =
ChatDetailsViewModel(chatRoomDao) as T
override fun <T : ViewModel?> create(modelClass: Class<T>) = ChatDetailsViewModel(chatRoomDao) as T
}
\ No newline at end of file
......@@ -7,7 +7,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class MessageInfoFragmentModule {
......
......@@ -24,7 +24,6 @@ fun Context.messageInformationIntent(messageId: String): Intent {
private const val INTENT_MESSAGE_ID = "message_id"
class MessageInfoActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
......@@ -45,7 +44,7 @@ class MessageInfoActivity : AppCompatActivity(), HasSupportFragmentInjector {
}
private fun setupToolbar() {
text_room_name.textContent = getString(R.string.message_information_title)
text_toolbar_title.textContent = getString(R.string.message_information_title)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
......
......@@ -12,7 +12,10 @@ import com.facebook.drawee.backends.pipeline.Fresco
import kotlinx.android.synthetic.main.item_action_button.view.*
import timber.log.Timber
class ActionsListAdapter(actions: List<Action>, var actionAttachmentOnClickListener: ActionAttachmentOnClickListener) : RecyclerView.Adapter<ActionsListAdapter.ViewHolder>() {
class ActionsListAdapter(
actions: List<Action>,
var actionAttachmentOnClickListener: ActionAttachmentOnClickListener
) : RecyclerView.Adapter<ActionsListAdapter.ViewHolder>() {
var actions: List<Action> = actions
......@@ -62,9 +65,7 @@ class ActionsListAdapter(actions: List<Action>, var actionAttachmentOnClickListe
return ViewHolder(view)
}
override fun getItemCount(): Int {
return actions.size
}
override fun getItemCount(): Int = actions.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val action = actions[position]
......
......@@ -92,14 +92,14 @@ abstract class BaseViewHolder<T : BaseUiModel<*>>(
data?.let { vm ->
vm.message.let {
val menuItems = view.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_message_unpin }?.apply {
setTitle(if (it.pinned) R.string.action_msg_unpin else R.string.action_msg_pin)
menuItems.find { it.itemId == R.id.action_pin }?.apply {
setTitle(if (it.pinned) R.string.action_unpin else R.string.action_pin)
isChecked = it.pinned
}
menuItems.find { it.itemId == R.id.action_message_star }?.apply {
menuItems.find { it.itemId == R.id.action_star }?.apply {
val isStarred = it.starred?.isNotEmpty() ?: false
setTitle(if (isStarred) R.string.action_msg_unstar else R.string.action_msg_star)
setTitle(if (isStarred) R.string.action_unstar else R.string.action_star)
isChecked = isStarred
}
view.context?.let {
......
......@@ -5,6 +5,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.chatroom.uimodel.AttachmentUiModel
import chat.rocket.android.chatroom.uimodel.BaseUiModel
......@@ -29,7 +30,8 @@ class ChatRoomAdapter(
private val actionSelectListener: OnActionSelected? = null,
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null,
private val navigator: ChatRoomNavigator? = null
private val navigator: ChatRoomNavigator? = null,
private val analyticsManager: AnalyticsManager? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseUiModel<*>>()
......@@ -44,8 +46,14 @@ class ChatRoomAdapter(
MessageViewHolder(
view,
actionsListener,
reactionListener
) { userId -> navigator?.toUserDetails(userId) }
reactionListener,
{ userId -> navigator?.toUserDetails(userId) },
{
if (roomId != null && roomType != null) {
navigator?.toVideoConference(roomId, roomType)
}
}
)
}
BaseUiModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview)
......@@ -76,13 +84,9 @@ class ChatRoomAdapter(
}
}
override fun getItemViewType(position: Int): Int {
return dataSet[position].viewType
}
override fun getItemViewType(position: Int): Int = dataSet[position].viewType
override fun getItemCount(): Int {
return dataSet.size
}
override fun getItemCount(): Int = dataSet.size
override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
if (holder !is MessageViewHolder) {
......@@ -105,8 +109,9 @@ class ChatRoomAdapter(
when (holder) {
is MessageViewHolder ->
holder.bind(dataSet[position] as MessageUiModel)
is UrlPreviewViewHolder ->
is UrlPreviewViewHolder -> {
holder.bind(dataSet[position] as UrlPreviewUiModel)
}
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyUiModel)
is AttachmentViewHolder ->
......@@ -174,12 +179,12 @@ class ChatRoomAdapter(
Timber.d("index: $index")
if (index > -1) {
dataSet[index] = message
dataSet.forEachIndexed { index, viewModel ->
dataSet.forEachIndexed { ind, viewModel ->
if (viewModel.messageId == message.messageId) {
if (viewModel.nextDownStreamMessage == null) {
viewModel.reactions = message.reactions
}
notifyItemChanged(index)
notifyItemChanged(ind)
}
}
// Delete message only if current is a system message update, i.e.: Message Removed
......@@ -236,47 +241,63 @@ class ChatRoomAdapter(
override fun isActionsEnabled(): Boolean = enableActions
override fun onActionSelected(item: MenuItem, message: Message) {
message.apply {
when (item.itemId) {
R.id.action_message_info -> {
actionSelectListener?.showMessageInfo(id)
}
R.id.action_message_reply -> {
if (roomName != null && roomType != null) {
actionSelectListener?.citeMessage(roomName, roomType, id, true)
if (analyticsManager != null && roomName != null && roomType != null && actionSelectListener != null) {
with(message) {
when (item.itemId) {
R.id.action_info -> {
actionSelectListener.showMessageInfo(id)
analyticsManager.logMessageActionInfo()
}
}
R.id.action_message_quote -> {
if (roomName != null && roomType != null) {
actionSelectListener?.citeMessage(roomName, roomType, id, false)
R.id.action_reply -> {
actionSelectListener.citeMessage(roomName, roomType, id, true)
analyticsManager.logMessageActionReply()
}
R.id.action_quote -> {
actionSelectListener.citeMessage(roomName, roomType, id, false)
analyticsManager.logMessageActionQuote()
}
R.id.action_copy -> {
actionSelectListener.copyMessage(id)
analyticsManager.logMessageActionCopy()
}
R.id.action_edit -> {
actionSelectListener.editMessage(roomId, id, this.message)
analyticsManager.logMessageActionEdit()
}
R.id.action_star -> {
actionSelectListener.toggleStar(id, !item.isChecked)
analyticsManager.logMessageActionStar()
}
R.id.action_pin -> {
actionSelectListener.togglePin(id, !item.isChecked)
analyticsManager.logMessageActionPin()
}
R.id.action_delete -> {
actionSelectListener.deleteMessage(roomId, id)
analyticsManager.logMessageActionDelete()
}
R.id.action_add_reaction -> {
actionSelectListener.showReactions(id)
analyticsManager.logMessageActionAddReaction()
}
R.id.action_permalink -> {
actionSelectListener.copyPermalink(id)
analyticsManager.logMessageActionPermalink()
}
R.id.action_report -> {
actionSelectListener.reportMessage(id)
analyticsManager.logMessageActionReport()
}
}
R.id.action_message_copy -> {
actionSelectListener?.copyMessage(id)
}
R.id.action_message_edit -> {
actionSelectListener?.editMessage(roomId, id, message.message)
}
R.id.action_message_star -> {
actionSelectListener?.toogleStar(id, !item.isChecked)
}
R.id.action_message_unpin -> {
actionSelectListener?.tooglePin(id, !item.isChecked)
}
R.id.action_message_delete -> {
actionSelectListener?.deleteMessage(roomId, id)
}
R.id.action_menu_msg_react -> {
actionSelectListener?.showReactions(id)
}
R.id.action_message_permalink -> {
actionSelectListener?.copyPermalink(id)
}
R.id.action_message_report -> {
actionSelectListener?.reportMessage(id)
}
else -> {
TODO("Not implemented")
}
}
}
......@@ -298,9 +319,9 @@ class ChatRoomAdapter(
fun editMessage(roomId: String, messageId: String, text: String)
fun toogleStar(id: String, star: Boolean)
fun toggleStar(id: String, star: Boolean)
fun tooglePin(id: String, pin: Boolean)
fun togglePin(id: String, pin: Boolean)
fun deleteMessage(roomId: String, id: String)
......
......@@ -13,8 +13,8 @@ import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiKeyboardListener
import chat.rocket.android.emoji.EmojiPickerPopup
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.emoji.internal.GlideApp
import chat.rocket.android.infrastructure.LocalRepository
import com.bumptech.glide.Glide
import kotlinx.android.synthetic.main.item_reaction.view.*
import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject
......@@ -103,9 +103,9 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
// The view at index 1 corresponds to the one to display custom emojis which are images.
view_flipper_reaction.displayedChild = 1
val glideRequest = if (reaction.url!!.endsWith("gif", true)) {
GlideApp.with(context).asGif()
Glide.with(context).asGif()
} else {
GlideApp.with(context).asBitmap()
Glide.with(context).asBitmap()
}
glideRequest.load(reaction.url).into(image_emoji)
......
......@@ -10,6 +10,7 @@ import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.core.model.MessageType
import chat.rocket.core.model.isSystemMessage
import com.bumptech.glide.load.resource.gif.GifDrawable
import kotlinx.android.synthetic.main.avatar.view.*
......@@ -19,7 +20,8 @@ class MessageViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
private val avatarListener: (String) -> Unit
private val avatarListener: (String) -> Unit,
private val joinVideoCallListener: (View) -> Unit
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener), Drawable.Callback {
init {
......@@ -51,6 +53,9 @@ class MessageViewHolder(
text_content.text_content.text = data.content
button_join_video_call.isVisible = data.message.type is MessageType.JitsiCallStarted
button_join_video_call.setOnClickListener { joinVideoCallListener(it) }
image_avatar.setImageURI(data.avatar)
text_content.setTextColor(if (data.isTemporary) Color.GRAY else Color.BLACK)
......
......@@ -24,8 +24,8 @@ class RoomSuggestionsAdapter : SuggestionsAdapter<RoomSuggestionsViewHolder>("#"
override fun bind(item: SuggestionModel, itemClickListener: SuggestionsAdapter.ItemClickListener?) {
item as ChatRoomSuggestionUiModel
with(itemView) {
val fullname = itemView.findViewById<TextView>(R.id.text_fullname)
val name = itemView.findViewById<TextView>(R.id.text_name)
val fullname = findViewById<TextView>(R.id.text_fullname)
val name = findViewById<TextView>(R.id.text_name)
name.text = item.name
fullname.text = item.fullName
setOnClickListener {
......
......@@ -8,15 +8,14 @@ import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.openTabbedUrl
import kotlinx.android.synthetic.main.message_url_preview.view.*
class UrlPreviewViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<UrlPreviewUiModel>(itemView, listener, reactionListener) {
class UrlPreviewViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<UrlPreviewUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(url_preview_layout)
}
setupActionMenu(itemView.url_preview_layout)
}
override fun bindViews(data: UrlPreviewUiModel) {
......
......@@ -7,7 +7,7 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class ChatRoomModule {
......
package chat.rocket.android.chatroom.presentation
import android.os.Build
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.chatdetails.ui.TAG_CHAT_DETAILS_FRAGMENT
import chat.rocket.android.chatinformation.ui.messageInformationIntent
......@@ -13,6 +15,7 @@ import chat.rocket.android.pinnedmessages.ui.TAG_PINNED_MESSAGES_FRAGMENT
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.userdetails.ui.TAG_USER_DETAILS_FRAGMENT
import chat.rocket.android.util.extensions.addFragmentBackStack
import chat.rocket.android.videoconference.ui.videoConferenceIntent
class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
......@@ -22,6 +25,19 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
}
}
fun toVideoConference(chatRoomId: String, chatRoomType: String) {
// TODO: Jitsi isn't working with Android M- version. We need to remove the condition bellow after it's solved. (https://github.com/jitsi/jitsi-meet/pull/3967)/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.startActivity(activity.videoConferenceIntent(chatRoomId, chatRoomType))
} else {
Toast.makeText(
activity,
"Sorry, unable to open the video conference due to device configuration",
Toast.LENGTH_SHORT
).show()
}
}
fun toChatRoom(
chatRoomId: String,
chatRoomName: String,
......@@ -51,6 +67,7 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
chatRoomId: String,
chatRoomType: String,
isChatRoomSubscribed: Boolean,
isChatRoomFavorite: Boolean,
isMenuDisabled: Boolean
) {
activity.addFragmentBackStack(TAG_CHAT_DETAILS_FRAGMENT, R.id.fragment_container) {
......@@ -58,6 +75,7 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
chatRoomId,
chatRoomType,
isChatRoomSubscribed,
isChatRoomFavorite,
isMenuDisabled
)
}
......
......@@ -13,13 +13,6 @@ import chat.rocket.core.model.ChatRoom
interface ChatRoomView : LoadingView, MessageView {
/**
* Shows the Favorite/Unfavorite chat room icon.
*
* @param isFavorite Shows the favorite icon if true, otherwise shows the unfavorite icon.
*/
fun showFavoriteIcon(isFavorite: Boolean)
/**
* Shows the chat room messages.
*
......
......@@ -10,8 +10,9 @@ import chat.rocket.android.server.infraestructure.DatabaseMessagesRepository
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.model.Message
import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
......@@ -33,7 +34,7 @@ class MessageService : JobService() {
}
override fun onStartJob(params: JobParameters?): Boolean {
launch(CommonPool) {
GlobalScope.launch(Dispatchers.IO) {
getAccountsInteractor.get().forEach { account ->
retrySendingMessages(params, account.serverUrl)
}
......@@ -44,7 +45,8 @@ class MessageService : JobService() {
private suspend fun retrySendingMessages(params: JobParameters?, serverUrl: String) {
val dbManager = dbFactory.create(serverUrl)
val messageRepository = DatabaseMessagesRepository(dbManager, DatabaseMessageMapper(dbManager))
val messageRepository =
DatabaseMessagesRepository(dbManager, DatabaseMessageMapper(dbManager))
val temporaryMessages = messageRepository.getAllUnsent()
.sortedWith(compareBy(Message::timestamp))
if (temporaryMessages.isNotEmpty()) {
......
......@@ -4,16 +4,15 @@ import DrawableHelper
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......@@ -31,18 +30,16 @@ fun Context.chatRoomIntent(
isCreator: Boolean = false,
isFavorite: Boolean = false,
chatRoomMessage: String? = null
): Intent {
return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
putExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, isReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putExtra(INTENT_CHAT_IS_SUBSCRIBED, isSubscribed)
putExtra(INTENT_CHAT_ROOM_IS_CREATOR, isCreator)
putExtra(INTENT_CHAT_ROOM_IS_FAVORITE, isFavorite)
putExtra(INTENT_CHAT_ROOM_MESSAGE, chatRoomMessage)
}
): Intent = Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
putExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, isReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putExtra(INTENT_CHAT_IS_SUBSCRIBED, isSubscribed)
putExtra(INTENT_CHAT_ROOM_IS_CREATOR, isCreator)
putExtra(INTENT_CHAT_ROOM_IS_FAVORITE, isFavorite)
putExtra(INTENT_CHAT_ROOM_MESSAGE, chatRoomMessage)
}
private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
......@@ -58,7 +55,6 @@ private const val INTENT_CHAT_ROOM_MESSAGE = "chat_room_message"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
// TODO - workaround for now... We will move to a single activity
@Inject
lateinit var serverInteractor: GetCurrentServerInteractor
......@@ -81,42 +77,44 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
return
}
val chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
with(intent) {
val chatRoomId = getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
val chatRoomName = intent.getStringExtra(INTENT_CHAT_ROOM_NAME)
requireNotNull(chatRoomName) { "no chat_room_name provided in Intent extras" }
val chatRoomName = getStringExtra(INTENT_CHAT_ROOM_NAME)
requireNotNull(chatRoomName) { "no chat_room_name provided in Intent extras" }
val chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
val chatRoomType = getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
val isReadOnly = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true)
val isReadOnly = getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true)
val isCreator = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_CREATOR, false)
val isCreator = getBooleanExtra(INTENT_CHAT_ROOM_IS_CREATOR, false)
val isFavorite = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_FAVORITE, false)
val isFavorite = getBooleanExtra(INTENT_CHAT_ROOM_IS_FAVORITE, false)
val chatRoomLastSeen = intent.getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
val chatRoomLastSeen = getLongExtra(INTENT_CHAT_ROOM_LAST_SEEN, -1)
val isSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true)
val isSubscribed = getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true)
val chatRoomMessage = intent.getStringExtra(INTENT_CHAT_ROOM_MESSAGE)
val chatRoomMessage = getStringExtra(INTENT_CHAT_ROOM_MESSAGE)
setupToolbar()
setupToolbar()
if (supportFragmentManager.findFragmentByTag(TAG_CHAT_ROOM_FRAGMENT) == null) {
addFragment(TAG_CHAT_ROOM_FRAGMENT, R.id.fragment_container) {
newInstance(
chatRoomId,
chatRoomName,
chatRoomType,
isReadOnly,
chatRoomLastSeen,
isSubscribed,
isCreator,
isFavorite,
chatRoomMessage
)
if (supportFragmentManager.findFragmentByTag(TAG_CHAT_ROOM_FRAGMENT) == null) {
addFragment(TAG_CHAT_ROOM_FRAGMENT, R.id.fragment_container) {
newInstance(
chatRoomId,
chatRoomName,
chatRoomType,
isReadOnly,
chatRoomLastSeen,
isSubscribed,
isCreator,
isFavorite,
chatRoomMessage
)
}
}
}
}
......@@ -136,31 +134,21 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
toolbar.setNavigationOnClickListener { finishActivity() }
}
fun showToolbarTitle(title: String) {
text_room_name.textContent = title
fun setupToolbarTitle(title: String) {
text_toolbar_title.textContent = title
}
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
}
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 setupExpandMoreForToolbar(listener: (View) -> Unit) {
DrawableHelper.compoundRightDrawable(
text_toolbar_title,
DrawableHelper.getDrawableFromId(R.drawable.ic_chatroom_toolbar_expand_more_20dp, this)
)
text_toolbar_title.setOnClickListener { listener(it) }
}
fun hideToolbarChatRoomIcon() {
text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
fun hideExpandMoreForToolbar() {
text_toolbar_title.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
text_toolbar_title.setOnClickListener(null)
}
private fun finishActivity() {
......
......@@ -4,10 +4,11 @@ import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.core.view.isVisible
import chat.rocket.android.emoji.internal.GlideApp
import chat.rocket.android.util.extensions.getFileName
import chat.rocket.android.util.extensions.getMimeType
import chat.rocket.common.util.ifNull
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
......@@ -25,18 +26,18 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
when {
mimeType.startsWith("image") -> {
if (mimeType.contains("gif")) {
GlideApp
Glide
.with(context)
.asGif()
.load(uri)
.fitCenter()
.apply(RequestOptions().fitCenter())
.into(imagePreview)
} else {
GlideApp
Glide
.with(context)
.asBitmap()
.load(uri)
.fitCenter()
.apply(RequestOptions().fitCenter())
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(
resource: Bitmap,
......@@ -75,10 +76,10 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
(citation ?: "") + description.text.toString()
)
}
alertDialog.dismiss()
alertDialog?.dismiss()
}
cancelButton.setOnClickListener { alertDialog.dismiss() }
alertDialog.show()
cancelButton.setOnClickListener { alertDialog?.dismiss() }
alertDialog?.show()
}
fun ChatRoomFragment.showDrawAttachmentDialog(byteArray: ByteArray) {
......@@ -92,9 +93,9 @@ fun ChatRoomFragment.showDrawAttachmentDialog(byteArray: ByteArray) {
byteArray,
(citation ?: "") + description.text.toString()
)
alertDialog.dismiss()
alertDialog?.dismiss()
}
cancelButton.setOnClickListener { alertDialog.dismiss() }
alertDialog.show()
}
\ No newline at end of file
cancelButton.setOnClickListener { alertDialog?.dismiss() }
alertDialog?.show()
}
......@@ -11,23 +11,6 @@ import chat.rocket.android.util.extension.onQueryTextListener
internal fun ChatRoomFragment.setupMenu(menu: Menu) {
setupSearchMessageMenuItem(menu, requireContext())
setupFavoriteMenuItem(menu)
setupDetailsMenuItem(menu)
}
internal fun ChatRoomFragment.setOnMenuItemClickListener(item: MenuItem) {
when (item.itemId) {
MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT -> presenter.toggleFavoriteChatRoom(
chatRoomId,
isFavorite
)
MENU_ACTION_SHOW_DETAILS -> presenter.toChatDetails(
chatRoomId,
chatRoomType,
isSubscribed,
disableMenu
)
}
}
private fun ChatRoomFragment.setupSearchMessageMenuItem(menu: Menu, context: Context) {
......@@ -37,7 +20,7 @@ private fun ChatRoomFragment.setupSearchMessageMenuItem(menu: Menu, context: Con
Menu.NONE,
R.string.title_search_message
).setActionView(SearchView(context))
.setIcon(R.drawable.ic_search_white_24dp)
.setIcon(R.drawable.ic_chatroom_toolbar_magnifier_20dp)
.setShowAsActionFlags(
MenuItem.SHOW_AS_ACTION_IF_ROOM or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
)
......@@ -53,8 +36,9 @@ private fun ChatRoomFragment.setupSearchMessageMenuItem(menu: Menu, context: Con
}
})
(searchItem?.actionView as? SearchView)?.let {
(searchItem.actionView as? SearchView)?.let {
// TODO: Check why we need to stylize the search text programmatically instead of by defining it in the styles.xml (ChatRoom.SearchView)
it.maxWidth = Integer.MAX_VALUE
stylizeSearchView(it, context)
setupSearchViewTextListener(it)
if (it.isIconified) {
......@@ -81,34 +65,4 @@ private fun ChatRoomFragment.setupSearchViewTextListener(searchView: SearchView)
isSearchTermQueried = true
}
}
}
private fun ChatRoomFragment.setupFavoriteMenuItem(menu: Menu) {
if (isFavorite) {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT,
Menu.NONE,
R.string.title_unfavorite_chat
).setIcon(R.drawable.ic_star_yellow_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} else {
menu.add(
Menu.NONE,
MENU_ACTION_FAVORITE_UNFAVOURITE_CHAT,
Menu.NONE,
R.string.title_favorite_chat
).setIcon(R.drawable.ic_star_border_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
}
private fun ChatRoomFragment.setupDetailsMenuItem(menu: Menu) {
menu.add(
Menu.NONE,
MENU_ACTION_SHOW_DETAILS,
Menu.NONE,
R.string.title_channel_details
).setIcon(R.drawable.ic_info_outline_white_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
}
\ No newline at end of file
......@@ -30,5 +30,5 @@ interface BaseUiModel<out T> {
internal fun Int.toViewType(): BaseUiModel.ViewType {
return BaseUiModel.ViewType.values().firstOrNull { it.viewType == this }
?: throw InvalidParameterException("Invalid viewType: $this for BaseUiModel.ViewType")
?: throw InvalidParameterException("Invalid viewType: $this for BaseUiModel.ViewType")
}
\ No newline at end of file
......@@ -26,7 +26,6 @@ data class MessageUiModel(
) : BaseMessageUiModel<Message> {
override val viewType: Int
get() = BaseUiModel.ViewType.MESSAGE.viewType
override val layoutId: Int
get() = R.layout.item_message
}
\ No newline at end of file
......@@ -36,14 +36,29 @@ class RoomUiModelMapper(
grouped: Boolean = false,
showLastMessage: Boolean = true
): List<ItemHolder<*>> {
val list = ArrayList<ItemHolder<*>>(rooms.size + 4)
val list = ArrayList<ItemHolder<*>>(rooms.size + 5)
var lastType: String? = null
rooms.forEach { room ->
if (grouped && lastType != room.chatRoom.type) {
list.add(HeaderItemHolder(roomType(room.chatRoom.type)))
if (grouped) {
val favRooms = rooms.filter { it.chatRoom.favorite == true }
val unfavRooms = rooms.filterNot { it.chatRoom.favorite == true }
if (favRooms.isNotEmpty()) {
list.add(HeaderItemHolder(context.resources.getString(R.string.header_favorite)))
}
favRooms.forEach { room ->
list.add(RoomItemHolder(map(room, showLastMessage)))
}
unfavRooms.forEach { room ->
if (lastType != room.chatRoom.type) {
list.add(HeaderItemHolder(roomType(room.chatRoom.type)))
}
list.add(RoomItemHolder(map(room, showLastMessage)))
lastType = room.chatRoom.type
}
} else {
rooms.forEach { room ->
list.add(RoomItemHolder(map(room, showLastMessage)))
}
list.add(RoomItemHolder(map(room, showLastMessage)))
lastType = room.chatRoom.type
}
return list
......@@ -62,76 +77,71 @@ class RoomUiModelMapper(
return list
}
private fun mapUser(user: User): RoomUiModel {
return with(user) {
val name = mapName(user.username!!, user.name)
val status = user.status
val avatar = serverUrl.avatarUrl(user.username!!)
val username = user.username!!
private fun mapUser(user: User): RoomUiModel = with(user) {
val name = mapName(user.username!!, user.name)
val status = user.status
val avatar = serverUrl.avatarUrl(user.username!!)
val username = user.username!!
RoomUiModel(
RoomUiModel(
id = user.id,
name = name,
type = roomTypeOf(RoomType.DIRECT_MESSAGE),
avatar = avatar,
status = status,
username = username
)
}
)
}
private fun mapRoom(room: Room, showLastMessage: Boolean = true): RoomUiModel {
return with(room) {
RoomUiModel(
private fun mapRoom(room: Room, showLastMessage: Boolean = true): RoomUiModel = with(room) {
RoomUiModel(
id = id,
name = name!!,
type = type,
avatar = serverUrl.avatarUrl(name!!, isGroupOrChannel = true),
lastMessage = if (showLastMessage) {
mapLastMessage(
lastMessage?.sender?.id, lastMessage?.sender?.username,
lastMessage?.sender?.name, lastMessage?.message,
isDirectMessage = type is RoomType.DirectMessage
lastMessage?.sender?.id, lastMessage?.sender?.username,
lastMessage?.sender?.name, lastMessage?.message,
isDirectMessage = type is RoomType.DirectMessage
)
} else {
null
},
muted = muted.orEmpty(),
writable = isChannelWritable(muted)
)
}
)
}
fun map(chatRoom: ChatRoom, showLastMessage: Boolean = true): RoomUiModel {
return with(chatRoom.chatRoom) {
val isUnread = alert || unread > 0
val type = roomTypeOf(type)
val status = chatRoom.status?.let { userStatusOf(it) }
val roomName = mapName(name, fullname)
val timestamp = mapDate(lastMessageTimestamp ?: updatedAt)
val avatar = if (type is RoomType.DirectMessage) {
serverUrl.avatarUrl(name)
} else {
serverUrl.avatarUrl(name, isGroupOrChannel = true)
}
val unread = mapUnread(unread)
val lastMessage = if (showLastMessage) {
mapLastMessage(
fun map(chatRoom: ChatRoom, showLastMessage: Boolean = true): RoomUiModel = with(chatRoom.chatRoom) {
val isUnread = alert || unread > 0
val type = roomTypeOf(type)
val status = chatRoom.status?.let { userStatusOf(it) }
val roomName = mapName(name, fullname)
val favorite = favorite
val timestamp = mapDate(lastMessageTimestamp ?: updatedAt)
val avatar = if (type is RoomType.DirectMessage) {
serverUrl.avatarUrl(name)
} else {
serverUrl.avatarUrl(name, isGroupOrChannel = true)
}
val unread = mapUnread(unread)
val lastMessage = if (showLastMessage) {
mapLastMessage(
lastMessageUserId,
chatRoom.lastMessageUserName,
chatRoom.lastMessageUserFullName,
lastMessageText,
type is RoomType.DirectMessage
)
} else {
null
}
val hasMentions = mapMentions(userMentions, groupMentions)
val open = open
val lastMessageMarkdown =
lastMessage?.let { Markwon.markdown(context, it.toString()).toString() }
)
} else {
null
}
val hasMentions = mapMentions(userMentions, groupMentions)
val open = open
val lastMessageMarkdown = lastMessage?.let { Markwon.markdown(context, it.toString()).toString() }
RoomUiModel(
RoomUiModel(
id = id,
name = roomName,
type = type,
......@@ -140,14 +150,14 @@ class RoomUiModelMapper(
date = timestamp,
unread = unread,
mentions = hasMentions,
favorite = favorite,
alert = isUnread,
lastMessage = lastMessageMarkdown,
status = status,
username = if (type is RoomType.DirectMessage) name else null,
muted = muted.orEmpty(),
writable = isChannelWritable(muted)
)
}
)
}
private fun isChannelWritable(muted: List<String>?): Boolean {
......@@ -155,14 +165,13 @@ class RoomUiModelMapper(
return canWriteToReadOnlyChannels || !muted.orEmpty().contains(currentUser?.username)
}
private fun roomType(type: String): String {
val resources = context.resources
return when (type) {
RoomType.CHANNEL -> resources.getString(R.string.header_channel)
RoomType.PRIVATE_GROUP -> resources.getString(R.string.header_private_groups)
RoomType.DIRECT_MESSAGE -> resources.getString(R.string.header_direct_messages)
RoomType.LIVECHAT -> resources.getString(R.string.header_live_chats)
else -> resources.getString(R.string.header_unknown)
private fun roomType(type: String): String = with(context.resources) {
when (type) {
RoomType.CHANNEL -> getString(R.string.header_channel)
RoomType.PRIVATE_GROUP -> getString(R.string.header_private_groups)
RoomType.DIRECT_MESSAGE -> getString(R.string.header_direct_messages)
RoomType.LIVECHAT -> getString(R.string.header_live_chats)
else -> getString(R.string.header_unknown)
}
}
......@@ -195,13 +204,11 @@ class RoomUiModelMapper(
}
}
private fun mapUnread(unread: Long): String? {
return when (unread) {
0L -> null
in 1..99 -> unread.toString()
else -> context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
private fun mapUnread(unread: Long): String? = when (unread) {
0L -> null
in 1..99 -> unread.toString()
else -> context.getString(R.string.msg_more_than_ninety_nine_unread_messages)
}
}
private fun mapMentions(userMentions: Long?, groupMentions: Long?): Boolean {
......
......@@ -8,6 +8,7 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.util.extension.setTextViewAppearance
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.UserStatus
import kotlinx.android.synthetic.main.item_chat.view.*
......@@ -16,12 +17,12 @@ import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit) :
ViewHolder<RoomItemHolder>(itemView) {
private val resources: Resources = itemView.resources
private val channelIcon: Drawable = resources.getDrawable(R.drawable.ic_hashtag_12dp)
private val groupIcon: Drawable = resources.getDrawable(R.drawable.ic_lock_12_dp)
private val onlineIcon: Drawable = resources.getDrawable(R.drawable.ic_status_online_12dp)
private val awayIcon: Drawable = resources.getDrawable(R.drawable.ic_status_away_12dp)
private val busyIcon: Drawable = resources.getDrawable(R.drawable.ic_status_busy_12dp)
private val offlineIcon: Drawable = resources.getDrawable(R.drawable.ic_status_invisible_12dp)
private val channelIcon: Drawable = resources.getDrawable(R.drawable.ic_hashtag_12dp, null)
private val groupIcon: Drawable = resources.getDrawable(R.drawable.ic_lock_12_dp, null)
private val onlineIcon: Drawable = resources.getDrawable(R.drawable.ic_status_online_12dp, null)
private val awayIcon: Drawable = resources.getDrawable(R.drawable.ic_status_away_12dp, null)
private val busyIcon: Drawable = resources.getDrawable(R.drawable.ic_status_busy_12dp, null)
private val offlineIcon: Drawable = resources.getDrawable(R.drawable.ic_status_invisible_12dp, null)
override fun bindViews(data: RoomItemHolder) {
val room = data.data
......@@ -53,14 +54,14 @@ class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit
if (room.unread == null) text_total_unread_messages.text = "!"
if (room.unread != null) text_total_unread_messages.text = room.unread
if (room.mentions) text_total_unread_messages.text = "@${room.unread}"
text_chat_name.setTextAppearance(context, R.style.ChatList_ChatName_Unread_TextView)
text_timestamp.setTextAppearance(context, R.style.ChatList_Timestamp_Unread_TextView)
text_last_message.setTextAppearance(context, R.style.ChatList_LastMessage_Unread_TextView)
text_chat_name.setTextViewAppearance(context, R.style.ChatList_ChatName_Unread_TextView)
text_timestamp.setTextViewAppearance(context, R.style.ChatList_Timestamp_Unread_TextView)
text_last_message.setTextViewAppearance(context, R.style.ChatList_LastMessage_Unread_TextView)
text_total_unread_messages.isVisible = true
} else {
text_chat_name.setTextAppearance(context, R.style.ChatList_ChatName_TextView)
text_timestamp.setTextAppearance(context, R.style.ChatList_Timestamp_TextView)
text_last_message.setTextAppearance(context, R.style.ChatList_LastMessage_TextView)
text_chat_name.setTextViewAppearance(context, R.style.ChatList_ChatName_TextView)
text_timestamp.setTextViewAppearance(context, R.style.ChatList_Timestamp_TextView)
text_last_message.setTextViewAppearance(context, R.style.ChatList_LastMessage_TextView)
text_total_unread_messages.isInvisible = true
}
......@@ -68,20 +69,16 @@ class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit
}
}
private fun getRoomDrawable(type: RoomType): Drawable? {
return when (type) {
is RoomType.Channel -> channelIcon
is RoomType.PrivateGroup -> groupIcon
else -> null
}
private fun getRoomDrawable(type: RoomType): Drawable? = when (type) {
is RoomType.Channel -> channelIcon
is RoomType.PrivateGroup -> groupIcon
else -> null
}
private fun getStatusDrawable(status: UserStatus): Drawable {
return when (status) {
is UserStatus.Online -> onlineIcon
is UserStatus.Away -> awayIcon
is UserStatus.Busy -> busyIcon
else -> offlineIcon
}
private fun getStatusDrawable(status: UserStatus): Drawable = when (status) {
is UserStatus.Online -> onlineIcon
is UserStatus.Away -> awayIcon
is UserStatus.Busy -> busyIcon
else -> offlineIcon
}
}
\ No newline at end of file
......@@ -19,22 +19,20 @@ class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) :
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> {
return when (viewType) {
VIEW_TYPE_ROOM -> {
val view = parent.inflate(R.layout.item_chat)
RoomViewHolder(view, listener)
}
VIEW_TYPE_HEADER -> {
val view = parent.inflate(R.layout.item_chatroom_header)
HeaderViewHolder(view)
}
VIEW_TYPE_LOADING -> {
val view = parent.inflate(R.layout.item_loading)
LoadingViewHolder(view)
}
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> = when (viewType) {
VIEW_TYPE_ROOM -> {
val view = parent.inflate(R.layout.item_chat)
RoomViewHolder(view, listener)
}
VIEW_TYPE_HEADER -> {
val view = parent.inflate(R.layout.item_chatroom_header)
HeaderViewHolder(view)
}
VIEW_TYPE_LOADING -> {
val view = parent.inflate(R.layout.item_loading)
LoadingViewHolder(view)
}
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
}
override fun getItemCount() = values.size
......@@ -49,13 +47,11 @@ class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) :
}
}
override fun getItemViewType(position: Int): Int {
return when (values[position]) {
is RoomItemHolder -> VIEW_TYPE_ROOM
is HeaderItemHolder -> VIEW_TYPE_HEADER
is LoadingItemHolder -> VIEW_TYPE_LOADING
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
}
override fun getItemViewType(position: Int): Int = when (values[position]) {
is RoomItemHolder -> VIEW_TYPE_ROOM
is HeaderItemHolder -> VIEW_TYPE_HEADER
is LoadingItemHolder -> VIEW_TYPE_LOADING
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
}
override fun onBindViewHolder(holder: ViewHolder<*>, position: Int) {
......
......@@ -12,6 +12,7 @@ data class RoomUiModel(
val date: CharSequence? = null,
val unread: String? = null,
val alert: Boolean = false,
val favorite: Boolean? = false,
val mentions: Boolean = false,
val lastMessage: CharSequence? = null,
val status: UserStatus? = null,
......
......@@ -2,6 +2,7 @@ package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.R
import chat.rocket.android.chatrooms.adapter.model.RoomUiModel
import chat.rocket.android.chatrooms.domain.FetchChatRoomsInteractor
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.model.ChatRoomEntity
......@@ -22,11 +23,12 @@ import chat.rocket.common.model.roomTypeOf
import chat.rocket.core.internal.realtime.createDirectMessage
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.show
import kotlinx.coroutines.experimental.withTimeout
import kotlinx.coroutines.withTimeout
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Named
import kotlin.coroutines.experimental.suspendCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ChatRoomsPresenter @Inject constructor(
private val view: ChatRoomsView,
......@@ -116,6 +118,7 @@ class ChatRoomsPresenter @Inject constructor(
retryIO("createDirectMessage($name)") {
withTimeout(10000) {
createDirectMessage(name)
FetchChatRoomsInteractor(client, dbManager).refreshChatRooms()
}
}
val fromTo = mutableListOf(myself.id, id).apply {
......
package chat.rocket.android.chatrooms.ui
import android.app.AlertDialog
import androidx.appcompat.app.AlertDialog
import android.app.ProgressDialog
import android.os.Bundle
import android.os.Handler
......@@ -36,7 +36,6 @@ import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.util.extension.onQueryTextListener
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.ifNotNullNorEmpty
import chat.rocket.android.util.extensions.ifNotNullNotEmpty
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast
......@@ -69,11 +68,9 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
private var progressDialog: ProgressDialog? = null
companion object {
fun newInstance(chatRoomId: String? = null): ChatRoomsFragment {
return ChatRoomsFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
fun newInstance(chatRoomId: String? = null): ChatRoomsFragment = ChatRoomsFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
}
}
......@@ -82,9 +79,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
setHasOptionsMenu(true)
val bundle = arguments
if (bundle != null) {
chatRoomId = bundle.getString(BUNDLE_CHAT_ROOM_ID)
arguments?.run {
chatRoomId = getString(BUNDLE_CHAT_ROOM_ID)
chatRoomId.ifNotNullNotEmpty { roomId ->
presenter.loadChatRoom(roomId)
chatRoomId = null
......@@ -129,12 +125,14 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
)
)
recycler_view.itemAnimator = DefaultItemAnimator()
recycler_view.adapter = adapter
viewModel.getChatRooms().observe(viewLifecycleOwner, Observer { rooms ->
rooms?.let {
Timber.d("Got items: $it")
adapter.values = it
if (recycler_view.adapter != adapter) {
recycler_view.adapter = adapter
}
if (rooms.isNotEmpty()) {
text_no_data_to_display.isVisible = false
}
......@@ -236,14 +234,16 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
)
}
AlertDialog.Builder(context)
.setTitle(R.string.dialog_sort_title)
.setView(dialogLayout)
.setPositiveButton(R.string.msg_sort) { dialog, _ ->
invalidateQueryOnSearch()
updateSort()
dialog.dismiss()
}.show()
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.dialog_sort_title)
.setView(dialogLayout)
.setPositiveButton(R.string.msg_sort) { dialog, _ ->
invalidateQueryOnSearch()
updateSort()
dialog.dismiss()
}.show()
}
}
}
return super.onOptionsItemSelected(item)
......@@ -318,21 +318,20 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
ui {
text_connection_status.fadeIn()
handler.removeCallbacks(dismissStatus)
when (state) {
text_connection_status.text = when (state) {
is State.Connected -> {
text_connection_status.text = getString(R.string.status_connected)
handler.postDelayed(dismissStatus, 2000)
getString(R.string.status_connected)
}
is State.Disconnected -> getString(R.string.status_disconnected)
is State.Connecting -> getString(R.string.status_connecting)
is State.Authenticating -> getString(R.string.status_authenticating)
is State.Disconnecting -> getString(R.string.status_disconnecting)
is State.Waiting -> getString(R.string.status_waiting, state.seconds)
else -> {
handler.postDelayed(dismissStatus, 500)
""
}
is State.Disconnected -> text_connection_status.text =
getString(R.string.status_disconnected)
is State.Connecting -> text_connection_status.text =
getString(R.string.status_connecting)
is State.Authenticating -> text_connection_status.text =
getString(R.string.status_authenticating)
is State.Disconnecting -> text_connection_status.text =
getString(R.string.status_disconnecting)
is State.Waiting -> text_connection_status.text =
getString(R.string.status_waiting, state.seconds)
}
}
}
......
......@@ -20,16 +20,16 @@ import chat.rocket.core.model.SpotlightResult
import com.shopify.livedataktx.distinct
import com.shopify.livedataktx.map
import com.shopify.livedataktx.nonNull
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.isActive
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.lang.IllegalArgumentException
import kotlin.coroutines.experimental.coroutineContext
import kotlin.coroutines.coroutineContext
class ChatRoomsViewModel(
private val connectionManager: ConnectionManager,
......@@ -107,7 +107,7 @@ class ChatRoomsViewModel(
}
private fun fetchRooms() {
launch {
GlobalScope.launch {
setLoadingState(LoadingState.Loading(repository.count()))
try {
interactor.refreshChatRooms()
......@@ -125,7 +125,7 @@ class ChatRoomsViewModel(
}
private suspend fun setLoadingState(state: LoadingState) {
withContext(UI) {
withContext(Dispatchers.Main) {
loadingState.value = state
}
}
......
......@@ -45,9 +45,7 @@ class CreateChannelFragment : Fragment(), CreateChannelView, ActionMode.Callback
lateinit var analyticsManager: AnalyticsManager
private var actionMode: ActionMode? = null
private val adapter: MembersAdapter = MembersAdapter {
if (it.username != null) {
processSelectedMember(it.username)
}
it.username?.run { processSelectedMember(this) }
}
private val compositeDisposable = CompositeDisposable()
private var channelType: String = RoomType.CHANNEL
......@@ -294,8 +292,8 @@ class CreateChannelFragment : Fragment(), CreateChannelView, ActionMode.Callback
private fun addChip(chipText: String) {
val chip = Chip(context)
chip.chipText = chipText
chip.isCloseIconEnabled = true
chip.text = chipText
chip.isCloseIconVisible = true
chip.setChipBackgroundColorResource(R.color.icon_grey)
setupChipOnCloseIconClickListener(chip)
chip_group_member.addView(chip)
......@@ -304,7 +302,7 @@ class CreateChannelFragment : Fragment(), CreateChannelView, ActionMode.Callback
private fun setupChipOnCloseIconClickListener(chip: Chip) {
chip.setOnCloseIconClickListener {
removeChip(it)
removeMember((it as Chip).chipText.toString())
removeMember((it as Chip).text.toString())
// whenever we remove a chip we should process the chip group visibility.
processChipGroupVisibility()
}
......
......@@ -37,6 +37,8 @@ import chat.rocket.android.settings.di.SettingsFragmentProvider
import chat.rocket.android.settings.password.di.PasswordFragmentProvider
import chat.rocket.android.settings.password.ui.PasswordActivity
import chat.rocket.android.userdetails.di.UserDetailsFragmentProvider
import chat.rocket.android.videoconference.di.VideoConferenceModule
import chat.rocket.android.videoconference.ui.VideoConferenceActivity
import chat.rocket.android.webview.adminpanel.di.AdminPanelWebViewFragmentProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -103,4 +105,8 @@ abstract class ActivityBuilder {
@PerActivity
@ContributesAndroidInjector(modules = [DrawModule::class])
abstract fun bindDrawingActivity(): DrawingActivity
@PerActivity
@ContributesAndroidInjector(modules = [VideoConferenceModule::class])
abstract fun bindVideoConferenceActivity(): VideoConferenceActivity
}
......@@ -63,7 +63,6 @@ import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger
import chat.rocket.common.util.NoOpLogger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.internal.AttachmentAdapterFactory
......
......@@ -13,7 +13,6 @@ import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.internal.ISO8601Date
import chat.rocket.common.model.TimestampAdapter
import chat.rocket.common.util.CalendarISO8601Converter
import chat.rocket.common.util.Logger
import chat.rocket.common.util.NoOpLogger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.internal.AttachmentAdapterFactory
......
......@@ -115,12 +115,12 @@ fun Attachment.asEntity(msgId: String, context: Context): List<BaseMessageEntity
val text = mapAttachmentText(text, attachments?.firstOrNull(), context)
val entity = AttachmentEntity(
list.add(AttachmentEntity(
_id = attachmentId,
messageId = msgId,
title = title,
type = type,
description = description,
description = description,
text = text,
titleLink = titleLink,
titleLinkDownload = titleLinkDownload.orFalse(),
......@@ -144,16 +144,14 @@ fun Attachment.asEntity(msgId: String, context: Context): List<BaseMessageEntity
buttonAlignment = buttonAlignment,
hasActions = actions?.isNotEmpty() == true,
hasFields = fields?.isNotEmpty() == true
)
list.add(entity)
))
fields?.forEach { field ->
val entity = AttachmentFieldEntity(
list.add(AttachmentFieldEntity(
attachmentId = attachmentId,
title = field.title,
value = field.value
)
list.add(entity)
))
}
actions?.forEach { action ->
......@@ -175,18 +173,14 @@ fun Attachment.asEntity(msgId: String, context: Context): List<BaseMessageEntity
return list
}
fun mapAttachmentText(text: String?, attachment: Attachment?, context: Context): String? {
return if (attachment != null) {
when {
attachment.imageUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_photo)
attachment.videoUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_video)
attachment.audioUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_audio)
attachment.titleLink.isNotNullNorEmpty() &&
attachment.type?.contentEquals("file") == true ->
context.getString(R.string.msg_preview_file)
else -> text
}
} else {
text
fun mapAttachmentText(text: String?, attachment: Attachment?, context: Context): String? = attachment?.run {
when {
imageUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_photo)
videoUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_video)
audioUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_audio)
titleLink.isNotNullNorEmpty() &&
type?.contentEquals("file") == true ->
context.getString(R.string.msg_preview_file)
else -> text
}
}
} ?: text
\ No newline at end of file
......@@ -36,8 +36,7 @@ class FavoriteMessagesPresenter @Inject constructor(
try {
view.showLoading()
dbManager.getRoom(roomId)?.let {
val favoriteMessages =
client.getFavoriteMessages(roomId, roomTypeOf(it.chatRoom.type), offset)
val favoriteMessages = client.getFavoriteMessages(roomId, roomTypeOf(it.chatRoom.type), offset)
val messageList = mapper.map(favoriteMessages.result, asNotReversed = true)
view.showFavoriteMessages(messageList)
offset += 1 * 30
......
......@@ -25,11 +25,9 @@ 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)
}
fun newInstance(chatRoomId: String): Fragment = FavoriteMessagesFragment().apply {
arguments = Bundle(1).apply {
putString(INTENT_CHAT_ROOM_ID, chatRoomId)
}
}
......@@ -48,12 +46,9 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
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" }
}
arguments?.run {
chatRoomId = getString(INTENT_CHAT_ROOM_ID, "")
} ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
}
override fun onCreateView(
......@@ -115,6 +110,6 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
}
private fun setupToolbar() {
(activity as ChatRoomActivity).showToolbarTitle(getString(R.string.title_favorite_messages))
(activity as ChatRoomActivity).setupToolbarTitle(getString(R.string.title_favorite_messages))
}
}
\ No newline at end of file
......@@ -30,11 +30,9 @@ 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)
}
fun newInstance(chatRoomId: String): Fragment = FilesFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
}
......@@ -55,12 +53,9 @@ class FilesFragment : Fragment(), FilesView {
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" }
}
arguments?.run {
chatRoomId = getString(BUNDLE_CHAT_ROOM_ID, "")
} ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
}
override fun onCreateView(
......@@ -152,7 +147,7 @@ class FilesFragment : Fragment(), FilesView {
}
private fun setupToolbar(totalFiles: Long) {
(activity as ChatRoomActivity).showToolbarTitle(
(activity as ChatRoomActivity).setupToolbarTitle(
(getString(
R.string.title_files_total,
totalFiles
......
......@@ -11,8 +11,10 @@ object AndroidPermissionsHelper {
const val WRITE_EXTERNAL_STORAGE_CODE = 1
fun checkPermission(context: Context, permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) ==
PackageManager.PERMISSION_GRANTED
return ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
fun requestPermission(context: Activity, permission: String, requestCode: Int) {
......
......@@ -53,8 +53,8 @@ object ImageHelper {
)
val toolbar = Toolbar(context).also {
it.inflateMenu(R.menu.image_actions)
it.setOnMenuItemClickListener {
return@setOnMenuItemClickListener when (it.itemId) {
it.setOnMenuItemClickListener { view ->
return@setOnMenuItemClickListener when (view.itemId) {
R.id.action_save_image -> saveImage(context)
else -> true
}
......@@ -62,20 +62,24 @@ object ImageHelper {
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 titleTextView = TextView(context).also { tv ->
with(tv) {
text = imageName
setTextColor(Color.WHITE)
setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize.toFloat())
ellipsize = TextUtils.TruncateAt.END
setSingleLine()
typeface = Typeface.DEFAULT_BOLD
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 backArrowView = ImageView(context).also { imgView ->
with(imgView) {
setImageResource(R.drawable.ic_arrow_back_white_24dp)
setOnClickListener { imageViewer?.onDismiss() }
setPadding(0, pad, pad, pad)
}
}
val layoutParams = AppBarLayout.LayoutParams(
......@@ -88,14 +92,16 @@ object ImageHelper {
}
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
with(it) {
layoutParams = lparams
setBackgroundColor(Color.BLACK)
addView(
toolbar, AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
)
)
}
}
val builder = ImageViewer.createPipelineDraweeControllerBuilder()
......
package chat.rocket.android.helper
object JitsiHelper {
/**
* Returns the for the Jitsi video conferencing URL.
*
* @param isSecureProtocol True if using SSL, false otherwise - from the public settings.
* @param domain The Jitsi domain - from public settings.
* @param prefix The Jitsi prefix - from public settings.
* @param uniqueIdentifier The server unique identifier - from public settings.
* @param chatRoomId The ChatRoom ID where the video conferencing was started.
*/
fun getJitsiUrl(
isSecureProtocol: Boolean,
domain: String?,
prefix: String?,
uniqueIdentifier: String?,
chatRoomId: String?
): String =
getJitsiProtocol(isSecureProtocol) +
domain +
"/" +
prefix +
uniqueIdentifier +
chatRoomId
private fun getJitsiProtocol(isSecureProtocol: Boolean) =
if (isSecureProtocol) "https://" else "http://"
}
\ No newline at end of file
......@@ -8,7 +8,7 @@ import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.ui.MainActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class MainModule {
......
......@@ -37,7 +37,7 @@ import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.getCustomEmojis
import chat.rocket.core.internal.rest.me
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.channels.Channel
import timber.log.Timber
import javax.inject.Inject
......@@ -210,7 +210,7 @@ class MainPresenter @Inject constructor(
}
}
private suspend fun saveAccount(uiModel: NavHeaderUiModel) {
private fun saveAccount(uiModel: NavHeaderUiModel) {
val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it)
}
......
......@@ -2,7 +2,7 @@ package chat.rocket.android.main.ui
import DrawableHelper
import android.app.Activity
import android.app.AlertDialog
import androidx.appcompat.app.AlertDialog
import android.app.ProgressDialog
import android.os.Bundle
import androidx.annotation.IdRes
......@@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.main.adapter.AccountsAdapter
import chat.rocket.android.main.adapter.Selector
import chat.rocket.android.main.presentation.MainPresenter
......@@ -37,6 +38,9 @@ import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.nav_header.view.*
import javax.inject.Inject
import android.app.NotificationManager
import android.content.Context
private const val CURRENT_STATE = "current_state"
......@@ -89,6 +93,9 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
presenter.toChatList(chatRoomId)
isFragmentAdded = true
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
notificationManager.cancelAll()
}
override fun onDestroy() {
......@@ -98,6 +105,21 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
}
}
override fun onBackPressed() {
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
closeDrawer()
} else {
supportFragmentManager.findFragmentById(R.id.fragment_container)?.let {
if (it !is ChatRoomsFragment && supportFragmentManager.backStackEntryCount == 0) {
presenter.toChatList(chatRoomId)
setCheckedNavDrawerItem(R.id.menu_action_chats)
} else {
super.onBackPressed()
}
}
}
}
override fun activityInjector(): AndroidInjector<Activity> = activityDispatchingAndroidInjector
override fun supportFragmentInjector(): AndroidInjector<Fragment> =
......@@ -180,7 +202,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
BuildConfig.RECOMMENDED_SERVER_VERSION
)
)
.setPositiveButton(R.string.msg_ok, null)
.setPositiveButton(android.R.string.ok, null)
.create()
.show()
}
......@@ -194,7 +216,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
)
)
.setOnDismissListener { presenter.logout() }
.setPositiveButton(R.string.msg_ok, null)
.setPositiveButton(android.R.string.ok, null)
.create()
.show()
}
......
......@@ -10,8 +10,9 @@ import chat.rocket.android.util.extensions.inflate
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_member.view.*
class MembersAdapter(private val listener: (MemberUiModel) -> Unit) :
RecyclerView.Adapter<MembersAdapter.ViewHolder>() {
class MembersAdapter(
private val listener: (MemberUiModel) -> Unit
) : RecyclerView.Adapter<MembersAdapter.ViewHolder>() {
private var dataSet: List<MemberUiModel> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MembersAdapter.ViewHolder =
......@@ -43,7 +44,8 @@ class MembersAdapter(private val listener: (MemberUiModel) -> Unit) :
fun bind(memberUiModel: MemberUiModel, listener: (MemberUiModel) -> Unit) = with(itemView) {
image_avatar.setImageURI(memberUiModel.avatarUri)
text_member.content = memberUiModel.displayName
text_member.setCompoundDrawablesRelativeWithIntrinsicBounds(DrawableHelper.getUserStatusDrawable(memberUiModel.status, context), null, null, null)
text_member.setCompoundDrawablesRelativeWithIntrinsicBounds(
DrawableHelper.getUserStatusDrawable(memberUiModel.status, context), null, null, null)
setOnClickListener { listener(memberUiModel) }
}
}
......
......@@ -3,6 +3,7 @@ package chat.rocket.android.members.presentation
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.members.uimodel.MemberUiModel
import chat.rocket.android.members.uimodel.MemberUiModelMapper
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
......@@ -23,7 +24,8 @@ class MembersPresenter @Inject constructor(
@Named("currentServer") private val currentServer: String,
private val strategy: CancelStrategy,
private val mapper: MemberUiModelMapper,
val factory: RocketChatClientFactory
val factory: RocketChatClientFactory,
private val userHelper: UserHelper
) {
private val client: RocketChatClient = factory.create(currentServer)
private var offset: Long = 0
......@@ -39,7 +41,7 @@ class MembersPresenter @Inject constructor(
view.showLoading()
dbManager.getRoom(roomId)?.let {
val members =
client.getMembers(roomId, roomTypeOf(it.chatRoom.type), offset, 60)
client.getMembers(roomId, roomTypeOf(it.chatRoom.type), offset, 60)
val memberUiModels = mapper.mapToUiModelList(members.result)
view.showMembers(memberUiModels, members.total)
offset += 1 * 60L
......@@ -59,6 +61,10 @@ class MembersPresenter @Inject constructor(
}
fun toMemberDetails(memberUiModel: MemberUiModel) {
navigator.toMemberDetails(memberUiModel.userId)
with(memberUiModel) {
if (userId != userHelper.user()?.id) {
navigator.toMemberDetails(userId)
}
}
}
}
......@@ -27,11 +27,9 @@ import kotlinx.android.synthetic.main.app_bar_chat_room.*
import kotlinx.android.synthetic.main.fragment_members.*
import javax.inject.Inject
fun newInstance(chatRoomId: String): Fragment {
return MembersFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
fun newInstance(chatRoomId: String): Fragment = MembersFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
}
......@@ -52,12 +50,9 @@ class MembersFragment : Fragment(), MembersView {
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" }
}
arguments?.run {
chatRoomId = getString(BUNDLE_CHAT_ROOM_ID, "")
} ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
}
override fun onCreateView(
......@@ -80,7 +75,7 @@ class MembersFragment : Fragment(), MembersView {
setupToolbar(total)
if (adapter.itemCount == 0) {
adapter.prependData(dataSet)
if (dataSet.size >= 59) { // TODO Check why the API retorns the specified count -1
if (dataSet.size >= 59) { // TODO Check why the API returns the specified count -1
recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
......@@ -136,14 +131,9 @@ class MembersFragment : Fragment(), MembersView {
private fun setupToolbar(totalMembers: Long? = null) {
with((activity as ChatRoomActivity)) {
if (totalMembers != null) {
showToolbarTitle(
(getString(
R.string.title_counted_members,
totalMembers
))
)
setupToolbarTitle((getString(R.string.title_counted_members, totalMembers)))
} else {
showToolbarTitle((getString(R.string.title_members)))
setupToolbarTitle((getString(R.string.title_members)))
}
this.clearLightStatusBar()
toolbar.isVisible = true
......
......@@ -25,11 +25,9 @@ import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_mentions.*
import javax.inject.Inject
fun newInstance(chatRoomId: String): Fragment {
return MentionsFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
fun newInstance(chatRoomId: String): Fragment = MentionsFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
}
......@@ -48,12 +46,9 @@ class MentionsFragment : Fragment(), MentionsView {
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" }
}
arguments?.run {
chatRoomId = getString(BUNDLE_CHAT_ROOM_ID, "")
} ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
}
override fun onCreateView(
......@@ -121,6 +116,6 @@ class MentionsFragment : Fragment(), MentionsView {
}
private fun setupToolbar() {
(activity as ChatRoomActivity).showToolbarTitle((getString(R.string.msg_mentions)))
(activity as ChatRoomActivity).setupToolbarTitle((getString(R.string.msg_mentions)))
}
}
\ No newline at end of file
......@@ -25,11 +25,9 @@ import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_pinned_messages.*
import javax.inject.Inject
fun newInstance(chatRoomId: String): Fragment {
return PinnedMessagesFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
fun newInstance(chatRoomId: String): Fragment = PinnedMessagesFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
}
}
......@@ -48,12 +46,9 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
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" }
}
arguments?.run {
chatRoomId = getString(BUNDLE_CHAT_ROOM_ID, "")
} ?: requireNotNull(arguments) { "no arguments supplied when the fragment was instantiated" }
}
override fun onCreateView(
......@@ -121,6 +116,6 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
}
private fun setupToolbar() {
(activity as ChatRoomActivity).showToolbarTitle((getString(R.string.title_pinned_messages)))
(activity as ChatRoomActivity).setupToolbarTitle((getString(R.string.title_pinned_messages)))
}
}
\ No newline at end of file
......@@ -38,13 +38,17 @@ class PreferencesFragment : Fragment(), PreferencesView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
setupListeners()
presenter.loadAnalyticsTrackingInformation()
analyticsManager.logScreenView(ScreenViewEvent.Preferences)
}
override fun onResume() {
setupToolbar()
super.onResume()
}
override fun setupAnalyticsTrackingView(isAnalyticsTrackingEnabled: Boolean) {
if (BuildConfig.FLAVOR == "foss") {
switch_analytics_tracking.isChecked = false
......
......@@ -27,8 +27,8 @@ import chat.rocket.core.internal.rest.deleteOwnAccount
import chat.rocket.core.internal.rest.resetAvatar
import chat.rocket.core.internal.rest.setAvatar
import chat.rocket.core.internal.rest.updateProfile
import kotlinx.coroutines.experimental.DefaultDispatcher
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
import javax.inject.Inject
......@@ -82,7 +82,7 @@ class ProfilePresenter @Inject constructor(
view.showLoading()
try {
user?.id?.let { id ->
retryIO { client.updateProfile(id, email, name, username) }
retryIO { client.updateProfile(userId = id, email = email, name = name, username = username) }
view.showProfileUpdateSuccessfullyMessage()
view.showProfile(
serverUrl.avatarUrl(user.username ?: ""),
......@@ -115,7 +115,7 @@ class ProfilePresenter @Inject constructor(
uriInteractor.getInputStream(uri)
}
}
user?.username?.let { view.reloadUserAvatar(it) }
user?.username?.let { view.reloadUserAvatar(serverUrl.avatarUrl(it)) }
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
......@@ -143,7 +143,7 @@ class ProfilePresenter @Inject constructor(
}
}
user?.username?.let { view.reloadUserAvatar(it) }
user?.username?.let { view.reloadUserAvatar(serverUrl.avatarUrl(it)) }
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
......@@ -163,7 +163,7 @@ class ProfilePresenter @Inject constructor(
user?.id?.let { id ->
retryIO { client.resetAvatar(id) }
}
user?.username?.let { view.reloadUserAvatar(it) }
user?.username?.let { view.reloadUserAvatar(serverUrl.avatarUrl(it)) }
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
......@@ -180,7 +180,7 @@ class ProfilePresenter @Inject constructor(
launchUI(strategy) {
view.showLoading()
try {
withContext(DefaultDispatcher) {
withContext(Dispatchers.Default) {
// REMARK: Backend API is only working with a lowercase hash.
// https://github.com/RocketChat/Rocket.Chat/issues/12573
retryIO { client.deleteOwnAccount(password.gethash().toHex().toLowerCase()) }
......@@ -198,4 +198,4 @@ class ProfilePresenter @Inject constructor(
}
}
}
}
\ No newline at end of file
}
......@@ -2,7 +2,7 @@ package chat.rocket.android.profile.ui
import DrawableHelper
import android.app.Activity
import android.app.AlertDialog
import androidx.appcompat.app.AlertDialog
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
......@@ -94,11 +94,13 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (resultData != null && resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_FOR_PERFORM_SAF) {
presenter.updateAvatar(resultData.data)
} else if (requestCode == REQUEST_CODE_FOR_PERFORM_CAMERA) {
presenter.preparePhotoAndUpdateAvatar(resultData.extras["data"] as Bitmap)
resultData?.run {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_FOR_PERFORM_SAF) {
data?.let { presenter.updateAvatar(it) }
} else if (requestCode == REQUEST_CODE_FOR_PERFORM_CAMERA) {
extras?.get("data")?.let { presenter.preparePhotoAndUpdateAvatar(it as Bitmap) }
}
}
}
}
......@@ -203,8 +205,7 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
}
private fun setupToolbar() {
(activity as AppCompatActivity?)?.supportActionBar?.title =
getString(R.string.title_profile)
(activity as AppCompatActivity?)?.supportActionBar?.title = getString(R.string.title_profile)
}
private fun setupListeners() {
......@@ -293,17 +294,14 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
}
fun showDeleteAccountDialog() {
val passwordEditText = EditText(context)
passwordEditText.hint = getString(R.string.msg_password)
val builder = AlertDialog.Builder(context)
builder.setTitle(R.string.title_are_you_sure)
.setView(passwordEditText)
.setPositiveButton(R.string.action_delete_account) { _, _ ->
presenter.deleteAccount(passwordEditText.text.toString())
}
.setNegativeButton(android.R.string.no) { dialog, _ -> dialog.cancel() }
.create()
.show()
context?.let {
val passwordEText = EditText(context);
val mDialogView = LayoutInflater.from(it).inflate(R.layout.item_account_delete, null)
val mBuilder = AlertDialog.Builder(it)
mBuilder.setView(mDialogView).setPositiveButton(R.string.action_delete_account) { _, _ ->
presenter.deleteAccount(passwordEText.text.toString())
}.setNegativeButton(android.R.string.no) { dialog, _ -> dialog.cancel() }.create().show()
}
}
}
......@@ -11,8 +11,8 @@ import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.sendMessage
import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.*
import javax.inject.Inject
......@@ -36,7 +36,7 @@ class DirectReplyReceiver : BroadcastReceiver() {
if (ACTION_REPLY == intent.action) {
val message = intent.getParcelableExtra<PushMessage>(EXTRA_PUSH_MESSAGE)
message?.let {
launch(UI) {
MainScope().launch {
val notificationId = it.notificationId.toInt()
val hostname = it.info.host
try {
......
......@@ -12,13 +12,14 @@ import android.os.Build
import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
import android.text.Html
import android.text.Spanned
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.RemoteInput
import android.text.Html
import android.text.Spanned
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_LEGACY
import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.domain.GetAccountInteractor
......@@ -29,7 +30,7 @@ import chat.rocket.common.model.RoomType
import chat.rocket.common.model.roomTypeOf
import com.squareup.moshi.Json
import com.squareup.moshi.Moshi
import kotlinx.coroutines.experimental.runBlocking
import kotlinx.coroutines.runBlocking
import se.ansman.kotshi.JsonSerializable
import se.ansman.kotshi.KotshiConstructor
import timber.log.Timber
......@@ -303,7 +304,11 @@ class PushManager @Inject constructor(
// CharSequence extensions
private fun CharSequence.fromHtml(): Spanned {
return Html.fromHtml(this as String)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(this as String, FROM_HTML_MODE_LEGACY, null, null)
} else {
Html.fromHtml(this as String)
}
}
// NotificationCompat.Builder extensions
......@@ -383,12 +388,12 @@ data class PushMessage(
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString().orEmpty(),
parcel.readString().orEmpty(),
parcel.readParcelable(PushMessage::class.java.classLoader) ?: PushInfo.EMPTY,
parcel.readString(),
parcel.readString(),
parcel.readParcelable(PushMessage::class.java.classLoader),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString().orEmpty(),
parcel.readString(),
parcel.readString())
......@@ -433,9 +438,9 @@ data class PushInfo @KotshiConstructor constructor(
}
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
roomTypeOf(parcel.readString()),
parcel.readString().orEmpty(),
parcel.readString().orEmpty(),
roomTypeOf(parcel.readString().orEmpty()),
parcel.readString(),
parcel.readParcelable(PushInfo::class.java.classLoader))
......@@ -481,7 +486,7 @@ data class PushSender @KotshiConstructor constructor(
val name: String?
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString().orEmpty(),
parcel.readString(),
parcel.readString())
......
......@@ -3,13 +3,12 @@ package chat.rocket.android.server.di
import androidx.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.server.presentation.ChangeServerNavigator
import chat.rocket.android.server.presentation.ChangeServerView
import chat.rocket.android.server.ui.ChangeServerActivity
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.Job
@Module
class ChangeServerModule {
......
......@@ -3,6 +3,7 @@ package chat.rocket.android.server.domain
import chat.rocket.android.server.domain.model.Account
interface AccountsRepository {
fun save(account: Account)
fun load(): List<Account>
fun remove(serverUrl: String)
......
package chat.rocket.android.server.domain
import chat.rocket.core.model.ChatRoom
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class ChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
......@@ -23,15 +23,16 @@ class ChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsR
* @param name The name of chat room to look for or a chat room that contains this name.
* @return A list of ChatRoom objects with the given name.
*/
suspend fun getAllByName(url: String, name: String): List<ChatRoom> = withContext(CommonPool) {
val allChatRooms = repository.get(url)
if (name.isEmpty()) {
return@withContext allChatRooms
suspend fun getAllByName(url: String, name: String): List<ChatRoom> =
withContext(Dispatchers.IO) {
val allChatRooms = repository.get(url)
if (name.isEmpty()) {
return@withContext allChatRooms
}
return@withContext allChatRooms.filter {
it.name.contains(name, true)
}
}
return@withContext allChatRooms.filter {
it.name.contains(name, true)
}
}
/**
* Get a specific [ChatRoom] by its id.
......@@ -40,11 +41,12 @@ class ChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsR
* @param roomId The id of the room to get.
* @return The [ChatRoom] object or null if we couldn't find any.
*/
suspend fun getById(serverUrl: String, roomId: String): ChatRoom? = withContext(CommonPool) {
return@withContext repository.get(serverUrl).find {
it.id == roomId
suspend fun getById(serverUrl: String, roomId: String): ChatRoom? =
withContext(Dispatchers.IO) {
return@withContext repository.get(serverUrl).find {
it.id == roomId
}
}
}
/**
* Get a specific [ChatRoom] by its name.
......
......@@ -3,7 +3,8 @@ package chat.rocket.android.server.domain
import javax.inject.Inject
class GetAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun get(url: String) = repository.load().firstOrNull { account ->
fun get(url: String) = repository.load().firstOrNull { account ->
url == account.serverUrl
}
}
\ No newline at end of file
......@@ -3,8 +3,9 @@ package chat.rocket.android.server.domain
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.core.internal.rest.permissions
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
......@@ -17,7 +18,7 @@ class RefreshPermissionsInteractor @Inject constructor(
) {
fun refreshAsync(server: String) {
launch(CommonPool) {
GlobalScope.launch(Dispatchers.IO) {
try {
factory.create(server).let { client ->
val permissions = retryIO(
......
......@@ -3,9 +3,10 @@ package chat.rocket.android.server.domain
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.core.internal.rest.settings
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
......@@ -18,10 +19,11 @@ class RefreshSettingsInteractor @Inject constructor(
) {
private var settingsFilter = arrayOf(
UNIQUE_IDENTIFIER,
LDAP_ENABLE,
CAS_ENABLE,
CAS_LOGIN_URL,
ACCOUNT_REGISTRATION,
ACCOUNT_LOGIN_FORM,
ACCOUNT_PASSWORD_RESET,
......@@ -37,6 +39,12 @@ class RefreshSettingsInteractor @Inject constructor(
ACCOUNT_WORDPRESS,
ACCOUNT_WORDPRESS_URL,
JITSI_ENABLED,
JISTI_ENABLE_CHANNELS,
JITSI_SSL,
JITSI_DOMAIN,
JITSI_URL_ROOM_PREFIX,
SITE_URL,
SITE_NAME,
FAVICON_512,
......@@ -65,7 +73,7 @@ class RefreshSettingsInteractor @Inject constructor(
)
suspend fun refresh(server: String) {
withContext(CommonPool) {
withContext(Dispatchers.IO) {
factory.create(server).let { client ->
val settings = retryIO(
description = "settings",
......@@ -81,7 +89,7 @@ class RefreshSettingsInteractor @Inject constructor(
}
fun refreshAsync(server: String) {
launch(CommonPool) {
GlobalScope.launch(Dispatchers.IO) {
try {
refresh(server)
} catch (ex: Exception) {
......
......@@ -3,7 +3,8 @@ package chat.rocket.android.server.domain
import javax.inject.Inject
class RemoveAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun remove(serverUrl: String) {
fun remove(serverUrl: String) {
repository.remove(serverUrl)
}
}
\ No newline at end of file
......@@ -4,5 +4,5 @@ import chat.rocket.android.server.domain.model.Account
import javax.inject.Inject
class SaveAccountInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun save(account: Account) = repository.save(account)
fun save(account: Account) = repository.save(account)
}
\ No newline at end of file
......@@ -5,7 +5,9 @@ import chat.rocket.core.model.Value
typealias PublicSettings = Map<String, Value<Any>>
// Authentication methods.
const val UNIQUE_IDENTIFIER = "uniqueID"
// Authentication methods
const val LDAP_ENABLE = "LDAP_Enable"
const val CAS_ENABLE = "CAS_enabled"
const val CAS_LOGIN_URL = "CAS_login_url"
......@@ -24,6 +26,13 @@ const val ACCOUNT_GITLAB_URL = "API_Gitlab_URL"
const val ACCOUNT_WORDPRESS = "Accounts_OAuth_Wordpress"
const val ACCOUNT_WORDPRESS_URL = "API_Wordpress_URL"
// Video call
const val JITSI_ENABLED = "Jitsi_Enabled"
const val JISTI_ENABLE_CHANNELS = "Jisti_Enable_Channels"
const val JITSI_SSL = "Jitsi_SSL"
const val JITSI_DOMAIN = "Jitsi_Domain"
const val JITSI_URL_ROOM_PREFIX = "Jitsi_URL_Room_Prefix"
const val SITE_URL = "Site_Url"
const val SITE_NAME = "Site_Name"
const val FAVICON_196 = "Assets_favicon_192"
......@@ -54,10 +63,13 @@ const val MESSAGE_READ_RECEIPT_STORE_USERS = "Message_Read_Receipt_Store_Users"
* Extension functions for Public Settings.
*
* If you need to access a Setting, add a const val key above, add it to the filter on
* ServerPresenter.kt and a extension function to access it
* RefreshSettingsInteractor.kt and a extension function to access it.
*/
fun PublicSettings.isLdapAuthenticationEnabled(): Boolean = this[LDAP_ENABLE]?.value == true
fun PublicSettings.uniqueIdentifier(): String? = this[UNIQUE_IDENTIFIER]?.value as String?
// Authentication
fun PublicSettings.isLdapAuthenticationEnabled(): Boolean = this[LDAP_ENABLE]?.value == true
fun PublicSettings.isCasAuthenticationEnabled(): Boolean = this[CAS_ENABLE]?.value == true
fun PublicSettings.casLoginUrl(): String = this[CAS_LOGIN_URL]?.value.toString()
fun PublicSettings.isRegistrationEnabledForNewUsers(): Boolean = this[ACCOUNT_REGISTRATION]?.value == "Public"
......@@ -74,6 +86,13 @@ fun PublicSettings.gitlabUrl(): String? = this[ACCOUNT_GITLAB_URL]?.value as Str
fun PublicSettings.isWordpressAuthenticationEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun PublicSettings.wordpressUrl(): String? = this[ACCOUNT_WORDPRESS_URL]?.value as String?
// Video call
fun PublicSettings.isJitsiEnabled(): Boolean = this[JITSI_ENABLED]?.value == true
fun PublicSettings.isJitsiEnabledForChannels(): Boolean = this[JISTI_ENABLE_CHANNELS]?.value == true
fun PublicSettings.isJitsiSSL(): Boolean = this[JITSI_SSL]?.value == true
fun PublicSettings.jitsiDomain(): String? = this[JITSI_DOMAIN]?.value as String?
fun PublicSettings.jitsiPrefix(): String? = this[JITSI_URL_ROOM_PREFIX]?.value as String?
fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true
fun PublicSettings.useSpecialCharsOnRoom(): Boolean = this[ALLOW_ROOM_NAME_SPECIAL_CHARS]?.value == true
fun PublicSettings.faviconLarge(): String? = this[FAVICON_512]?.value as String?
......
......@@ -23,26 +23,31 @@ import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself
import chat.rocket.core.model.Room
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.channels.SendChannel
import kotlinx.coroutines.experimental.channels.actor
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.selects.select
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.actor
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.selects.select
import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.coroutines.experimental.CoroutineContext
import kotlin.coroutines.CoroutineContext
class ConnectionManager(
internal val client: RocketChatClient,
private val dbManager: DatabaseManager
) {
) : CoroutineScope {
private var connectJob : Job? = null
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO
val statusLiveData = MutableLiveData<State>()
private val statusChannelList = CopyOnWriteArrayList<Channel<State>>()
private val statusChannel = Channel<State>(Channel.CONFLATED)
private var connectJob: Job? = null
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val userDataChannels = ArrayList<Channel<Myself>>()
......@@ -60,7 +65,7 @@ class ConnectionManager(
private val messagesContext = newSingleThreadContext("messagesContext")
fun connect() {
if (connectJob?.isActive == true && (state !is State.Disconnected)) {
if (connectJob?.isActive == true && state !is State.Disconnected) {
Timber.d("Already connected, just returning...")
return
}
......@@ -78,32 +83,32 @@ class ConnectionManager(
when (status) {
is State.Connected -> {
dbManager.clearUsersStatus()
client.subscribeSubscriptions { _, id ->
Timber.d("Subscribed to subscriptions: $id")
subscriptionId = id
}
client.subscribeRooms { _, id ->
Timber.d("Subscribed to rooms: $id")
roomsId = id
}
client.subscribeUserData { _, id ->
Timber.d("Subscribed to the userData id: $id")
userDataId = id
}
client.subscribeActiveUsers { _, id ->
Timber.d("Subscribed to the activeUser id: $id")
activeUserId = id
}
resubscribeRooms()
temporaryStatus?.let { client.setTemporaryStatus(it) }
temporaryStatus?.let { status ->
client.setTemporaryStatus(status)
}
}
is State.Waiting -> {
Timber.d("Connection in: ${status.seconds}")
}
is State.Waiting -> Timber.d("Connection in: ${status.seconds}")
}
statusLiveData.postValue(status)
......@@ -116,8 +121,9 @@ class ConnectionManager(
}
var totalBatchedUsers = 0
val userActor = createBatchActor<User>(activeUsersContext, parent = connectJob,
maxSize = 500, maxTime = 1000) { users ->
val userActor = createBatchActor<User>(
activeUsersContext, parent = connectJob, maxSize = 500, maxTime = 1000
) { users ->
totalBatchedUsers += users.size
Timber.d("Processing Users batch: ${users.size} - $totalBatchedUsers")
......@@ -125,8 +131,9 @@ class ConnectionManager(
dbManager.processUsersBatch(users)
}
val roomsActor = createBatchActor<StreamMessage<BaseRoom>>(roomsContext, parent = connectJob,
maxSize = 10) { batch ->
val roomsActor = createBatchActor<StreamMessage<BaseRoom>>(
roomsContext, parent = connectJob, maxSize = 10
) { batch ->
Timber.d("processing Stream batch: ${batch.size} - $batch")
dbManager.processChatRoomsBatch(batch)
......@@ -135,16 +142,15 @@ class ConnectionManager(
if (it.type == Type.Updated) {
if (it.data is Room) {
val room = it.data as Room
roomsChannels[it.data.id]?.let { channel ->
channel.offer(room)
}
roomsChannels[it.data.id]?.offer(room)
}
}
}
}
val messagesActor = createBatchActor<Message>(messagesContext, parent = connectJob,
maxSize = 100, maxTime = 500) { messages ->
val messagesActor = createBatchActor<Message>(
messagesContext, parent = connectJob, maxSize = 100, maxTime = 500
) { messages ->
Timber.d("Processing Messages batch: ${messages.size}")
dbManager.processMessagesBatch(messages.distinctBy { it.id })
......@@ -157,7 +163,7 @@ class ConnectionManager(
}
// stream-notify-user - ${userId}/rooms-changed
launch(parent = connectJob) {
launch {
for (room in client.roomsChannel) {
Timber.d("GOT Room streamed")
roomsActor.send(room)
......@@ -170,7 +176,7 @@ class ConnectionManager(
}
// stream-notify-user - ${userId}/subscriptions-changed
launch(parent = connectJob) {
launch {
for (subscription in client.subscriptionsChannel) {
Timber.d("GOT Subscription streamed")
roomsActor.send(subscription)
......@@ -178,7 +184,7 @@ class ConnectionManager(
}
// stream-room-messages - $roomId
launch(parent = connectJob) {
launch {
for (message in client.messagesChannel) {
Timber.d("Received new Message for room ${message.roomId}")
messagesActor.send(message)
......@@ -186,7 +192,7 @@ class ConnectionManager(
}
// userData
launch(parent = connectJob) {
launch {
for (myself in client.userDataChannel) {
Timber.d("Got userData")
dbManager.updateSelfUser(myself)
......@@ -197,7 +203,7 @@ class ConnectionManager(
}
// activeUsers
launch(parent = connectJob) {
launch {
for (user in client.activeUsersChannel) {
userActor.send(user)
}
......@@ -231,7 +237,7 @@ class ConnectionManager(
}
private fun resubscribeRooms() {
roomMessagesChannels.toList().map { (roomId, channel) ->
roomMessagesChannels.toList().map { (roomId, _) ->
client.subscribeRoomMessages(roomId) { _, id ->
Timber.d("Subscribed to $roomId: $id")
subscriptionIdMap[roomId] = id
......@@ -286,16 +292,18 @@ class ConnectionManager(
}
}
private inline fun <T> createBatchActor(context: CoroutineContext = CommonPool,
parent: Job? = null,
maxSize: Int = 100,
maxTime: Int = 500,
crossinline block: (List<T>) -> Unit): SendChannel<T> {
return actor(context, parent = parent) {
private inline fun <T> createBatchActor(
context: CoroutineContext = Dispatchers.IO,
parent: Job? = null,
maxSize: Int = 100,
maxTime: Int = 500,
crossinline block: (List<T>) -> Unit
): SendChannel<T> {
return actor(context) {
val batch = ArrayList<T>(maxSize)
var deadline = 0L // deadline for sending this batch to callback block
while(true) {
while (true) {
// when deadline is reached or size is exceeded, pass the batch to the callback block
val remainingTime = deadline - System.currentTimeMillis()
if (batch.isNotEmpty() && remainingTime <= 0 || batch.size >= maxSize) {
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.model.AttachmentActionEntity
import chat.rocket.android.db.model.AttachmentEntity
import chat.rocket.android.db.model.FullMessage
import chat.rocket.android.db.model.ReactionEntity
import chat.rocket.android.db.model.UrlEntity
import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.db.model.*
import chat.rocket.android.util.retryDB
import chat.rocket.common.model.SimpleRoom
import chat.rocket.common.model.SimpleUser
......@@ -14,7 +9,6 @@ import chat.rocket.core.model.Message
import chat.rocket.core.model.Reactions
import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.Color
import chat.rocket.core.model.attachment.DEFAULT_COLOR_STR
import chat.rocket.core.model.attachment.Field
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
......@@ -22,8 +16,8 @@ import chat.rocket.core.model.messageTypeOf
import chat.rocket.core.model.url.Meta
import chat.rocket.core.model.url.ParsedUrl
import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
suspend fun map(message: FullMessage): Message? = map(listOf(message)).firstOrNull()
......@@ -58,7 +52,8 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
val attachments = this.attachments?.let { mapAttachments(it).asReversed() }
val messageType = messageTypeOf(this.message.type)
list.add(Message(
list.add(
Message(
id = this.message.id,
roomId = this.message.roomId,
message = this.message.message,
......@@ -82,7 +77,8 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
role = this.message.role,
synced = this.message.synced,
unread = this.message.unread
))
)
)
}
}
......@@ -106,13 +102,19 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
val parsedUrl = url.hostname?.let {
ParsedUrl(host = it)
}
val meta = if (!url.description.isNullOrEmpty() || !url.imageUrl.isNullOrEmpty() || !url.title.isNullOrEmpty()) {
val raw = HashMap<String, String>()
if (url.description != null) raw["ogDescription"] = url.description
if (url.title != null) raw["ogTitle"] = url.title
if (url.imageUrl != null) raw["ogImage"] = url.imageUrl
Meta(title = url.title,description = url.description, imageUrl = url.imageUrl, raw = raw)
} else null
val meta =
if (!url.description.isNullOrEmpty() || !url.imageUrl.isNullOrEmpty() || !url.title.isNullOrEmpty()) {
val raw = HashMap<String, String>()
if (url.description != null) raw["ogDescription"] = url.description
if (url.title != null) raw["ogTitle"] = url.title
if (url.imageUrl != null) raw["ogImage"] = url.imageUrl
Meta(
title = url.title,
description = url.description,
imageUrl = url.imageUrl,
raw = raw
)
} else null
list.add(Url(url = url.url, meta = meta, parsedUrl = parsedUrl))
}
......@@ -135,7 +137,7 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
attachments.forEach { attachment ->
with(attachment) {
val fields = if (hasFields) {
withContext(CommonPool) {
withContext(Dispatchers.IO) {
retryDB("getAttachmentFields(${attachment._id})") {
dbManager.messageDao().getAttachmentFields(attachment._id)
}
......@@ -144,7 +146,7 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
null
}
val actions = if (hasActions) {
withContext(CommonPool) {
withContext(Dispatchers.IO) {
retryDB("getAttachmentActions(${attachment._id})") {
dbManager.messageDao().getAttachmentActions(attachment._id)
}
......@@ -152,8 +154,8 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
} else {
null
}
val attachment = Attachment(
list.add(
Attachment(
title = title,
type = type,
description = description,
......@@ -179,10 +181,11 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
authorLink = authorLink,
fields = fields,
fallback = fallback,
buttonAlignment = if (actions != null && actions.isNotEmpty()) buttonAlignment ?: "vertical" else null,
buttonAlignment = if (actions != null && actions.isNotEmpty()) buttonAlignment
?: "vertical" else null,
actions = actions
)
)
list.add(attachment)
}
}
return list
......@@ -190,9 +193,11 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
private fun mapAction(action: AttachmentActionEntity): Action? {
return when (action.type) {
"button" -> ButtonAction(action.type, action.text, action.url, action.isWebView,
action.webViewHeightRatio, action.imageUrl, action.message,
action.isMessageInChatWindow)
"button" -> ButtonAction(
action.type, action.text, action.url, action.isWebView,
action.webViewHeightRatio, action.imageUrl, action.message,
action.isMessageInChatWindow
)
else -> null
}
}
......
......@@ -14,13 +14,13 @@ class SharedPreferencesAccountsRepository(
private val moshi: Moshi
) : AccountsRepository {
override fun save(newAccount: Account) {
val accounts = load()
val newList = accounts.filter { account -> newAccount.serverUrl != account.serverUrl }
override fun save(account: Account) {
load().filter { it.serverUrl != account.serverUrl }
.toMutableList()
newList.add(0, newAccount)
save(newList)
.apply {
add(0, account)
save(this)
}
}
override fun load(): List<Account> {
......@@ -28,22 +28,16 @@ class SharedPreferencesAccountsRepository(
val type = Types.newParameterizedType(List::class.java, Account::class.java)
val adapter = moshi.adapter<List<Account>>(type)
return adapter.fromJson(json) ?: emptyList()
return json?.let { adapter.fromJson(it) } ?: emptyList()
}
override fun remove(serverUrl: String) {
val accounts = load()
val newList = accounts.filter { account -> serverUrl != account.serverUrl }
.toMutableList()
save(newList)
save(load().filter { account -> serverUrl != account.serverUrl }.toMutableList())
}
private fun save(accounts: List<Account>) {
val type = Types.newParameterizedType(List::class.java, Account::class.java)
val adapter = moshi.adapter<List<Account>>(type)
preferences.edit {
putString(ACCOUNTS_KEY, adapter.toJson(accounts))
}
preferences.edit { putString(ACCOUNTS_KEY, adapter.toJson(accounts)) }
}
}
\ No newline at end of file
......@@ -15,8 +15,7 @@ class SharedPrefsBasicAuthRepository(
) : BasicAuthRepository {
override fun save(basicAuth: BasicAuth) {
val newList = load().filter { basicAuth -> basicAuth.host != basicAuth.host }
.toMutableList()
val newList = load().filter { auth -> auth.host != auth.host }.toMutableList()
newList.add(0, basicAuth)
save(newList)
}
......@@ -26,7 +25,7 @@ class SharedPrefsBasicAuthRepository(
val type = Types.newParameterizedType(List::class.java, BasicAuth::class.java)
val adapter = moshi.adapter<List<BasicAuth>>(type)
return adapter.fromJson(json) ?: emptyList()
return json?.let { adapter.fromJson(it) ?: emptyList() } ?: emptyList()
}
private fun save(basicAuths: List<BasicAuth>) {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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