Unverified Commit 972ca2cf authored by Pancor's avatar Pancor Committed by GitHub

Merge branch 'develop-2.x' into feature/save-unfinished-message

parents c16c4b0b 8ea28419
...@@ -3,6 +3,7 @@ apply plugin: 'io.fabric' ...@@ -3,6 +3,7 @@ apply plugin: 'io.fabric'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
android { android {
compileSdkVersion versions.compileSdk compileSdkVersion versions.compileSdk
...@@ -12,8 +13,8 @@ android { ...@@ -12,8 +13,8 @@ android {
applicationId "chat.rocket.android" applicationId "chat.rocket.android"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode 2000 versionCode 2001
versionName "2.0.0-alpha1" versionName "2.0.0-beta1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true multiDexEnabled true
} }
...@@ -29,12 +30,16 @@ android { ...@@ -29,12 +30,16 @@ android {
buildTypes { buildTypes {
release { release {
buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"'
buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.63.0"'
signingConfig signingConfigs.release signingConfig signingConfigs.release
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
debug { debug {
buildConfigField "String", "REQUIRED_SERVER_VERSION", '"0.62.0"'
buildConfigField "String", "RECOMMENDED_SERVER_VERSION", '"0.63.0"'
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
} }
} }
...@@ -59,6 +64,7 @@ dependencies { ...@@ -59,6 +64,7 @@ dependencies {
implementation libraries.constraintLayout implementation libraries.constraintLayout
implementation libraries.cardView implementation libraries.cardView
implementation libraries.flexbox implementation libraries.flexbox
implementation libraries.customTabs
implementation libraries.androidKtx implementation libraries.androidKtx
...@@ -110,8 +116,6 @@ dependencies { ...@@ -110,8 +116,6 @@ dependencies {
androidTestImplementation(libraries.expressoCore, { androidTestImplementation(libraries.expressoCore, {
exclude group: 'com.android.support', module: 'support-annotations' exclude group: 'com.android.support', module: 'support-annotations'
}) })
implementation 'com.android.support:customtabs:27.0.2'
} }
kotlin { kotlin {
......
...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME ...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
...@@ -35,6 +35,10 @@ ...@@ -35,6 +35,10 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".server.ui.ChangeServerActivity"
android:theme="@style/AuthenticationTheme" />
<activity <activity
android:name=".main.ui.MainActivity" android:name=".main.ui.MainActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
...@@ -50,6 +54,11 @@ ...@@ -50,6 +54,11 @@
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" /> android:theme="@style/AppTheme" />
<activity
android:name=".webview.oauth.ui.OauthWebViewActivity"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
android:theme="@style/AppTheme" />
<activity <activity
android:name=".chatroom.ui.ChatRoomActivity" android:name=".chatroom.ui.ChatRoomActivity"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
......
app/src/main/ic_launcher-web.png

20 KB | W: | H:

app/src/main/ic_launcher-web.png

39.1 KB | W: | H:

app/src/main/ic_launcher-web.png
app/src/main/ic_launcher-web.png
app/src/main/ic_launcher-web.png
app/src/main/ic_launcher-web.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -3,30 +3,48 @@ package chat.rocket.android.app ...@@ -3,30 +3,48 @@ package chat.rocket.android.app
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.app.Service import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.SharedPreferences
import androidx.content.edit
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.app.migration.RealmMigration
import chat.rocket.android.app.migration.RocketChatLibraryModule
import chat.rocket.android.app.migration.RocketChatServerModule
import chat.rocket.android.app.migration.model.RealmBasedServerInfo
import chat.rocket.android.app.migration.model.RealmPublicSetting
import chat.rocket.android.app.migration.model.RealmSession
import chat.rocket.android.app.migration.model.RealmUser
import chat.rocket.android.authentication.domain.model.toToken
import chat.rocket.android.dagger.DaggerAppComponent import chat.rocket.android.dagger.DaggerAppComponent
import chat.rocket.android.helper.CrashlyticsTree import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.MultiServerTokenRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.widget.emoji.EmojiRepository import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.common.model.Token import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository import chat.rocket.core.model.Value
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore import com.crashlytics.android.core.CrashlyticsCore
import com.facebook.drawee.backends.pipeline.DraweeConfig import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.core.ImagePipelineConfig
import com.jakewharton.threetenabp.AndroidThreeTen import com.jakewharton.threetenabp.AndroidThreeTen
import dagger.android.AndroidInjector import dagger.android.*
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.HasServiceInjector
import io.fabric.sdk.android.Fabric import io.fabric.sdk.android.Fabric
import io.realm.Realm
import io.realm.RealmConfiguration
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking
import timber.log.Timber import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject import javax.inject.Inject
class RocketChatApplication : Application(), HasActivityInjector, HasServiceInjector {
class RocketChatApplication : Application(), HasActivityInjector, HasServiceInjector,
HasBroadcastReceiverInjector {
@Inject @Inject
lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity> lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
...@@ -34,6 +52,9 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -34,6 +52,9 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
@Inject @Inject
lateinit var serviceDispatchingAndroidInjector: DispatchingAndroidInjector<Service> lateinit var serviceDispatchingAndroidInjector: DispatchingAndroidInjector<Service>
@Inject
lateinit var broadcastReceiverInjector: DispatchingAndroidInjector<BroadcastReceiver>
@Inject @Inject
lateinit var imagePipelineConfig: ImagePipelineConfig lateinit var imagePipelineConfig: ImagePipelineConfig
@Inject @Inject
...@@ -48,14 +69,25 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -48,14 +69,25 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
lateinit var settingsRepository: SettingsRepository lateinit var settingsRepository: SettingsRepository
@Inject @Inject
lateinit var tokenRepository: TokenRepository lateinit var tokenRepository: TokenRepository
@Inject
lateinit var accountRepository: AccountsRepository
@Inject
lateinit var saveCurrentServerRepository: SaveCurrentServerInteractor
@Inject
lateinit var prefs: SharedPreferences
@Inject
lateinit var getAccountsInteractor: GetAccountsInteractor
@Inject
lateinit var localRepository: LocalRepository
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
DaggerAppComponent.builder().application(this).build().inject(this) DaggerAppComponent.builder().application(this).build().inject(this)
// TODO - remove this when we have a proper service handling connection... // TODO - remove this on the future, temporary migration stuff for pre-release versions.
initCurrentServer() migrateInternalTokens()
context = WeakReference(applicationContext)
AndroidThreeTen.init(this) AndroidThreeTen.init(this)
EmojiRepository.load(this) EmojiRepository.load(this)
...@@ -63,17 +95,143 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -63,17 +95,143 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
setupCrashlytics() setupCrashlytics()
setupFresco() setupFresco()
setupTimber() setupTimber()
// TODO - remove this and all realm stuff when we got to 80% in 2.0
try {
if (!localRepository.hasMigrated()) {
migrateFromLegacy()
}
} catch (ex: Exception) {
Timber.d(ex, "Error migrating old accounts")
}
}
private fun migrateFromLegacy() {
Realm.init(this)
val serveListConfiguration = RealmConfiguration.Builder()
.name("server.list.realm")
.schemaVersion(6)
.migration(RealmMigration())
.modules(RocketChatServerModule())
.build()
val serverRealm = Realm.getInstance(serveListConfiguration)
val serversInfoList = serverRealm.where(RealmBasedServerInfo::class.java).findAll().toList()
serversInfoList.forEach { server ->
val hostname = server.hostname
val url = if (server.insecure) "http://$hostname" else "https://$hostname"
val config = RealmConfiguration.Builder()
.name("${server.hostname}.realm")
.schemaVersion(6)
.migration(RealmMigration())
.modules(RocketChatLibraryModule())
.build()
val realm = Realm.getInstance(config)
val user = realm.where(RealmUser::class.java)
.isNotEmpty(RealmUser.EMAILS).findFirst()
val session = realm.where(RealmSession::class.java).findFirst()
migratePublicSettings(url, realm)
if (user != null && session != null) {
val authToken = session.token
settingsRepository.get(url)
migrateServerInfo(url, authToken!!, settingsRepository.get(url), user)
}
realm.close()
}
migrateCurrentServer(serversInfoList)
serverRealm.close()
localRepository.setMigrated(true)
}
private fun migrateServerInfo(url: String, authToken: String, settings: PublicSettings, user: RealmUser) {
val userId = user._id
val avatar = UrlHelper.getAvatarUrl(url, user.username!!)
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(url, it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(url, it)
}
val account = Account(url, icon, logo, user.username!!, avatar)
launch(CommonPool) {
tokenRepository.save(url, Token(userId!!, authToken))
accountRepository.save(account)
}
}
private fun migratePublicSettings(url: String, realm: Realm) {
val settings = realm.where(RealmPublicSetting::class.java).findAll()
val serverSettings = hashMapOf<String, Value<Any>>()
settings.toList().forEach { setting ->
val type = setting.type!!
val value = setting.value!!
val convertedSetting = when (type) {
"string" -> Value(value)
"language" -> Value(value)
"boolean" -> Value(value.toBoolean())
"int" -> try {
Value(value.toInt())
} catch (ex: NumberFormatException) {
Value(0)
}
else -> null // ignore
} }
// TODO - remove this when we have a proper service handling connection... if (convertedSetting != null) {
private fun initCurrentServer() { val id = setting._id!!
val currentServer = getCurrentServerInteractor.get() serverSettings.put(id, convertedSetting)
val serverToken = currentServer?.let { multiServerRepository.get(currentServer) }
val settings = currentServer?.let { settingsRepository.get(currentServer) }
if (currentServer != null && serverToken != null && settings != null) {
tokenRepository.save(Token(serverToken.userId, serverToken.authToken))
} }
} }
settingsRepository.save(url, serverSettings)
}
private fun migrateCurrentServer(serversList: List<RealmBasedServerInfo>) {
if (getCurrentServerInteractor.get() == null) {
var currentServer = getSharedPreferences("cache", Context.MODE_PRIVATE)
.getString("KEY_SELECTED_SERVER_HOSTNAME", null)
currentServer = if (serversList.isNotEmpty()) {
val server = serversList.find { it.hostname == currentServer }
val hostname = server!!.hostname
if (server.insecure) {
"http://$hostname"
} else {
"https://$hostname"
}
} else {
"http://$currentServer"
}
saveCurrentServerRepository.save(currentServer)
}
}
private fun migrateInternalTokens() {
if (!prefs.getBoolean(INTERNAL_TOKEN_MIGRATION_NEEDED, true)) {
Timber.d("Tokens already migrated")
return
}
getCurrentServerInteractor.get()?.let { serverUrl ->
multiServerRepository.get(serverUrl)?.let { token ->
tokenRepository.save(serverUrl, Token(token.userId, token.authToken))
}
}
runBlocking {
getAccountsInteractor.get().forEach { account ->
multiServerRepository.get(account.serverUrl)?.let { token ->
tokenRepository.save(account.serverUrl, token.toToken())
}
}
}
prefs.edit { putBoolean(INTERNAL_TOKEN_MIGRATION_NEEDED, false) }
}
private fun setupCrashlytics() { private fun setupCrashlytics() {
val core = CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build() val core = CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()
...@@ -99,4 +257,23 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje ...@@ -99,4 +257,23 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
override fun serviceInjector(): AndroidInjector<Service> { override fun serviceInjector(): AndroidInjector<Service> {
return serviceDispatchingAndroidInjector return serviceDispatchingAndroidInjector
} }
override fun broadcastReceiverInjector(): AndroidInjector<BroadcastReceiver> {
return broadcastReceiverInjector
}
companion object {
var context: WeakReference<Context>? = null
fun getAppContext(): Context? {
return context?.get()
}
}
} }
private fun LocalRepository.setMigrated(migrated: Boolean) {
save(LocalRepository.MIGRATION_FINISHED_KEY, migrated)
}
private fun LocalRepository.hasMigrated() = getBoolean(LocalRepository.MIGRATION_FINISHED_KEY)
private const val INTERNAL_TOKEN_MIGRATION_NEEDED = "INTERNAL_TOKEN_MIGRATION_NEEDED"
\ No newline at end of file
package chat.rocket.android.app.migration
import chat.rocket.android.BuildConfig
import chat.rocket.android.app.migration.model.RealmUser
import io.realm.DynamicRealm
import io.realm.RealmMigration
class RealmMigration : RealmMigration {
override fun migrate(dynamicRealm: DynamicRealm, oldVersion: Long, newVersion: Long) {
var oldVersion = oldVersion
val schema = dynamicRealm.schema
if (oldVersion == 0L) {
// NOOP
oldVersion++
}
if (oldVersion == 1L) {
oldVersion++
}
if (oldVersion == 2L) {
oldVersion++
}
if (oldVersion == 3L) {
oldVersion++
}
if (oldVersion == 4L) {
oldVersion++
}
if (oldVersion == 5L) {
val userSchema = schema.get("RealmUser")
try {
userSchema?.addField(RealmUser.NAME, String::class.java)
} catch (e: IllegalArgumentException) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
// ignore; it makes here if the schema for this model was already update before without migration
}
}
}
// hack around to avoid "new different configuration cannot access the same file" error
override fun hashCode(): Int {
return 37
}
override fun equals(o: Any?): Boolean {
return o is chat.rocket.android.app.migration.RealmMigration
}
// end hack
}
\ No newline at end of file
package chat.rocket.android.app.migration
import io.realm.annotations.RealmModule
@RealmModule(library = true, allClasses = true)
class RocketChatLibraryModule
\ No newline at end of file
package chat.rocket.android.app.migration
import chat.rocket.android.app.migration.model.RealmBasedServerInfo
import io.realm.annotations.RealmModule
@RealmModule(library = true, classes = arrayOf(RealmBasedServerInfo::class))
class RocketChatServerModule
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmBasedServerInfo : RealmObject() {
@PrimaryKey
@JvmField var hostname: String? = null
@JvmField var name: String? = null
@JvmField var session: String? = null
@JvmField var insecure: Boolean = false
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmEmail : RealmObject() {
@PrimaryKey
@JvmField
var address: String? = null
@JvmField
var verified: Boolean = false
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmPreferences : RealmObject() {
@PrimaryKey
@JvmField
var id: String? = null
@JvmField
var newRoomNotification: String? = null
@JvmField
var newMessageNotification: String? = null
@JvmField
var useEmojis: Boolean = false
@JvmField
var convertAsciiEmoji: Boolean = false
@JvmField
var saveMobileBandwidth: Boolean = false
@JvmField
var collapseMediaByDefault: Boolean = false
@JvmField
var unreadRoomsMode: Boolean = false
@JvmField
var autoImageLoad: Boolean = false
@JvmField
var emailNotificationMode: String? = null
@JvmField
var unreadAlert: Boolean = false
@JvmField
var desktopNotificationDuration: Int = 0
@JvmField
var viewMode: Int = 0
@JvmField
var hideUsernames: Boolean = false
@JvmField
var hideAvatars: Boolean = false
@JvmField
var hideFlexTab: Boolean = false
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmPublicSetting : RealmObject() {
@PrimaryKey
@JvmField
var _id: String? = null
@JvmField
var group: String? = null
@JvmField
var type: String? = null
@JvmField
var value: String? = null
@JvmField
var _updatedAt: Long = 0
@JvmField
var meta: String? = null
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmSession : RealmObject() {
@JvmField
@PrimaryKey
var sessionId: Int = 0 //only 0 is used!
@JvmField
var token: String? = null
@JvmField
var tokenVerified: Boolean = false
@JvmField
var error: String? = null
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmSettings : RealmObject() {
@PrimaryKey
@JvmField
var id: String? = null
@JvmField
var preferences: RealmPreferences? = null
}
\ No newline at end of file
package chat.rocket.android.app.migration.model
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
open class RealmUser : RealmObject() {
companion object {
const val ID = "_id"
const val NAME = "name"
const val USERNAME = "username"
const val STATUS = "status"
const val UTC_OFFSET = "utcOffset"
const val EMAILS = "emails"
const val SETTINGS = "settings"
const val STATUS_ONLINE = "online"
const val STATUS_BUSY = "busy"
const val STATUS_AWAY = "away"
const val STATUS_OFFLINE = "offline"
}
@PrimaryKey
@JvmField
var _id: String? = null
@JvmField
var name: String? = null
@JvmField
var username: String? = null
@JvmField
var status: String? = null
@JvmField
var utcOffset: Double = 0.toDouble()
@JvmField
var emails: RealmList<RealmEmail>? = null
@JvmField
var settings: RealmSettings? = null
}
\ No newline at end of file
package chat.rocket.android.authentication.di package chat.rocket.android.authentication.di
import android.content.Context
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.dagger.scope.PerActivity import chat.rocket.android.dagger.scope.PerActivity
...@@ -12,5 +11,5 @@ class AuthenticationModule { ...@@ -12,5 +11,5 @@ class AuthenticationModule {
@Provides @Provides
@PerActivity @PerActivity
fun provideAuthenticationNavigator(activity: AuthenticationActivity, context: Context) = AuthenticationNavigator(activity, context) fun provideAuthenticationNavigator(activity: AuthenticationActivity) = AuthenticationNavigator(activity)
} }
\ No newline at end of file
package chat.rocket.android.authentication.domain.model package chat.rocket.android.authentication.domain.model
import chat.rocket.common.model.Token
import se.ansman.kotshi.JsonSerializable import se.ansman.kotshi.JsonSerializable
@JsonSerializable @JsonSerializable
data class TokenModel(val userId: String, val authToken: String) data class TokenModel(val userId: String, val authToken: String)
fun TokenModel.toToken() = Token(userId, authToken)
\ No newline at end of file
package chat.rocket.android.authentication.infraestructure
import chat.rocket.common.model.Token
import chat.rocket.core.TokenRepository
class MemoryTokenRepository : TokenRepository {
var savedToken: Token? = null
override fun get(): Token? {
return savedToken
}
override fun save(token: Token) {
savedToken = token
}
}
\ No newline at end of file
package chat.rocket.android.authentication.infraestructure
import android.content.SharedPreferences
import androidx.content.edit
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.common.model.Token
import com.squareup.moshi.Moshi
import timber.log.Timber
class SharedPreferencesTokenRepository(private val prefs: SharedPreferences, moshi: Moshi) : TokenRepository {
private var servers = prefs.getStringSet(KEY_SERVERS, emptySet()).toMutableSet()
private var currentUrl: String? = null
private var currentToken: Token? = null
private val adapter = moshi.adapter<TokenModel>(TokenModel::class.java)
override fun get(url: String): Token? {
if (currentToken != null && url == currentUrl) {
return currentToken
}
try {
prefs.getString(tokenKey(url), null)?.let { tokenStr ->
val model = adapter.fromJson(tokenStr)
model?.let {
val token = Token(model.userId, model.authToken)
currentToken = token
currentUrl = url
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error parsing token for ${tokenKey(url)}")
ex.printStackTrace()
}
return currentToken
}
override fun save(url: String, token: Token) {
try {
val model = TokenModel(token.userId, token.authToken)
val str = adapter.toJson(model)
servers.add(url)
prefs.edit {
putString(tokenKey(url), str)
putStringSet(KEY_SERVERS, servers)
}
currentToken = token
currentUrl = url
} catch (ex: Exception) {
Timber.d(ex, "Error saving token for ${tokenKey(url)}")
ex.printStackTrace()
}
}
override fun remove(url: String) {
servers.remove(url)
prefs.edit {
remove(url)
putStringSet(KEY_SERVERS, servers)
}
}
override fun clear() {
servers.forEach { server ->
prefs.edit { remove(server) }
}
servers.clear()
prefs.edit {
remove(KEY_SERVERS)
}
}
private fun tokenKey(url: String) = "$KEY_TOKEN$url"
}
private const val KEY_TOKEN = "KEY_TOKEN_"
private const val KEY_SERVERS = "KEY_SERVERS"
\ No newline at end of file
package chat.rocket.android.authentication.login.presentation package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.authentication.domain.model.TokenModel import chat.rocket.android.BuildConfig
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.OauthHelper
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.generateRandomString import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.isEmailValid import chat.rocket.android.util.VersionInfo
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.*
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatTwoFactorException import chat.rocket.common.RocketChatTwoFactorException
import chat.rocket.common.model.Token
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.* import chat.rocket.core.internal.rest.*
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.delay
import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
private const val TYPE_LOGIN_USER_EMAIL = 0
private const val TYPE_LOGIN_CAS = 1
private const val TYPE_LOGIN_OAUTH = 2
private const val SERVICE_NAME_GITHUB = "github"
private const val SERVICE_NAME_GOOGLE = "google"
private const val SERVICE_NAME_LINKEDIN = "linkedin"
private const val SERVICE_NAME_GILAB = "gitlab"
class LoginPresenter @Inject constructor(private val view: LoginView, class LoginPresenter @Inject constructor(private val view: LoginView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
private val multiServerRepository: MultiServerTokenRepository, private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val settingsInteractor: GetSettingsInteractor, private val getAccountsInteractor: GetAccountsInteractor,
private val serverInteractor: GetCurrentServerInteractor, settingsInteractor: GetSettingsInteractor,
factory: RocketChatClientFactory) { serverInteractor: GetCurrentServerInteractor,
private val saveAccountInteractor: SaveAccountInteractor,
private val factory: RocketChatClientFactory)
: CheckServerPresenter(strategy, factory.create(serverInteractor.get()!!), view) {
// TODO - we should validate the current server when opening the app, and have a nonnull get() // TODO - we should validate the current server when opening the app, and have a nonnull get()
private val client: RocketChatClient = factory.create(serverInteractor.get()!!) private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private val settings: PublicSettings = settingsInteractor.get(currentServer)
private lateinit var usernameOrEmail: String
private lateinit var password: String
private lateinit var credentialToken: String
private lateinit var credentialSecret: String
fun setupView() { fun setupView() {
val server = serverInteractor.get() setupLoginView()
if (server == null) { setupUserRegistrationView()
navigator.toServerScreen() setupCasView()
return setupOauthServicesView()
checkServerInfo()
}
fun authenticateWithUserAndPassword(usernameOrEmail: String, password: String) {
when {
usernameOrEmail.isBlank() -> {
view.alertWrongUsernameOrEmail()
}
password.isEmpty() -> {
view.alertWrongPassword()
}
else -> {
this.usernameOrEmail = usernameOrEmail
this.password = password
doAuthentication(TYPE_LOGIN_USER_EMAIL)
}
}
}
fun authenticateWithCas(token: String) {
credentialToken = token
doAuthentication(TYPE_LOGIN_CAS)
}
fun authenticateWithOauth(token: String, secret: String) {
credentialToken = token
credentialSecret = secret
doAuthentication(TYPE_LOGIN_OAUTH)
} }
val settings = settingsInteractor.get(server)
fun signup() = navigator.toSignUp()
private fun setupLoginView() {
if (settings.isLoginFormEnabled()) { if (settings.isLoginFormEnabled()) {
view.showFormView() view.showFormView()
view.setupLoginButtonListener() view.setupLoginButtonListener()
...@@ -46,88 +98,136 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -46,88 +98,136 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
} else { } else {
view.hideFormView() view.hideFormView()
} }
if (settings.isRegistrationEnabledForNewUsers()) {
view.showSignUpView()
view.setupSignUpView()
} }
private fun setupCasView() {
if (settings.isCasAuthenticationEnabled()) { if (settings.isCasAuthenticationEnabled()) {
val token = generateRandomString(17) val token = generateRandomString(17)
view.setupCasButtonListener(UrlHelper.getCasUrl(settings.casLoginUrl(), server, token), token) view.setupCasButtonListener(UrlHelper.getCasUrl(settings.casLoginUrl(), currentServer, token), token)
view.showCasButton() view.showCasButton()
} }
}
private fun setupUserRegistrationView() {
if (settings.isRegistrationEnabledForNewUsers()) {
view.showSignUpView()
view.setupSignUpView()
}
}
private fun setupOauthServicesView() {
launchUI(strategy) {
try {
val services = client.settingsOauth().services
if (services.isNotEmpty()) {
val state = "{\"loginStyle\":\"popup\",\"credentialToken\":\"${generateRandomString(40)}\",\"isCordova\":true}".encodeToBase64()
var totalSocialAccountsEnabled = 0 var totalSocialAccountsEnabled = 0
if (settings.isFacebookAuthenticationEnabled()) { if (settings.isFacebookAuthenticationEnabled()) {
view.enableLoginByFacebook() // //TODO: Remove until we have this implemented
totalSocialAccountsEnabled++ // view.enableLoginByFacebook()
// totalSocialAccountsEnabled++
} }
if (settings.isGithubAuthenticationEnabled()) { if (settings.isGithubAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_GITHUB)
if (clientId != null) {
view.setupGithubButtonListener(OauthHelper.getGithubOauthUrl(clientId, state), state)
view.enableLoginByGithub() view.enableLoginByGithub()
totalSocialAccountsEnabled++ totalSocialAccountsEnabled++
} }
}
if (settings.isGoogleAuthenticationEnabled()) { if (settings.isGoogleAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_GOOGLE)
if (clientId != null) {
view.setupGoogleButtonListener(OauthHelper.getGoogleOauthUrl(clientId, currentServer, state), state)
view.enableLoginByGoogle() view.enableLoginByGoogle()
totalSocialAccountsEnabled++ totalSocialAccountsEnabled++
} }
}
if (settings.isLinkedinAuthenticationEnabled()) { if (settings.isLinkedinAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_LINKEDIN)
if (clientId != null) {
view.setupLinkedinButtonListener(OauthHelper.getLinkedinOauthUrl(clientId, currentServer, state), state)
view.enableLoginByLinkedin() view.enableLoginByLinkedin()
totalSocialAccountsEnabled++ totalSocialAccountsEnabled++
} }
}
if (settings.isMeteorAuthenticationEnabled()) { if (settings.isMeteorAuthenticationEnabled()) {
view.enableLoginByMeteor() //TODO: Remove until we have this implemented
totalSocialAccountsEnabled++ // view.enableLoginByMeteor()
// totalSocialAccountsEnabled++
} }
if (settings.isTwitterAuthenticationEnabled()) { if (settings.isTwitterAuthenticationEnabled()) {
view.enableLoginByTwitter() //TODO: Remove until we have this implemented
totalSocialAccountsEnabled++ // view.enableLoginByTwitter()
// totalSocialAccountsEnabled++
} }
if (settings.isGitlabAuthenticationEnabled()) { if (settings.isGitlabAuthenticationEnabled()) {
val clientId = getOauthClientId(services, SERVICE_NAME_GILAB)
if (clientId != null) {
view.setupGitlabButtonListener(OauthHelper.getGitlabOauthUrl(clientId, currentServer, state), state)
view.enableLoginByGitlab() view.enableLoginByGitlab()
totalSocialAccountsEnabled++ totalSocialAccountsEnabled++
} }
}
if (totalSocialAccountsEnabled > 0) { if (totalSocialAccountsEnabled > 0) {
view.showOauthView() view.enableOauthView()
if (totalSocialAccountsEnabled > 3) { if (totalSocialAccountsEnabled > 3) {
view.setupFabListener() view.setupFabListener()
} }
} else {
view.disableOauthView()
} }
} else {
view.disableOauthView()
} }
} catch (exception: RocketChatException) {
fun authenticate(usernameOrEmail: String, password: String) { Timber.e(exception)
val server = serverInteractor.get() view.disableOauthView()
when {
server == null -> {
navigator.toServerScreen()
} }
usernameOrEmail.isBlank() -> {
view.alertWrongUsernameOrEmail()
} }
password.isEmpty() -> {
view.alertWrongPassword()
} }
else -> {
private fun doAuthentication(loginType: Int) {
launchUI(strategy) { launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) { if (NetworkHelper.hasInternetAccess()) {
view.disableUserInput() view.disableUserInput()
view.showLoading() view.showLoading()
try { try {
val token = if (usernameOrEmail.isEmailValid()) { val token = when (loginType) {
TYPE_LOGIN_USER_EMAIL -> {
if (usernameOrEmail.isEmail()) {
client.loginWithEmail(usernameOrEmail, password) client.loginWithEmail(usernameOrEmail, password)
} else { } else {
val settings = settingsInteractor.get(server)
if (settings.isLdapAuthenticationEnabled()) { if (settings.isLdapAuthenticationEnabled()) {
client.loginWithLdap(usernameOrEmail, password) client.loginWithLdap(usernameOrEmail, password)
} else { } else {
client.login(usernameOrEmail, password) client.login(usernameOrEmail, password)
} }
} }
}
saveToken(server, TokenModel(token.userId, token.authToken), client.me().username) TYPE_LOGIN_CAS -> {
delay(3, TimeUnit.SECONDS)
client.loginWithCas(credentialToken)
}
TYPE_LOGIN_OAUTH -> {
client.loginWithOauth(credentialToken, credentialSecret)
}
else -> {
throw IllegalStateException("Expected TYPE_LOGIN_USER_EMAIL, TYPE_LOGIN_CAS or TYPE_LOGIN_OAUTH")
}
}
val username = client.me().username
if (username != null) {
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username)
saveAccount(username)
saveToken(token)
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
} else if (loginType == TYPE_LOGIN_OAUTH) {
view.alertRequiresUsername()
}
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
when (exception) { when (exception) {
is RocketChatTwoFactorException -> { is RocketChatTwoFactorException -> {
...@@ -150,53 +250,33 @@ class LoginPresenter @Inject constructor(private val view: LoginView, ...@@ -150,53 +250,33 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
} }
} }
} }
}
}
fun authenticateWithCas(casToken: String) { private fun getOauthClientId(listMap: List<Map<String, String>>, serviceName: String): String? {
launchUI(strategy) { return listMap.find { map -> map.containsValue(serviceName) }
if (NetworkHelper.hasInternetAccess()) { ?.get("appId")
view.disableUserInput()
view.showLoading()
try {
val server = serverInteractor.get()
if (server != null) {
delay(3, TimeUnit.SECONDS)
val token = client.loginWithCas(casToken)
saveToken(server, TokenModel(token.userId, token.authToken), client.me().username)
registerPushToken()
navigator.toChatList()
} else {
navigator.toServerScreen()
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
} }
} finally {
view.hideLoading() private suspend fun saveAccount(username: String) {
view.enableUserInput() val icon = settings.favicon()?.let {
} UrlHelper.getServerLogoUrl(currentServer, it)
} else {
view.showNoInternetConnection()
} }
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
} }
val thumb = UrlHelper.getAvatarUrl(currentServer, username)
val account = Account(currentServer, icon, logo, username, thumb)
saveAccountInteractor.save(account)
} }
fun signup() = navigator.toSignUp() private fun saveToken(token: Token) {
tokenRepository.save(currentServer, token)
private suspend fun saveToken(server: String, tokenModel: TokenModel, username: String?) {
multiServerRepository.save(server, tokenModel)
localRepository.save(LocalRepository.USERNAME_KEY, username)
registerPushToken()
} }
private suspend fun registerPushToken() { private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let { localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it) client.registerPushToken(it, getAccountsInteractor.get(), factory)
} }
// TODO: Schedule push token registering when it comes up null // TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
} }
} }
\ No newline at end of file
package chat.rocket.android.authentication.login.presentation package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.behaviours.InternetView import chat.rocket.android.core.behaviours.InternetView
import chat.rocket.android.core.behaviours.LoadingView import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
interface LoginView : LoadingView, MessageView, InternetView { interface LoginView : LoadingView, MessageView, InternetView, VersionCheckView {
/** /**
* Shows the form view (i.e the username/email and password fields) if it is enabled by the server settings. * Shows the form view (i.e the username/email and password fields) if it is enabled by the server settings.
...@@ -37,7 +38,7 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -37,7 +38,7 @@ interface LoginView : LoadingView, MessageView, InternetView {
/** /**
* Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server settings. * Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server settings.
* *
* REMARK: We must set up the CAS button listener [setupCasButtonListener]. * REMARK: We must set up the CAS button listener before showing it [setupCasButtonListener].
*/ */
fun showCasButton() fun showCasButton()
...@@ -49,8 +50,8 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -49,8 +50,8 @@ interface LoginView : LoadingView, MessageView, InternetView {
/** /**
* Setups the CAS button when tapped. * Setups the CAS button when tapped.
* *
* @param casUrl The CAS URL to login/sign up with. * @param casUrl The CAS URL to authenticate with.
* @param casToken The requested Token sent to the CAS server. * @param casToken The requested token to be sent to the CAS server.
*/ */
fun setupCasButtonListener(casUrl: String, casToken: String) fun setupCasButtonListener(casUrl: String, casToken: String)
...@@ -72,18 +73,18 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -72,18 +73,18 @@ interface LoginView : LoadingView, MessageView, InternetView {
fun hideSignUpView() fun hideSignUpView()
/** /**
* Shows the oauth view if the login via social accounts is enabled by the server settings. * Enables and shows the oauth view if there is login via social accounts enabled by the server settings.
* *
* REMARK: We must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle], * REMARK: We must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle],
* [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter] or [enableLoginByGitlab]) for the oauth view. * [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter] or [enableLoginByGitlab]) for the oauth view.
* If the possibility of login via social accounts exceeds 3 different ways we should set up the FAB ([setupFabListener]) to show the remaining view(s). * If the possibility of login via social accounts exceeds 3 different ways we should set up the FAB ([setupFabListener]) to show the remaining view(s).
*/ */
fun showOauthView() fun enableOauthView()
/** /**
* Hides the oauth view. * Disables and hides the Oauth view if there is not login via social accounts enabled by the server settings.
*/ */
fun hideOauthView() fun disableOauthView()
/** /**
* Shows the login button. * Shows the login button.
...@@ -96,40 +97,80 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -96,40 +97,80 @@ interface LoginView : LoadingView, MessageView, InternetView {
fun hideLoginButton() fun hideLoginButton()
/** /**
* Shows the "login by Facebook view if it is enabled by the server settings. * Shows the "login by Facebook view if it is enable by the server settings.
*/ */
fun enableLoginByFacebook() fun enableLoginByFacebook()
/** /**
* Shows the "login by Github" view if it is enabled by the server settings. * Shows the "login by Github" view if it is enable by the server settings.
*
* REMARK: We must set up the Github button listener before enabling it [setupGithubButtonListener].
*/ */
fun enableLoginByGithub() fun enableLoginByGithub()
/** /**
* Shows the "login by Google" view if it is enabled by the server settings. * Setups the Github button when tapped.
*
* @param githubUrl The Github OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGithubButtonListener(githubUrl: String, state: String)
/**
* Shows the "login by Google" view if it is enable by the server settings.
*
* REMARK: We must set up the Google button listener before enabling it [setupGoogleButtonListener].
*/ */
fun enableLoginByGoogle() fun enableLoginByGoogle()
/** /**
* Shows the "login by Linkedin" view if it is enabled by the server settings. * Setups the Google button when tapped.
*
* @param googleUrl The Google OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGoogleButtonListener(googleUrl: String, state: String)
/**
* Shows the "login by Linkedin" view if it is enable by the server settings.
*
* REMARK: We must set up the Linkedin button listener before enabling it [setupLinkedinButtonListener].
*/ */
fun enableLoginByLinkedin() fun enableLoginByLinkedin()
/** /**
* Shows the "login by Meteor" view if it is enabled by the server settings. * Setups the Linkedin button when tapped.
*
* @param linkedinUrl The Linkedin OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupLinkedinButtonListener(linkedinUrl: String, state: String)
/**
* Shows the "login by Meteor" view if it is enable by the server settings.
*/ */
fun enableLoginByMeteor() fun enableLoginByMeteor()
/** /**
* Shows the "login by Twitter" view if it is enabled by the server settings. * Shows the "login by Twitter" view if it is enable by the server settings.
*/ */
fun enableLoginByTwitter() fun enableLoginByTwitter()
/** /**
* Shows the "login by Gitlab" view if it is enabled by the server settings. * Shows the "login by Gitlab" view if it is enable by the server settings.
*
* REMARK: We must set up the Gitlab button listener before enabling it [setupGitlabButtonListener].
*/ */
fun enableLoginByGitlab() fun enableLoginByGitlab()
/**
* Setups the Gitlab button when tapped.
*
* @param gitlabUrl The Gitlab OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupGitlabButtonListener(gitlabUrl: String, state: String)
/** /**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)). * Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*/ */
...@@ -146,4 +187,9 @@ interface LoginView : LoadingView, MessageView, InternetView { ...@@ -146,4 +187,9 @@ interface LoginView : LoadingView, MessageView, InternetView {
* Alerts the user about a wrong inputted password. * Alerts the user about a wrong inputted password.
*/ */
fun alertWrongPassword() fun alertWrongPassword()
/**
* Alerts the user about the need of creating an username using the web app when creating an user through OAuth.
*/
fun alertRequiresUsername()
} }
\ No newline at end of file
...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.login.ui ...@@ -2,6 +2,7 @@ package chat.rocket.android.authentication.login.ui
import DrawableHelper import DrawableHelper
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
...@@ -13,21 +14,29 @@ import android.view.ViewGroup ...@@ -13,21 +14,29 @@ import android.view.ViewGroup
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.Toast
import chat.rocket.android.BuildConfig
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.login.presentation.LoginPresenter import chat.rocket.android.authentication.login.presentation.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.webview.cas.ui.webViewIntent import chat.rocket.android.webview.cas.ui.INTENT_CAS_TOKEN
import chat.rocket.android.webview.cas.ui.casWebViewIntent
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_SECRET
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN
import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.* import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject import javax.inject.Inject
internal const val REQUEST_CODE_FOR_CAS = 1 internal const val REQUEST_CODE_FOR_CAS = 1
internal const val REQUEST_CODE_FOR_OAUTH = 2
class LoginFragment : Fragment(), LoginView { class LoginFragment : Fragment(), LoginView {
@Inject lateinit var presenter: LoginPresenter @Inject lateinit var presenter: LoginPresenter
private var isOauthViewEnable = false
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener { private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
areLoginOptionsNeeded() areLoginOptionsNeeded()
} }
...@@ -64,10 +73,14 @@ class LoginFragment : Fragment(), LoginView { ...@@ -64,10 +73,14 @@ class LoginFragment : Fragment(), LoginView {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_FOR_CAS) {
data?.apply { data?.apply {
presenter.authenticateWithCas(getStringExtra("cas_token")) presenter.authenticateWithCas(getStringExtra(INTENT_CAS_TOKEN))
}
} else if (requestCode == REQUEST_CODE_FOR_OAUTH) {
data?.apply {
presenter.authenticateWithOauth(getStringExtra(INTENT_OAUTH_CREDENTIAL_TOKEN), getStringExtra(INTENT_OAUTH_CREDENTIAL_SECRET))
} }
} }
} }
...@@ -121,7 +134,7 @@ class LoginFragment : Fragment(), LoginView { ...@@ -121,7 +134,7 @@ class LoginFragment : Fragment(), LoginView {
override fun setupLoginButtonListener() { override fun setupLoginButtonListener() {
button_log_in.setOnClickListener { button_log_in.setOnClickListener {
presenter.authenticate(text_username_or_email.textContent, text_password.textContent) presenter.authenticateWithUserAndPassword(text_username_or_email.textContent, text_password.textContent)
} }
} }
...@@ -147,7 +160,7 @@ class LoginFragment : Fragment(), LoginView { ...@@ -147,7 +160,7 @@ class LoginFragment : Fragment(), LoginView {
override fun setupCasButtonListener(casUrl: String, casToken: String) { override fun setupCasButtonListener(casUrl: String, casToken: String) {
button_cas.setOnClickListener { button_cas.setOnClickListener {
startActivityForResult(context?.webViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS) startActivityForResult(context?.casWebViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold) activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
} }
} }
...@@ -173,14 +186,15 @@ class LoginFragment : Fragment(), LoginView { ...@@ -173,14 +186,15 @@ class LoginFragment : Fragment(), LoginView {
text_new_to_rocket_chat.setVisible(false) text_new_to_rocket_chat.setVisible(false)
} }
override fun showOauthView() { override fun enableOauthView() {
isOauthViewEnable = true
showThreeSocialAccountsMethods() showThreeSocialAccountsMethods()
social_accounts_container.setVisible(true) social_accounts_container.setVisible(true)
} }
override fun hideOauthView() { override fun disableOauthView() {
isOauthViewEnable = false
social_accounts_container.setVisible(false) social_accounts_container.setVisible(false)
button_fab.setVisible(false)
} }
override fun showLoginButton() { override fun showLoginButton() {
...@@ -192,31 +206,60 @@ class LoginFragment : Fragment(), LoginView { ...@@ -192,31 +206,60 @@ class LoginFragment : Fragment(), LoginView {
} }
override fun enableLoginByFacebook() { override fun enableLoginByFacebook() {
button_facebook.isEnabled = true button_facebook.isClickable = true
} }
override fun enableLoginByGithub() { override fun enableLoginByGithub() {
button_github.isEnabled = true button_github.isClickable = true
}
override fun setupGithubButtonListener(githubUrl: String, state: String) {
button_github.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(githubUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
} }
override fun enableLoginByGoogle() { override fun enableLoginByGoogle() {
button_google.isEnabled = true button_google.isClickable = true
}
// TODO: Use custom tabs instead of web view. See https://github.com/RocketChat/Rocket.Chat.Android/issues/968
override fun setupGoogleButtonListener(googleUrl: String, state: String) {
button_google.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(googleUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
} }
override fun enableLoginByLinkedin() { override fun enableLoginByLinkedin() {
button_linkedin.isEnabled = true button_linkedin.isClickable = true
}
override fun setupLinkedinButtonListener(linkedinUrl: String, state: String) {
button_linkedin.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(linkedinUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
} }
override fun enableLoginByMeteor() { override fun enableLoginByMeteor() {
button_meteor.isEnabled = true button_meteor.isClickable = true
} }
override fun enableLoginByTwitter() { override fun enableLoginByTwitter() {
button_twitter.isEnabled = true button_twitter.isClickable = true
} }
override fun enableLoginByGitlab() { override fun enableLoginByGitlab() {
button_gitlab.isEnabled = true button_gitlab.isClickable = true
}
override fun setupGitlabButtonListener(gitlabUrl: String, state: String) {
button_gitlab.setOnClickListener {
startActivityForResult(context?.oauthWebViewIntent(gitlabUrl, state), REQUEST_CODE_FOR_OAUTH)
activity?.overridePendingTransition(R.anim.slide_up, R.anim.hold)
}
} }
override fun setupFabListener() { override fun setupFabListener() {
...@@ -249,12 +292,37 @@ class LoginFragment : Fragment(), LoginView { ...@@ -249,12 +292,37 @@ class LoginFragment : Fragment(), LoginView {
text_password.requestFocus() text_password.requestFocus()
} }
override fun alertRequiresUsername() {
showToast(getString(R.string.msg_requires_username), Toast.LENGTH_LONG)
}
override fun alertNotRecommendedVersion() {
context?.let {
AlertDialog.Builder(it)
.setMessage(getString(R.string.msg_ver_not_recommended, BuildConfig.RECOMMENDED_SERVER_VERSION))
.setPositiveButton(R.string.msg_ok, null)
.create()
.show()
}
}
override fun blockAndAlertNotRequiredVersion() {
context?.let {
AlertDialog.Builder(it)
.setMessage(getString(R.string.msg_ver_not_minimum, BuildConfig.REQUIRED_SERVER_VERSION))
.setOnDismissListener { activity?.onBackPressed() }
.setPositiveButton(R.string.msg_ok, null)
.create()
.show()
}
}
private fun showRemainingSocialAccountsView() { private fun showRemainingSocialAccountsView() {
social_accounts_container.postDelayed({ social_accounts_container.postDelayed({
(0..social_accounts_container.childCount) (0..social_accounts_container.childCount)
.mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton } .mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
.filter { it.isEnabled } .filter { it.isClickable }
.forEach { it.visibility = View.VISIBLE } .forEach { it.setVisible(true)}
}, 1000) }, 1000)
} }
...@@ -284,13 +352,23 @@ class LoginFragment : Fragment(), LoginView { ...@@ -284,13 +352,23 @@ class LoginFragment : Fragment(), LoginView {
} }
private fun showThreeSocialAccountsMethods() { private fun showThreeSocialAccountsMethods() {
var count = 0 (0..social_accounts_container.childCount)
for (i in 0..social_accounts_container.childCount) { .mapNotNull { social_accounts_container.getChildAt(it) as? ImageButton }
val view = social_accounts_container.getChildAt(i) as? ImageButton ?: continue .filter { it.isClickable }
if (view.isEnabled && count < 3) { .take(3)
view.visibility = View.VISIBLE .forEach { it.setVisible(true) }
count++ }
fun showOauthView() {
if (isOauthViewEnable) {
social_accounts_container.setVisible(true)
} }
} }
fun hideOauthView() {
if (isOauthViewEnable) {
social_accounts_container.setVisible(false)
button_fab.setVisible(false)
}
} }
} }
\ No newline at end of file
package chat.rocket.android.authentication.presentation package chat.rocket.android.authentication.presentation
import android.content.Context
import android.content.Intent import android.content.Intent
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.login.ui.LoginFragment import chat.rocket.android.authentication.login.ui.LoginFragment
import chat.rocket.android.authentication.signup.ui.SignupFragment import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack import chat.rocket.android.util.extensions.addFragmentBackStack
import chat.rocket.android.webview.ui.webViewIntent import chat.rocket.android.webview.ui.webViewIntent
class AuthenticationNavigator(internal val activity: AuthenticationActivity, internal val context: Context) { class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
fun toLogin() { fun toLogin() {
activity.addFragmentBackStack("LoginFragment", R.id.fragment_container) { activity.addFragmentBackStack("LoginFragment", R.id.fragment_container) {
...@@ -32,7 +33,7 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int ...@@ -32,7 +33,7 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int
} }
fun toWebPage(url: String) { fun toWebPage(url: String) {
activity.startActivity(context.webViewIntent(url)) activity.startActivity(activity.webViewIntent(url))
activity.overridePendingTransition(R.anim.slide_up, R.anim.hold) activity.overridePendingTransition(R.anim.slide_up, R.anim.hold)
} }
...@@ -41,7 +42,13 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int ...@@ -41,7 +42,13 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity, int
activity.finish() activity.finish()
} }
fun toChatList(serverUrl: String) {
activity.startActivity(activity.changeServerIntent(serverUrl))
activity.finish()
}
fun toServerScreen() { fun toServerScreen() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. activity.startActivity(activity.newServerIntent())
activity.finish()
} }
} }
\ No newline at end of file
package chat.rocket.android.authentication.presentation package chat.rocket.android.authentication.presentation
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.common.model.Token import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.core.TokenRepository
import javax.inject.Inject import javax.inject.Inject
class AuthenticationPresenter @Inject constructor(private val navigator: AuthenticationNavigator, class AuthenticationPresenter @Inject constructor(
private val navigator: AuthenticationNavigator,
private val getCurrentServerInteractor: GetCurrentServerInteractor, private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val multiServerRepository: MultiServerTokenRepository, private val getAccountInteractor: GetAccountInteractor,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val tokenRepository: TokenRepository) { private val localRepository: LocalRepository,
private val tokenRepository: TokenRepository
fun loadCredentials(callback: (authenticated: Boolean) -> Unit) { ) {
suspend fun loadCredentials(newServer: Boolean, callback: (authenticated: Boolean) -> Unit) {
val currentServer = getCurrentServerInteractor.get() val currentServer = getCurrentServerInteractor.get()
val serverToken = currentServer?.let { multiServerRepository.get(currentServer) } val serverToken = currentServer?.let { tokenRepository.get(currentServer) }
val settings = currentServer?.let { settingsRepository.get(currentServer) } val settings = currentServer?.let { settingsRepository.get(currentServer) }
val account = currentServer?.let { getAccountInteractor.get(currentServer) }
account?.let {
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, account.userName)
}
if (currentServer == null || serverToken == null || settings == null) { if (newServer || currentServer == null || serverToken == null || settings == null) {
callback(false) callback(false)
} else { } else {
tokenRepository.save(Token(serverToken.userId, serverToken.authToken))
callback(true) callback(true)
navigator.toChatList() navigator.toChatList()
} }
......
...@@ -4,6 +4,7 @@ import chat.rocket.android.authentication.presentation.AuthenticationNavigator ...@@ -4,6 +4,7 @@ import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
...@@ -14,12 +15,20 @@ class ServerPresenter @Inject constructor(private val view: ServerView, ...@@ -14,12 +15,20 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveCurrentServerInteractor, private val serverInteractor: SaveCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor) { private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor) {
fun connect(server: String) { fun connect(server: String) {
if (!UrlHelper.isValidUrl(server)) { if (!UrlHelper.isValidUrl(server)) {
view.showInvalidServerUrlMessage() view.showInvalidServerUrlMessage()
} else { } else {
launchUI(strategy) { launchUI(strategy) {
// Check if we already have an account for this server...
val account = getAccountsInteractor.get().firstOrNull { it.serverUrl == server }
if (account != null) {
navigator.toChatList(server)
return@launchUI
}
if (NetworkHelper.hasInternetAccess()) { if (NetworkHelper.hasInternetAccess()) {
view.showLoading() view.showLoading()
try { try {
......
package chat.rocket.android.authentication.server.presentation
interface VersionCheckView {
/**
* Alerts the user about the server version not meeting the recommended server version.
*/
fun alertNotRecommendedVersion()
/**
* Block user to proceed and alert him due to server having an unsupported server version.
*/
fun blockAndAlertNotRequiredVersion()
}
\ No newline at end of file
...@@ -56,13 +56,21 @@ class ServerFragment : Fragment(), ServerView { ...@@ -56,13 +56,21 @@ class ServerFragment : Fragment(), ServerView {
enableUserInput(true) enableUserInput(true)
} }
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int){
showToast(resId)
}
override fun showMessage(message: String) = showToast(message) override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection)) override fun showNoInternetConnection() {
showMessage(getString(R.string.msg_no_internet_connection))
}
private fun enableUserInput(value: Boolean) { private fun enableUserInput(value: Boolean) {
button_connect.isEnabled = value button_connect.isEnabled = value
......
...@@ -5,9 +5,12 @@ import chat.rocket.android.core.lifecycle.CancelStrategy ...@@ -5,9 +5,12 @@ import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
...@@ -15,6 +18,7 @@ import chat.rocket.core.internal.rest.login ...@@ -15,6 +18,7 @@ import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.registerPushToken
import chat.rocket.core.internal.rest.signup import chat.rocket.core.internal.rest.signup
import chat.rocket.core.model.Myself
import javax.inject.Inject import javax.inject.Inject
class SignupPresenter @Inject constructor(private val view: SignupView, class SignupPresenter @Inject constructor(private val view: SignupView,
...@@ -22,8 +26,13 @@ class SignupPresenter @Inject constructor(private val view: SignupView, ...@@ -22,8 +26,13 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory) { private val factory: RocketChatClientFactory,
private val client: RocketChatClient = factory.create(serverInteractor.get()!!) private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
fun signup(name: String, username: String, password: String, email: String) { fun signup(name: String, username: String, password: String, email: String) {
val server = serverInteractor.get() val server = serverInteractor.get()
...@@ -55,7 +64,8 @@ class SignupPresenter @Inject constructor(private val view: SignupView, ...@@ -55,7 +64,8 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
// TODO This function returns a user token so should we save it? // TODO This function returns a user token so should we save it?
client.login(username, password) client.login(username, password)
val me = client.me() val me = client.me()
localRepository.save(LocalRepository.USERNAME_KEY, me.username) localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me)
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
...@@ -90,8 +100,21 @@ class SignupPresenter @Inject constructor(private val view: SignupView, ...@@ -90,8 +100,21 @@ class SignupPresenter @Inject constructor(private val view: SignupView,
private suspend fun registerPushToken() { private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let { localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it) client.registerPushToken(it, getAccountsInteractor.get(), factory)
} }
// TODO: Schedule push token registering when it comes up null // TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
}
private suspend fun saveAccount(me: Myself) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val thumb = UrlHelper.getAvatarUrl(currentServer, me.username!!)
val account = Account(currentServer, icon, logo, me.username!!, thumb)
saveAccountInteractor.save(account)
} }
} }
\ No newline at end of file
...@@ -97,9 +97,13 @@ class SignupFragment : Fragment(), SignupView { ...@@ -97,9 +97,13 @@ class SignupFragment : Fragment(), SignupView {
enableUserInput(true) enableUserInput(true)
} }
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message) override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() { override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error)) showMessage(getString(R.string.msg_generic_error))
......
package chat.rocket.android.authentication.twofactor.presentation package chat.rocket.android.authentication.twofactor.presentation
import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.authentication.presentation.AuthenticationNavigator import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.MultiServerTokenRepository import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatAuthException import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.me
import chat.rocket.core.model.Myself
import javax.inject.Inject import javax.inject.Inject
class TwoFAPresenter @Inject constructor(private val view: TwoFAView, class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator, private val navigator: AuthenticationNavigator,
private val multiServerRepository: MultiServerTokenRepository, private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory) { private val factory: RocketChatClientFactory,
private val client: RocketChatClient = factory.create(serverInteractor.get()!!) private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
// TODO: If the usernameOrEmail and password was informed by the user on the previous screen, then we should pass only the pin, like this: fun authenticate(pin: EditText) // TODO: If the usernameOrEmail and password was informed by the user on the previous screen, then we should pass only the pin, like this: fun authenticate(pin: EditText)
fun authenticate(usernameOrEmail: String, password: String, twoFactorAuthenticationCode: String) { fun authenticate(usernameOrEmail: String, password: String, twoFactorAuthenticationCode: String) {
...@@ -45,10 +52,9 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView, ...@@ -45,10 +52,9 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
// The token is saved via the client TokenProvider // The token is saved via the client TokenProvider
val token = val token =
client.login(usernameOrEmail, password, twoFactorAuthenticationCode) client.login(usernameOrEmail, password, twoFactorAuthenticationCode)
multiServerRepository.save( val me = client.me()
server, saveAccount(me)
TokenModel(token.userId, token.authToken) tokenRepository.save(server, token)
)
registerPushToken() registerPushToken()
navigator.toChatList() navigator.toChatList()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
...@@ -76,8 +82,21 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView, ...@@ -76,8 +82,21 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private suspend fun registerPushToken() { private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let { localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it) client.registerPushToken(it, getAccountsInteractor.get(), factory)
} }
// TODO: Schedule push token registering when it comes up null // TODO: When the push token is null, at some point we should receive it with
// onTokenRefresh() on FirebaseTokenService, we need to confirm it.
}
private suspend fun saveAccount(me: Myself) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val logo = settings.wideTile()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val thumb = UrlHelper.getAvatarUrl(currentServer, me.username!!)
val account = Account(currentServer, icon, logo, me.username!!, thumb)
saveAccountInteractor.save(account)
} }
} }
\ No newline at end of file
...@@ -66,7 +66,9 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -66,7 +66,9 @@ class TwoFAFragment : Fragment(), TwoFAView {
text_two_factor_auth.shake() text_two_factor_auth.shake()
} }
override fun alertInvalidTwoFactorAuthenticationCode() = showMessage(getString(R.string.msg_invalid_2fa_code)) override fun alertInvalidTwoFactorAuthenticationCode() {
showMessage(getString(R.string.msg_invalid_2fa_code))
}
override fun showLoading() { override fun showLoading() {
enableUserInput(false) enableUserInput(false)
...@@ -78,9 +80,13 @@ class TwoFAFragment : Fragment(), TwoFAView { ...@@ -78,9 +80,13 @@ class TwoFAFragment : Fragment(), TwoFAView {
enableUserInput(true) enableUserInput(true)
} }
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message) override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
package chat.rocket.android.authentication.ui package chat.rocket.android.authentication.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
...@@ -7,40 +9,58 @@ import chat.rocket.android.R ...@@ -7,40 +9,58 @@ import chat.rocket.android.R
import chat.rocket.android.authentication.presentation.AuthenticationPresenter import chat.rocket.android.authentication.presentation.AuthenticationPresenter
import chat.rocket.android.authentication.server.ui.ServerFragment import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.util.extensions.addFragment import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.launchUI
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector import dagger.android.support.HasSupportFragmentInjector
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import javax.inject.Inject import javax.inject.Inject
class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector { class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject lateinit var presenter: AuthenticationPresenter @Inject lateinit var presenter: AuthenticationPresenter
val job = Job()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this) AndroidInjection.inject(this)
presenter.loadCredentials { authenticated -> setContentView(R.layout.activity_authentication)
if (authenticated) { setTheme(R.style.AuthenticationTheme)
// just call onCreate, and the presenter will call the navigator...
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} else {
launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
presenter.loadCredentials(newServer) { authenticated ->
if (!authenticated) {
showServerInput(savedInstanceState) showServerInput(savedInstanceState)
} }
} }
} }
}
override fun onDestroy() {
job.cancel()
super.onDestroy()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> { override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector return fragmentDispatchingAndroidInjector
} }
fun showServerInput(savedInstanceState: Bundle?) { fun showServerInput(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_authentication)
setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState)
addFragment("ServerFragment", R.id.fragment_container) { addFragment("ServerFragment", R.id.fragment_container) {
ServerFragment.newInstance() ServerFragment.newInstance()
} }
} }
} }
const val INTENT_ADD_NEW_SERVER = "INTENT_ADD_NEW_SERVER"
fun Context.newServerIntent(): Intent {
return Intent(this, AuthenticationActivity::class.java).apply {
putExtra(INTENT_ADD_NEW_SERVER, true)
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
\ No newline at end of file
...@@ -2,9 +2,9 @@ package chat.rocket.android.chatroom.adapter ...@@ -2,9 +2,9 @@ package chat.rocket.android.chatroom.adapter
import android.support.annotation.IntDef import android.support.annotation.IntDef
const val PEOPLE = 0L const val PEOPLE = 0
const val ROOMS = 1L const val ROOMS = 1
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
@IntDef(value = [PEOPLE, ROOMS]) @IntDef(PEOPLE, ROOMS)
annotation class AutoCompleteType annotation class AutoCompleteType
...@@ -49,6 +49,10 @@ class ChatRoomAdapter( ...@@ -49,6 +49,10 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.message_url_preview) val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view, actionsListener, reactionListener) UrlPreviewViewHolder(view, actionsListener, reactionListener)
} }
BaseViewModel.ViewType.MESSAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_message_attachment)
MessageAttachmentViewHolder(view, actionsListener, reactionListener)
}
else -> { else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}") throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
} }
...@@ -87,6 +91,7 @@ class ChatRoomAdapter( ...@@ -87,6 +91,7 @@ class ChatRoomAdapter(
is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel) is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel)
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel) is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel) is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
} }
} }
...@@ -117,19 +122,22 @@ class ChatRoomAdapter( ...@@ -117,19 +122,22 @@ class ChatRoomAdapter(
fun updateItem(message: BaseViewModel<*>) { fun updateItem(message: BaseViewModel<*>) {
var index = dataSet.indexOfLast { it.messageId == message.messageId } var index = dataSet.indexOfLast { it.messageId == message.messageId }
val indexOfFirst = dataSet.indexOfFirst { it.messageId == message.messageId } val indexOfNext = dataSet.indexOfFirst { it.messageId == message.messageId }
Timber.d("index: $index") Timber.d("index: $index")
if (index > -1) { if (index > -1) {
dataSet[index] = message dataSet[index] = message
dataSet.forEachIndexed { index, viewModel ->
if (viewModel.messageId == message.messageId) {
if (viewModel.nextDownStreamMessage == null) {
viewModel.reactions = message.reactions
}
notifyItemChanged(index) notifyItemChanged(index)
while (dataSet[index].nextDownStreamMessage != null) { }
dataSet[index].nextDownStreamMessage!!.reactions = message.reactions
notifyItemChanged(--index)
} }
// Delete message only if current is a system message update, i.e.: Message Removed // Delete message only if current is a system message update, i.e.: Message Removed
if (message.message.isSystemMessage() && indexOfFirst > -1 && indexOfFirst != index) { if (message.message.isSystemMessage() && indexOfNext > -1 && indexOfNext != index) {
dataSet.removeAt(indexOfFirst) dataSet.removeAt(indexOfNext)
notifyItemRemoved(indexOfFirst) notifyItemRemoved(indexOfNext)
} }
} }
} }
......
...@@ -12,7 +12,7 @@ import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder ...@@ -12,7 +12,7 @@ import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
class CommandSuggestionsAdapter : SuggestionsAdapter<CommandSuggestionsViewHolder>(token = "/", class CommandSuggestionsAdapter : SuggestionsAdapter<CommandSuggestionsViewHolder>(token = "/",
constraint = CONSTRAINT_BOUND_TO_START, threshold = UNLIMITED_RESULT_COUNT) { constraint = CONSTRAINT_BOUND_TO_START, threshold = RESULT_COUNT_UNLIMITED) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommandSuggestionsViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommandSuggestionsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_command_item, parent, val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_command_item, parent,
......
package chat.rocket.android.chatroom.adapter
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message.view.*
class MessageAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
text_content.movementMethod = LinkMovementMethod()
setupActionMenu(text_content)
}
}
override fun bindViews(data: MessageAttachmentViewModel) {
with(itemView) {
text_message_time.text = data.time
text_sender.text = data.senderName
text_content.text = data.content
}
}
}
\ No newline at end of file
...@@ -97,7 +97,7 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() ...@@ -97,7 +97,7 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
val countTextView = findViewById<TextView>(R.id.text_count) val countTextView = findViewById<TextView>(R.id.text_count)
emojiTextView.text = reaction.unicode emojiTextView.text = reaction.unicode
countTextView.text = reaction.count.toString() countTextView.text = reaction.count.toString()
val myself = localRepository.get(LocalRepository.USERNAME_KEY) val myself = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
if (reaction.usernames.contains(myself)) { if (reaction.usernames.contains(myself)) {
val context = itemView.context val context = itemView.context
val resources = context.resources val resources = context.resources
......
package chat.rocket.android.chatroom.adapter package chat.rocket.android.chatroom.adapter
import DrawableHelper import DrawableHelper
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
...@@ -13,10 +14,32 @@ import chat.rocket.android.util.extensions.setVisible ...@@ -13,10 +14,32 @@ import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.autocompletion.model.SuggestionModel import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder import chat.rocket.android.widget.autocompletion.ui.BaseSuggestionViewHolder
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter
import chat.rocket.common.model.UserStatus
import com.facebook.drawee.view.SimpleDraweeView import com.facebook.drawee.view.SimpleDraweeView
class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>("@") { class PeopleSuggestionsAdapter(context: Context) : SuggestionsAdapter<PeopleSuggestionViewHolder>("@") {
init {
val allDescription = context.getString(R.string.suggest_all_description)
val hereDescription = context.getString(R.string.suggest_here_description)
val pinnedList = listOf(
PeopleSuggestionViewModel(imageUri = null,
text = "all",
username = "all",
name = allDescription,
status = null,
pinned = false,
searchList = listOf("all")),
PeopleSuggestionViewModel(imageUri = null,
text = "here",
username = "here",
name = hereDescription,
status = null,
pinned = false,
searchList = listOf("here"))
)
setPinnedSuggestions(pinnedList)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeopleSuggestionViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeopleSuggestionViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_member_item, parent, val view = LayoutInflater.from(parent.context).inflate(R.layout.suggestion_member_item, parent,
false) false)
...@@ -34,15 +57,19 @@ class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>( ...@@ -34,15 +57,19 @@ class PeopleSuggestionsAdapter : SuggestionsAdapter<PeopleSuggestionViewHolder>(
val statusView = itemView.findViewById<ImageView>(R.id.image_status) val statusView = itemView.findViewById<ImageView>(R.id.image_status)
username.text = item.username username.text = item.username
name.text = item.name name.text = item.name
if (item.imageUri.isEmpty()) { if (item.imageUri?.isEmpty() != false) {
avatar.setVisible(false) avatar.setVisible(false)
} else { } else {
avatar.setVisible(true) avatar.setVisible(true)
avatar.setImageURI(item.imageUri) avatar.setImageURI(item.imageUri)
} }
val status = item.status ?: UserStatus.Offline() val status = item.status
if (status != null) {
val statusDrawable = DrawableHelper.getUserStatusDrawable(status, itemView.context) val statusDrawable = DrawableHelper.getUserStatusDrawable(status, itemView.context)
statusView.setImageDrawable(statusDrawable) statusView.setImageDrawable(statusDrawable)
} else {
statusView.setVisible(false)
}
setOnClickListener { setOnClickListener {
itemClickListener?.onClick(item) itemClickListener?.onClick(item)
} }
......
...@@ -53,7 +53,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -53,7 +53,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer) private val manager = factory.create(currentServer)
private val client = manager.client private val client = manager.client
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private val messagesChannel = Channel<Message>() private val messagesChannel = Channel<Message>()
private var chatRoomId: String? = null private var chatRoomId: String? = null
...@@ -291,7 +291,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -291,7 +291,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
view.showReplyingAction( view.showReplyingAction(
username = user, username = user,
replyMarkdown = "[ ]($serverUrl/$room/$roomName?msg=$id) $mention ", replyMarkdown = "[ ]($serverUrl/$room/$roomName?msg=$id) $mention ",
quotedMessage = m.message quotedMessage = mapper.map(message).last().preview?.message ?: ""
) )
} }
} }
...@@ -364,7 +364,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -364,7 +364,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
try { try {
val members = client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 50).result val members = client.getMembers(chatRoomId, roomTypeOf(chatRoomType), offset, 50).result
usersRepository.saveAll(members) usersRepository.saveAll(members)
val self = localRepository.get(LocalRepository.USERNAME_KEY) val self = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
// Take at most the 100 most recent messages distinguished by user. Can return less. // Take at most the 100 most recent messages distinguished by user. Can return less.
val recentMessages = messagesRepository.getRecentMessages(chatRoomId, 100) val recentMessages = messagesRepository.getRecentMessages(chatRoomId, 100)
.filterNot { filterSelfOut && it.sender?.username == self } .filterNot { filterSelfOut && it.sender?.username == self }
...@@ -402,7 +402,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -402,7 +402,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
} }
} }
fun spotlight(query: String, @AutoCompleteType type: Long, filterSelfOut: Boolean = false) { fun spotlight(query: String, @AutoCompleteType type: Int, filterSelfOut: Boolean = false) {
launchUI(strategy) { launchUI(strategy) {
try { try {
val (users, rooms) = client.spotlight(query) val (users, rooms) = client.spotlight(query)
...@@ -411,7 +411,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -411,7 +411,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
if (users.isNotEmpty()) { if (users.isNotEmpty()) {
usersRepository.saveAll(users) usersRepository.saveAll(users)
} }
val self = localRepository.get(LocalRepository.USERNAME_KEY) val self = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
view.populatePeopleSuggestions(users.map { view.populatePeopleSuggestions(users.map {
val username = it.username ?: "" val username = it.username ?: ""
val name = it.name ?: "" val name = it.name ?: ""
...@@ -499,7 +499,6 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView, ...@@ -499,7 +499,6 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
//TODO: cache the commands //TODO: cache the commands
val commands = client.commands(0, 100).result val commands = client.commands(0, 100).result
view.populateCommandSuggestions(commands.map { view.populateCommandSuggestions(commands.map {
println("${it.command} - ${it.description}")
CommandSuggestionViewModel(it.command, it.description ?: "", listOf(it.command)) CommandSuggestionViewModel(it.command, it.description ?: "", listOf(it.command))
}) })
} catch (ex: RocketChatException) { } catch (ex: RocketChatException) {
......
...@@ -24,7 +24,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag ...@@ -24,7 +24,7 @@ class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessag
getSettingsInteractor: GetSettingsInteractor) { getSettingsInteractor: GetSettingsInteractor) {
private val client = factory.create(serverInteractor.get()!!) private val client = factory.create(serverInteractor.get()!!)
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private var pinnedMessagesListOffset: Int = 0 private var pinnedMessagesListOffset: Int = 0
/** /**
......
package chat.rocket.android.chatroom.ui package chat.rocket.android.chatroom.ui
import android.graphics.drawable.Drawable
import android.support.design.widget.BaseTransientBottomBar import android.support.design.widget.BaseTransientBottomBar
import android.support.v4.view.ViewCompat import android.support.v4.view.ViewCompat
import android.text.Spannable import android.text.Spannable
import android.text.SpannableStringBuilder
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
...@@ -27,7 +27,6 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> { ...@@ -27,7 +27,6 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
actionSnackbar.cancelView = view.findViewById(R.id.image_view_action_cancel_quote) as ImageView actionSnackbar.cancelView = view.findViewById(R.id.image_view_action_cancel_quote) as ImageView
actionSnackbar.duration = BaseTransientBottomBar.LENGTH_INDEFINITE actionSnackbar.duration = BaseTransientBottomBar.LENGTH_INDEFINITE
val spannable = Markwon.markdown(context, content).trim() val spannable = Markwon.markdown(context, content).trim()
actionSnackbar.marginDrawable = context.getDrawable(R.drawable.quote)
actionSnackbar.messageTextView.content = spannable actionSnackbar.messageTextView.content = spannable
return actionSnackbar return actionSnackbar
} }
...@@ -37,19 +36,16 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> { ...@@ -37,19 +36,16 @@ class ActionSnackbar : BaseTransientBottomBar<ActionSnackbar> {
lateinit var cancelView: View lateinit var cancelView: View
private lateinit var messageTextView: TextView private lateinit var messageTextView: TextView
private lateinit var titleTextView: TextView private lateinit var titleTextView: TextView
private lateinit var marginDrawable: Drawable
var text: String = "" var text: String = ""
set(value) { set(value) {
val spannable = parser.renderMarkdown(value) as Spannable val spannable = SpannableStringBuilder.valueOf(value)
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
messageTextView.content = spannable messageTextView.content = spannable
} }
var title: String = "" var title: String = ""
set(value) { set(value) {
val spannable = Markwon.markdown(this.context, value) as Spannable val spannable = Markwon.markdown(this.context, value) as Spannable
spannable.setSpan(MessageParser.QuoteMarginSpan(marginDrawable, 10), 0, spannable.length, 0)
titleTextView.content = spannable titleTextView.content = spannable
} }
......
...@@ -102,11 +102,8 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -102,11 +102,8 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector return fragmentDispatchingAndroidInjector
} }
private fun setupToolbar() { fun showRoomTypeIcon(showRoomTypeIcon: Boolean) {
setSupportActionBar(toolbar) if (showRoomTypeIcon) {
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName
val roomType = roomTypeOf(chatRoomType) val roomType = roomTypeOf(chatRoomType)
val drawable = when (roomType) { val drawable = when (roomType) {
is RoomType.Channel -> { is RoomType.Channel -> {
...@@ -127,6 +124,17 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector { ...@@ -127,6 +124,17 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
DrawableHelper.tintDrawable(mutableDrawable, this, R.color.white) DrawableHelper.tintDrawable(mutableDrawable, this, R.color.white)
DrawableHelper.compoundDrawable(text_room_name, mutableDrawable) DrawableHelper.compoundDrawable(text_room_name, mutableDrawable)
} }
} else {
text_room_name.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName
showRoomTypeIcon(true)
toolbar.setNavigationOnClickListener { toolbar.setNavigationOnClickListener {
finishActivity() finishActivity()
......
...@@ -37,7 +37,9 @@ import kotlinx.android.synthetic.main.message_attachment_options.* ...@@ -37,7 +37,9 @@ import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.message_composer.* import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.* import kotlinx.android.synthetic.main.message_list.*
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.absoluteValue
fun newInstance(chatRoomId: String, fun newInstance(chatRoomId: String,
chatRoomName: String, chatRoomName: String,
...@@ -89,6 +91,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -89,6 +91,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private val centerX by lazy { recycler_view.right } private val centerX by lazy { recycler_view.right }
private val centerY by lazy { recycler_view.bottom } private val centerY by lazy { recycler_view.bottom }
private val handler = Handler() private val handler = Handler()
private var verticalScrollOffset = AtomicInteger(0)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
...@@ -121,6 +124,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -121,6 +124,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
setupMessageComposer() setupMessageComposer()
setupSuggestionsView() setupSuggestionsView()
setupActionSnackbar() setupActionSnackbar()
activity?.apply {
(this as? ChatRoomActivity)?.showRoomTypeIcon(true)
}
} }
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
...@@ -209,13 +216,54 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -209,13 +216,54 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
} }
}) })
} }
recycler_view.addOnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
val y = oldBottom - bottom
if (y.absoluteValue > 0) {
// if y is positive the keyboard is up else it's down
recycler_view.post {
if (y > 0 || verticalScrollOffset.get().absoluteValue >= y.absoluteValue) {
recycler_view.scrollBy(0, y)
} else {
recycler_view.scrollBy(0, verticalScrollOffset.get())
}
}
}
}
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
var state = AtomicInteger(RecyclerView.SCROLL_STATE_IDLE)
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
state.compareAndSet(RecyclerView.SCROLL_STATE_IDLE, newState)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
if (!state.compareAndSet(RecyclerView.SCROLL_STATE_SETTLING, newState)) {
state.compareAndSet(RecyclerView.SCROLL_STATE_DRAGGING, newState)
}
}
RecyclerView.SCROLL_STATE_DRAGGING -> {
state.compareAndSet(RecyclerView.SCROLL_STATE_IDLE, newState)
}
RecyclerView.SCROLL_STATE_SETTLING -> {
state.compareAndSet(RecyclerView.SCROLL_STATE_DRAGGING, newState)
}
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (state.get() != RecyclerView.SCROLL_STATE_IDLE) {
verticalScrollOffset.getAndAdd(dy)
}
}
})
} }
val oldMessagesCount = adapter.itemCount val oldMessagesCount = adapter.itemCount
adapter.appendData(dataSet) adapter.appendData(dataSet)
recycler_view.scrollToPosition(92)
if (oldMessagesCount == 0 && dataSet.isNotEmpty()) { if (oldMessagesCount == 0 && dataSet.isNotEmpty()) {
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
} }
presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true) presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true)
} }
...@@ -236,11 +284,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -236,11 +284,14 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
presenter.uploadFile(chatRoomId, uri, "") presenter.uploadFile(chatRoomId, uri, "")
} }
override fun showInvalidFileMessage() = showMessage(getString(R.string.msg_invalid_file)) override fun showInvalidFileMessage() {
showMessage(getString(R.string.msg_invalid_file))
}
override fun showNewMessage(message: List<BaseViewModel<*>>) { override fun showNewMessage(message: List<BaseViewModel<*>>) {
adapter.prependData(message) adapter.prependData(message)
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
} }
override fun disableSendMessageButton() { override fun disableSendMessageButton() {
...@@ -281,6 +332,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -281,6 +332,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
if (!recycler_view.isAtBottom()) { if (!recycler_view.isAtBottom()) {
if (adapter.itemCount > 0) { if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
} }
} }
} }
...@@ -290,9 +342,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -290,9 +342,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun hideLoading() = view_loading.setVisible(false) override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(message: String) = showToast(message) override fun showMessage(message: String) {
showToast(message)
}
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
...@@ -426,6 +482,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -426,6 +482,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private fun setupFab() { private fun setupFab() {
button_fab.setOnClickListener { button_fab.setOnClickListener {
recycler_view.scrollToPosition(0) recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
button_fab.hide() button_fab.hide()
} }
} }
...@@ -450,11 +507,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -450,11 +507,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
emojiKeyboardPopup.listener = this emojiKeyboardPopup.listener = this
text_message.listener = object : ComposerEditText.ComposerEditTextListener { text_message.listener = object : ComposerEditText.ComposerEditTextListener {
override fun onKeyboardOpened() { override fun onKeyboardOpened() {
if (recycler_view.isAtBottom()) {
if (adapter.itemCount > 0) {
recycler_view.scrollToPosition(0)
}
}
} }
override fun onKeyboardClosed() { override fun onKeyboardClosed() {
...@@ -521,7 +573,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR ...@@ -521,7 +573,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
private fun setupSuggestionsView() { private fun setupSuggestionsView() {
suggestions_view.anchorTo(text_message) suggestions_view.anchorTo(text_message)
.setMaximumHeight(resources.getDimensionPixelSize(R.dimen.suggestions_box_max_height)) .setMaximumHeight(resources.getDimensionPixelSize(R.dimen.suggestions_box_max_height))
.addTokenAdapter(PeopleSuggestionsAdapter()) .addTokenAdapter(PeopleSuggestionsAdapter(context!!))
.addTokenAdapter(CommandSuggestionsAdapter()) .addTokenAdapter(CommandSuggestionsAdapter())
.addTokenAdapter(RoomSuggestionsAdapter()) .addTokenAdapter(RoomSuggestionsAdapter())
.addSuggestionProviderAction("@") { query -> .addSuggestionProviderAction("@") { query ->
......
...@@ -66,9 +66,13 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView { ...@@ -66,9 +66,13 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
override fun hideLoading() = view_loading.setVisible(false) override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message) override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
...@@ -12,8 +12,9 @@ data class AudioAttachmentViewModel( ...@@ -12,8 +12,9 @@ data class AudioAttachmentViewModel(
override val attachmentTitle: CharSequence, override val attachmentTitle: CharSequence,
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null override var nextDownStreamMessage: BaseViewModel<*>? = null,
) : BaseFileAttachmentViewModel<AudioAttachment> { override var preview: Message? = null
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
override val layoutId: Int override val layoutId: Int
......
...@@ -11,6 +11,7 @@ interface BaseViewModel<out T> { ...@@ -11,6 +11,7 @@ interface BaseViewModel<out T> {
val layoutId: Int val layoutId: Int
var reactions: List<ReactionViewModel> var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>? var nextDownStreamMessage: BaseViewModel<*>?
var preview: Message?
enum class ViewType(val viewType: Int) { enum class ViewType(val viewType: Int) {
MESSAGE(0), MESSAGE(0),
......
...@@ -12,7 +12,8 @@ data class ImageAttachmentViewModel( ...@@ -12,7 +12,8 @@ data class ImageAttachmentViewModel(
override val attachmentTitle: CharSequence, override val attachmentTitle: CharSequence,
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
) : BaseFileAttachmentViewModel<ImageAttachment> { ) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
data class MessageAttachmentViewModel(
override val message: Message,
override val rawData: Message,
override val messageId: String,
var senderName: String,
val time: CharSequence,
val content: CharSequence,
val isPinned: Boolean,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null,
override var preview: Message? = null
) : BaseViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_message_attachment
}
\ No newline at end of file
...@@ -14,6 +14,7 @@ data class MessageViewModel( ...@@ -14,6 +14,7 @@ data class MessageViewModel(
override val isPinned: Boolean, override val isPinned: Boolean,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null, override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
var isFirstUnread: Boolean var isFirstUnread: Boolean
) : BaseMessageViewModel<Message> { ) : BaseMessageViewModel<Message> {
override val viewType: Int override val viewType: Int
......
...@@ -13,7 +13,8 @@ data class UrlPreviewViewModel( ...@@ -13,7 +13,8 @@ data class UrlPreviewViewModel(
val description: CharSequence?, val description: CharSequence?,
val thumbUrl: String?, val thumbUrl: String?,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
) : BaseViewModel<Url> { ) : BaseViewModel<Url> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
...@@ -12,7 +12,8 @@ data class VideoAttachmentViewModel( ...@@ -12,7 +12,8 @@ data class VideoAttachmentViewModel(
override val attachmentTitle: CharSequence, override val attachmentTitle: CharSequence,
override val id: Long, override val id: Long,
override var reactions: List<ReactionViewModel>, override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
) : BaseFileAttachmentViewModel<VideoAttachment> { ) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
...@@ -4,9 +4,7 @@ import DateTimeHelper ...@@ -4,9 +4,7 @@ import DateTimeHelper
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan import android.text.style.StyleSpan
import chat.rocket.android.R import chat.rocket.android.R
...@@ -15,7 +13,6 @@ import chat.rocket.android.helper.UrlHelper ...@@ -15,7 +13,6 @@ import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.core.TokenRepository
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import chat.rocket.core.model.MessageType import chat.rocket.core.model.MessageType
import chat.rocket.core.model.Value import chat.rocket.core.model.Value
...@@ -25,22 +22,23 @@ import chat.rocket.core.model.url.Url ...@@ -25,22 +22,23 @@ import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
import okhttp3.HttpUrl import okhttp3.HttpUrl
import timber.log.Timber
import java.security.InvalidParameterException import java.security.InvalidParameterException
import javax.inject.Inject import javax.inject.Inject
class ViewModelMapper @Inject constructor(private val context: Context, class ViewModelMapper @Inject constructor(private val context: Context,
private val parser: MessageParser, private val parser: MessageParser,
private val messagesRepository: MessagesRepository, private val messagesRepository: MessagesRepository,
private val getAccountInteractor: GetAccountInteractor,
tokenRepository: TokenRepository, tokenRepository: TokenRepository,
localRepository: LocalRepository,
serverInteractor: GetCurrentServerInteractor, serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor) { getSettingsInteractor: GetSettingsInteractor,
localRepository: LocalRepository) {
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)!! private val currentServer = serverInteractor.get()!!
private val settings: Map<String, Value<Any>> = getSettingsInteractor.get(currentServer)
private val baseUrl = settings.baseUrl() private val baseUrl = settings.baseUrl()
private val currentUsername: String? = localRepository.get(LocalRepository.USERNAME_KEY) private val token = tokenRepository.get(currentServer)
private val token = tokenRepository.get() private val currentUsername: String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
suspend fun map(message: Message): List<BaseViewModel<*>> { suspend fun map(message: Message): List<BaseViewModel<*>> {
return translate(message) return translate(message)
...@@ -70,9 +68,17 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -70,9 +68,17 @@ class ViewModelMapper @Inject constructor(private val context: Context,
} }
mapMessage(message).let { mapMessage(message).let {
if (list.size > 0) {
it.preview = list[0].preview
}
list.add(it) list.add(it)
} }
for (i in list.size - 1 downTo 0) {
val next = if (i - 1 < 0) null else list[i - 1]
list[i].nextDownStreamMessage = next
}
return@withContext list return@withContext list
} }
...@@ -85,27 +91,47 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -85,27 +91,47 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val description = url.meta?.description val description = url.meta?.description
return UrlPreviewViewModel(message, url, message.id, title, hostname, description, thumb, return UrlPreviewViewModel(message, url, message.id, title, hostname, description, thumb,
getReactions(message)) getReactions(message), preview = message.copy(message = url.url))
} }
private fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? { private suspend fun mapAttachment(message: Message, attachment: Attachment): BaseViewModel<*>? {
return when (attachment) { return when (attachment) {
is FileAttachment -> mapFileAttachment(message, attachment) is FileAttachment -> mapFileAttachment(message, attachment)
is MessageAttachment -> mapMessageAttachment(message, attachment)
else -> null else -> null
} }
} }
private suspend fun mapMessageAttachment(message: Message, attachment: MessageAttachment): MessageAttachmentViewModel {
val attachmentAuthor = attachment.author!!
val time = getTime(attachment.timestamp!!)
val attachmentText = when (attachment.attachments.orEmpty().firstOrNull()) {
is ImageAttachment -> context.getString(R.string.msg_preview_photo)
is VideoAttachment -> context.getString(R.string.msg_preview_video)
is AudioAttachment -> context.getString(R.string.msg_preview_audio)
else -> attachment.text ?: ""
}
val content = stripMessageQuotes(message)
return MessageAttachmentViewModel(message = content, rawData = message,
messageId = message.id, time = time, senderName = attachmentAuthor,
content = attachmentText, isPinned = message.pinned, reactions = getReactions(message),
preview = message.copy(message = content.message))
}
private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseViewModel<*>? { private fun mapFileAttachment(message: Message, attachment: FileAttachment): BaseViewModel<*>? {
val attachmentUrl = attachmentUrl(attachment) val attachmentUrl = attachmentUrl(attachment)
val attachmentTitle = attachmentTitle(attachment) val attachmentTitle = attachmentTitle(attachment)
val id = attachmentId(message, attachment) val id = attachmentId(message, attachment)
return when (attachment) { return when (attachment) {
is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id, is ImageAttachment -> ImageAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message)) attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_photo)))
is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id, is VideoAttachment -> VideoAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message)) attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_video)))
is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id, is AudioAttachment -> AudioAttachmentViewModel(message, attachment, message.id,
attachmentUrl, attachmentTitle, id, getReactions(message)) attachmentUrl, attachmentTitle, id, getReactions(message),
preview = message.copy(message = context.getString(R.string.msg_preview_audio)))
else -> null else -> null
} }
} }
...@@ -149,32 +175,20 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -149,32 +175,20 @@ class ViewModelMapper @Inject constructor(private val context: Context,
val sender = getSenderName(message) val sender = getSenderName(message)
val time = getTime(message.timestamp) val time = getTime(message.timestamp)
val avatar = getUserAvatar(message) val avatar = getUserAvatar(message)
val preview = mapMessagePreview(message)
val baseUrl = settings.baseUrl() val content = getContent(stripMessageQuotes(message))
var quote: Message? = null MessageViewModel(message = stripMessageQuotes(message), rawData = message,
val urls = ArrayList<Url>()
message.urls?.let {
if (it.isEmpty()) return@let
for (url in it) {
urls.add(url)
baseUrl?.let {
val quoteUrl = HttpUrl.parse(url.url)
val serverUrl = HttpUrl.parse(baseUrl)
if (quoteUrl != null && serverUrl != null) {
quote = makeQuote(quoteUrl, serverUrl)?.let {
getMessageWithoutQuoteMarkdown(it)
}
}
}
}
}
val content = getContent(context, getMessageWithoutQuoteMarkdown(message), quote)
MessageViewModel(message = getMessageWithoutQuoteMarkdown(message), rawData = message,
messageId = message.id, avatar = avatar!!, time = time, senderName = sender, messageId = message.id, avatar = avatar!!, time = time, senderName = sender,
content = content, isPinned = message.pinned, reactions = getReactions(message), content = content, isPinned = message.pinned, reactions = getReactions(message),
isFirstUnread = false) isFirstUnread = false, preview = preview)
}
private suspend fun mapMessagePreview(message: Message): Message {
return when (message.isSystemMessage()) {
false -> stripMessageQuotes(message)
true -> message.copy(message = getSystemMessage(message).toString())
}
} }
private fun getReactions(message: Message): List<ReactionViewModel> { private fun getReactions(message: Message): List<ReactionViewModel> {
...@@ -196,7 +210,7 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -196,7 +210,7 @@ class ViewModelMapper @Inject constructor(private val context: Context,
return reactions ?: emptyList() return reactions ?: emptyList()
} }
private fun getMessageWithoutQuoteMarkdown(message: Message): Message { private suspend fun stripMessageQuotes(message: Message): Message {
val baseUrl = settings.baseUrl() val baseUrl = settings.baseUrl()
return message.copy( return message.copy(
message = message.message.replace("\\[\\s\\]\\($baseUrl.*\\)".toRegex(), "").trim() message = message.message.replace("\\[\\s\\]\\($baseUrl.*\\)".toRegex(), "").trim()
...@@ -227,32 +241,14 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -227,32 +241,14 @@ class ViewModelMapper @Inject constructor(private val context: Context,
private fun getTime(timestamp: Long) = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(timestamp)) private fun getTime(timestamp: Long) = DateTimeHelper.getTime(DateTimeHelper.getLocalDateTime(timestamp))
private fun makeQuote(quoteUrl: HttpUrl, serverUrl: HttpUrl): Message? { private suspend fun getContent(message: Message): CharSequence {
if (quoteUrl.host() == serverUrl.host()) {
val msgIdToQuote = quoteUrl.queryParameter("msg")
Timber.d("Will quote message Id: $msgIdToQuote")
return if (msgIdToQuote != null) messagesRepository.getById(msgIdToQuote) else null
}
return null
}
private suspend fun getContent(context: Context, message: Message, quote: Message?): CharSequence {
return when (message.isSystemMessage()) { return when (message.isSystemMessage()) {
true -> getSystemMessage(message, context) true -> getSystemMessage(message)
false -> getNormalMessage(message, quote) false -> parser.renderMarkdown(message, currentUsername)
} }
} }
private suspend fun getNormalMessage(message: Message, quote: Message?): CharSequence { private fun getSystemMessage(message: Message): CharSequence {
var quoteViewModel: MessageViewModel? = null
if (quote != null) {
val quoteMessage: Message = quote
quoteViewModel = mapMessage(quoteMessage)
}
return parser.renderMarkdown(message.message, quoteViewModel, currentUsername)
}
private fun getSystemMessage(message: Message, context: Context): CharSequence {
val content = when (message.type) { val content = when (message.type) {
//TODO: Add implementation for Welcome type. //TODO: Add implementation for Welcome type.
is MessageType.MessageRemoved -> context.getString(R.string.message_removed) is MessageType.MessageRemoved -> context.getString(R.string.message_removed)
...@@ -261,68 +257,17 @@ class ViewModelMapper @Inject constructor(private val context: Context, ...@@ -261,68 +257,17 @@ class ViewModelMapper @Inject constructor(private val context: Context,
is MessageType.UserAdded -> context.getString(R.string.message_user_added_by, message.message, message.sender?.username) is MessageType.UserAdded -> context.getString(R.string.message_user_added_by, message.message, message.sender?.username)
is MessageType.RoomNameChanged -> context.getString(R.string.message_room_name_changed, message.message, message.sender?.username) is MessageType.RoomNameChanged -> context.getString(R.string.message_room_name_changed, message.message, message.sender?.username)
is MessageType.UserRemoved -> context.getString(R.string.message_user_removed_by, message.message, message.sender?.username) is MessageType.UserRemoved -> context.getString(R.string.message_user_removed_by, message.message, message.sender?.username)
is MessageType.MessagePinned -> { is MessageType.MessagePinned -> context.getString(R.string.message_pinned)
val attachment = message.attachments?.get(0)
val pinnedSystemMessage = context.getString(R.string.message_pinned)
if (attachment != null && attachment is MessageAttachment) {
return SpannableStringBuilder(pinnedSystemMessage)
.apply {
setSpan(StyleSpan(Typeface.ITALIC), 0, length, 0)
setSpan(ForegroundColorSpan(Color.GRAY), 0, length, 0)
}
.append(quoteMessage(attachment.author!!, attachment.text!!, attachment.timestamp!!))
}
return pinnedSystemMessage
}
else -> { else -> {
throw InvalidParameterException("Invalid message type: ${message.type}") throw InvalidParameterException("Invalid message type: ${message.type}")
} }
} }
//isSystemMessage = true
val spannableMsg = SpannableStringBuilder(content) val spannableMsg = SpannableStringBuilder(content)
spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length, spannableMsg.setSpan(StyleSpan(Typeface.ITALIC), 0, spannableMsg.length,
0) 0)
spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length, spannableMsg.setSpan(ForegroundColorSpan(Color.GRAY), 0, spannableMsg.length,
0) 0)
/*if (attachmentType == null) {
val username = message.sender?.username
val message = message.message
val usernameTextStartIndex = if (username != null) content.indexOf(username) else -1
val usernameTextEndIndex = if (username != null) usernameTextStartIndex + username.length else -1
val messageTextStartIndex = if (message.isNotEmpty()) content.indexOf(message) else -1
val messageTextEndIndex = messageTextStartIndex + message.length
if (usernameTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), usernameTextStartIndex, usernameTextEndIndex,
0)
}
if (messageTextStartIndex > -1) {
spannableMsg.setSpan(StyleSpan(Typeface.BOLD_ITALIC), messageTextStartIndex, messageTextEndIndex,
0)
}
} else if (attachmentType == AttachmentType.Message) {
spannableMsg.append(quoteMessage(attachmentMessageAuthor!!, attachmentMessageText!!, attachmentTimestamp!!))
}*/
return spannableMsg return spannableMsg
} }
private fun quoteMessage(author: String, text: String, timestamp: Long): CharSequence {
return SpannableStringBuilder().apply {
val header = "\n$author ${getTime(timestamp)}\n"
append(SpannableString(header).apply {
setSpan(StyleSpan(Typeface.BOLD), 1, author.length + 1, 0)
setSpan(MessageParser.QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), 1, length, 0)
setSpan(AbsoluteSizeSpan(context.resources.getDimensionPixelSize(R.dimen.message_time_text_size)),
author.length + 1, length, 0)
})
append(SpannableString(parser.renderMarkdown(text)).apply {
setSpan(MessageParser.QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), 0, length, 0)
})
}
}
} }
\ No newline at end of file
...@@ -3,7 +3,7 @@ package chat.rocket.android.chatroom.viewmodel.suggestion ...@@ -3,7 +3,7 @@ package chat.rocket.android.chatroom.viewmodel.suggestion
import chat.rocket.android.widget.autocompletion.model.SuggestionModel import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.common.model.UserStatus import chat.rocket.common.model.UserStatus
class PeopleSuggestionViewModel(val imageUri: String, class PeopleSuggestionViewModel(val imageUri: String?,
text: String, text: String,
val username: String, val username: String,
val name: String, val name: String,
......
package chat.rocket.android.chatrooms.presentation package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants
import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.main.presentation.MainNavigator import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.server.domain.* import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManager import chat.rocket.android.server.infraestructure.ConnectionManager
...@@ -9,7 +13,10 @@ import chat.rocket.android.server.infraestructure.chatRooms ...@@ -9,7 +13,10 @@ import chat.rocket.android.server.infraestructure.chatRooms
import chat.rocket.android.server.infraestructure.state import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.model.* import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.RoomType
import chat.rocket.common.model.SimpleUser
import chat.rocket.common.model.User
import chat.rocket.core.internal.model.Subscription import chat.rocket.core.internal.model.Subscription
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.StreamMessage import chat.rocket.core.internal.realtime.StreamMessage
...@@ -22,6 +29,7 @@ import kotlinx.coroutines.experimental.android.UI ...@@ -22,6 +29,7 @@ import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.Channel
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.reflect.KProperty1
class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
...@@ -30,13 +38,14 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -30,13 +38,14 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private val getChatRoomsInteractor: GetChatRoomsInteractor, private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val saveChatRoomsInteractor: SaveChatRoomsInteractor, private val saveChatRoomsInteractor: SaveChatRoomsInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor, private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val viewModelMapper: ViewModelMapper,
settingsRepository: SettingsRepository, settingsRepository: SettingsRepository,
factory: ConnectionManagerFactory) { factory: ConnectionManagerFactory) {
private val manager: ConnectionManager = factory.create(serverInteractor.get()!!) private val manager: ConnectionManager = factory.create(serverInteractor.get()!!)
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val client = manager.client private val client = manager.client
private var reloadJob: Deferred<List<ChatRoom>>? = null private var reloadJob: Deferred<List<ChatRoom>>? = null
private val settings = settingsRepository.get(currentServer)!! private val settings = settingsRepository.get(currentServer)
private val subscriptionsChannel = Channel<StreamMessage<BaseRoom>>() private val subscriptionsChannel = Channel<StreamMessage<BaseRoom>>()
private val stateChannel = Channel<State>() private val stateChannel = Channel<State>()
...@@ -89,9 +98,9 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -89,9 +98,9 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
val chatRoomsCombined = mutableListOf<ChatRoom>() val chatRoomsCombined = mutableListOf<ChatRoom>()
chatRoomsCombined.addAll(usersToChatRooms(users)) chatRoomsCombined.addAll(usersToChatRooms(users))
chatRoomsCombined.addAll(roomsToChatRooms(rooms)) chatRoomsCombined.addAll(roomsToChatRooms(rooms))
view.updateChatRooms(chatRoomsCombined) view.updateChatRooms(getChatRoomsWithPreviews(chatRoomsCombined.toList()))
} else { } else {
view.updateChatRooms(roomList) view.updateChatRooms(getChatRoomsWithPreviews(roomList))
} }
} catch (ex: RocketChatException) { } catch (ex: RocketChatException) {
Timber.e(ex) Timber.e(ex)
...@@ -101,21 +110,52 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -101,21 +110,52 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
private suspend fun usersToChatRooms(users: List<User>): List<ChatRoom> { private suspend fun usersToChatRooms(users: List<User>): List<ChatRoom> {
return users.map { return users.map {
ChatRoom(it.id, RoomType.DIRECT_MESSAGE, SimpleUser( ChatRoom(id = it.id,
username = it.username, name = it.name, id = null), it.name ?: "", type = RoomType.DIRECT_MESSAGE,
it.name, false, null, null, null, user = SimpleUser(username = it.username, name = it.name, id = null),
null, null, false, false, false, name = it.name ?: "",
0L, null, 0L, null, client fullName = it.name,
readonly = false,
updatedAt = null,
timestamp = null,
lastSeen = null,
topic = null,
description = null,
announcement = null,
default = false,
open = false,
alert = false,
unread = 0L,
userMenstions = null,
groupMentions = 0L,
lastMessage = null,
client = client
) )
} }
} }
private suspend fun roomsToChatRooms(rooms: List<Room>): List<ChatRoom> { private suspend fun roomsToChatRooms(rooms: List<Room>): List<ChatRoom> {
return rooms.map { return rooms.map {
ChatRoom(it.id, it.type, it.user, it.name ?: "", ChatRoom(id = it.id,
it.fullName, it.readonly, it.updatedAt, null, null, type = it.type,
it.topic, it.announcement, false, false, false, user = it.user,
0L, null, 0L, it.lastMessage, client name = it.name ?: "",
fullName = it.fullName,
readonly = it.readonly,
updatedAt = it.updatedAt,
timestamp = null,
lastSeen = null,
topic = it.topic,
description = it.description,
announcement = it.announcement,
default = false,
open = false,
alert = false,
unread = 0L,
userMenstions = null,
groupMentions = 0L,
lastMessage = it.lastMessage,
client = client
) )
} }
} }
...@@ -125,18 +165,72 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -125,18 +165,72 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
val sortedRooms = sortRooms(chatRooms) val sortedRooms = sortRooms(chatRooms)
Timber.d("Loaded rooms: ${sortedRooms.size}") Timber.d("Loaded rooms: ${sortedRooms.size}")
saveChatRoomsInteractor.save(currentServer, sortedRooms) saveChatRoomsInteractor.save(currentServer, sortedRooms)
return sortedRooms return getChatRoomsWithPreviews(sortedRooms)
}
fun updateSortedChatRooms() {
val currentServer = serverInteractor.get()!!
launchUI(strategy) {
val roomList = getChatRoomsInteractor.get(currentServer)
view.updateChatRooms(sortRooms(roomList))
}
} }
private fun sortRooms(chatRooms: List<ChatRoom>): List<ChatRoom> { private fun sortRooms(chatRooms: List<ChatRoom>): List<ChatRoom> {
val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY)
val groupByType = SharedPreferenceHelper.getBoolean(Constants.CHATROOM_GROUP_BY_TYPE_KEY, false)
val openChatRooms = getOpenChatRooms(chatRooms) val openChatRooms = getOpenChatRooms(chatRooms)
return sortChatRooms(openChatRooms)
return when (sortType) {
ChatRoomsSortOrder.ALPHABETICAL -> {
when (groupByType) {
true -> openChatRooms.sortedWith(compareBy(ChatRoom::type).thenBy { it.name })
false -> openChatRooms.sortedWith(compareBy(ChatRoom::name))
}
}
ChatRoomsSortOrder.ACTIVITY -> {
when (groupByType) {
true -> openChatRooms.sortedWith(compareBy(ChatRoom::type).thenByDescending { it.lastMessage?.timestamp })
false -> openChatRooms.sortedByDescending { chatRoom ->
chatRoom.lastMessage?.timestamp
}
}
}
else -> {
openChatRooms
}
}
}
private fun compareBy(selector: KProperty1<ChatRoom, RoomType>): Comparator<ChatRoom> {
return Comparator { a, b -> getTypeConstant(a.type) - getTypeConstant(b.type) }
}
private fun getTypeConstant(roomType: RoomType): Int {
return when (roomType) {
is RoomType.Channel -> Constants.CHATROOM_CHANNEL
is RoomType.PrivateGroup -> Constants.CHATROOM_PRIVATE_GROUP
is RoomType.DirectMessage -> Constants.CHATROOM_DM
is RoomType.Livechat -> Constants.CHATROOM_LIVE_CHAT
else -> 0
}
} }
private fun updateRooms() { private fun updateRooms() {
Timber.d("Updating Rooms") Timber.d("Updating Rooms")
launch { launch(strategy.jobs) {
view.updateChatRooms(getChatRoomsInteractor.get(currentServer)) view.updateChatRooms(getChatRoomsWithPreviews(getChatRoomsInteractor.get(currentServer)))
}
}
private suspend fun getChatRoomsWithPreviews(chatRooms: List<ChatRoom>): List<ChatRoom> {
return chatRooms.map {
if (it.lastMessage != null) {
it.copy(lastMessage = viewModelMapper.map(it.lastMessage!!).last().preview)
} else {
it
}
} }
} }
...@@ -245,25 +339,27 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -245,25 +339,27 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList() val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == room.id } val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == room.id }
chatRoom?.apply { chatRoom?.apply {
val newRoom = ChatRoom(room.id, val newRoom = ChatRoom(id = room.id,
room.type, type = room.type,
room.user ?: user, user = room.user ?: user,
room.name ?: name, name = room.name ?: name,
room.fullName ?: fullName, fullName = room.fullName ?: fullName,
room.readonly, readonly = room.readonly,
room.updatedAt ?: updatedAt, updatedAt = room.updatedAt ?: updatedAt,
timestamp, timestamp = timestamp,
lastSeen, lastSeen = lastSeen,
room.topic, topic = room.topic,
room.announcement, description = room.description,
default, announcement = room.announcement,
open, default = default,
alert, favorite = favorite,
unread, open = open,
userMenstions, alert = alert,
groupMentions, unread = unread,
room.lastMessage, userMenstions = userMenstions,
client) groupMentions = groupMentions,
lastMessage = room.lastMessage,
client = client)
removeRoom(room.id, chatRooms) removeRoom(room.id, chatRooms)
chatRooms.add(newRoom) chatRooms.add(newRoom)
saveChatRoomsInteractor.save(currentServer, sortRooms(chatRooms)) saveChatRoomsInteractor.save(currentServer, sortRooms(chatRooms))
...@@ -272,29 +368,31 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView, ...@@ -272,29 +368,31 @@ class ChatRoomsPresenter @Inject constructor(private val view: ChatRoomsView,
// Update a ChatRoom with a Subscription information // Update a ChatRoom with a Subscription information
private fun updateSubscription(subscription: Subscription) { private fun updateSubscription(subscription: Subscription) {
Timber.d("Updating subscrition: ${subscription.id} - ${subscription.name}") Timber.d("Updating subscription: ${subscription.id} - ${subscription.name}")
val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList() val chatRooms = getChatRoomsInteractor.get(currentServer).toMutableList()
val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId } val chatRoom = chatRooms.find { chatRoom -> chatRoom.id == subscription.roomId }
chatRoom?.apply { chatRoom?.apply {
val newRoom = ChatRoom(subscription.roomId, val newRoom = ChatRoom(id = subscription.roomId,
subscription.type, type = subscription.type,
subscription.user ?: user, user = subscription.user ?: user,
subscription.name, name = subscription.name,
subscription.fullName ?: fullName, fullName = subscription.fullName ?: fullName,
subscription.readonly ?: readonly, readonly = subscription.readonly ?: readonly,
subscription.updatedAt ?: updatedAt, updatedAt = subscription.updatedAt ?: updatedAt,
subscription.timestamp ?: timestamp, timestamp = subscription.timestamp ?: timestamp,
subscription.lastSeen ?: lastSeen, lastSeen = subscription.lastSeen ?: lastSeen,
topic, topic = topic,
announcement, description = description,
subscription.isDefault, announcement = announcement,
subscription.open, default = subscription.isDefault,
subscription.alert, favorite = subscription.isFavorite,
subscription.unread, open = subscription.open,
subscription.userMentions, alert = subscription.alert,
subscription.groupMentions, unread = subscription.unread,
lastMessage, userMenstions = subscription.userMentions,
client) groupMentions = subscription.groupMentions,
lastMessage = lastMessage,
client = client)
removeRoom(subscription.roomId, chatRooms) removeRoom(subscription.roomId, chatRooms)
chatRooms.add(newRoom) chatRooms.add(newRoom)
saveChatRoomsInteractor.save(currentServer, sortRooms(chatRooms)) saveChatRoomsInteractor.save(currentServer, sortRooms(chatRooms))
......
...@@ -3,14 +3,18 @@ package chat.rocket.android.chatrooms.ui ...@@ -3,14 +3,18 @@ package chat.rocket.android.chatrooms.ui
import DateTimeHelper import DateTimeHelper
import DrawableHelper import DrawableHelper
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.Color
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.helper.UrlHelper import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.checkIfMyself
import chat.rocket.android.server.domain.PublicSettings import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.useRealName import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.util.extensions.content import chat.rocket.android.util.extensions.content
...@@ -26,6 +30,7 @@ import kotlinx.android.synthetic.main.unread_messages_badge.view.* ...@@ -26,6 +30,7 @@ import kotlinx.android.synthetic.main.unread_messages_badge.view.*
class ChatRoomsAdapter(private val context: Context, class ChatRoomsAdapter(private val context: Context,
private val settings: PublicSettings, private val settings: PublicSettings,
private val localRepository: LocalRepository,
private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() { private val listener: (ChatRoom) -> Unit) : RecyclerView.Adapter<ChatRoomsAdapter.ViewHolder>() {
var dataSet: MutableList<ChatRoom> = ArrayList() var dataSet: MutableList<ChatRoom> = ArrayList()
...@@ -116,21 +121,30 @@ class ChatRoomsAdapter(private val context: Context, ...@@ -116,21 +121,30 @@ class ChatRoomsAdapter(private val context: Context,
val lastMessageSender = lastMessage?.sender val lastMessageSender = lastMessage?.sender
if (lastMessage != null && lastMessageSender != null) { if (lastMessage != null && lastMessageSender != null) {
val message = lastMessage.message val message = lastMessage.message
val senderUsername = lastMessageSender.username val senderUsername = if (settings.useRealName()) {
lastMessageSender.name ?: lastMessageSender.username
} else {
lastMessageSender.username
}
when (senderUsername) { when (senderUsername) {
chatRoom.name -> { chatRoom.name -> {
textView.content = message textView.content = message
} }
// TODO Change to MySelf
// chatRoom.user?.username -> {
// holder.lastMessage.textContent = context.getString(R.string.msg_you) + ": $message"
// }
else -> { else -> {
textView.content = "@$senderUsername: $message" val user = if (localRepository.checkIfMyself(lastMessageSender.username!!)) {
"${context.getString(R.string.msg_you)}: "
} else {
"$senderUsername: "
}
val spannable = SpannableStringBuilder(user)
val len = spannable.length
spannable.setSpan(ForegroundColorSpan(Color.BLACK), 0, len - 1, 0)
spannable.append(message)
textView.content = spannable
} }
} }
} else { } else {
textView.content = "" textView.content = context.getText(R.string.msg_no_messages_yet)
} }
} }
......
package chat.rocket.android.chatrooms.ui package chat.rocket.android.chatrooms.ui
import android.app.AlertDialog
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
...@@ -9,13 +12,20 @@ import android.support.v7.widget.DefaultItemAnimator ...@@ -9,13 +12,20 @@ import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.SearchView import android.support.v7.widget.SearchView
import android.view.* import android.view.*
import android.widget.CheckBox
import android.widget.RadioGroup
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView import chat.rocket.android.chatrooms.presentation.ChatRoomsView
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.helper.ChatRoomsSortOrder
import chat.rocket.android.helper.Constants
import chat.rocket.android.helper.SharedPreferenceHelper
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.SettingsRepository import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.util.extensions.* import chat.rocket.android.util.extensions.*
import chat.rocket.android.widget.DividerItemDecoration import chat.rocket.android.widget.DividerItemDecoration
import chat.rocket.common.model.RoomType
import chat.rocket.core.internal.realtime.State import chat.rocket.core.internal.realtime.State
import chat.rocket.core.model.ChatRoom import chat.rocket.core.model.ChatRoom
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
...@@ -27,14 +37,18 @@ import kotlinx.coroutines.experimental.async ...@@ -27,14 +37,18 @@ import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.launch
import javax.inject.Inject import javax.inject.Inject
class ChatRoomsFragment : Fragment(), ChatRoomsView { class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject lateinit var presenter: ChatRoomsPresenter @Inject lateinit var presenter: ChatRoomsPresenter
@Inject lateinit var serverInteractor: GetCurrentServerInteractor @Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var settingsRepository: SettingsRepository @Inject lateinit var settingsRepository: SettingsRepository
@Inject lateinit var localRepository: LocalRepository
private lateinit var preferences: SharedPreferences
private var searchView: SearchView? = null private var searchView: SearchView? = null
private val handler = Handler() private val handler = Handler()
private var listJob: Job? = null private var listJob: Job? = null
private var sectionedAdapter: SimpleSectionedRecyclerViewAdapter? = null
companion object { companion object {
fun newInstance() = ChatRoomsFragment() fun newInstance() = ChatRoomsFragment()
...@@ -44,6 +58,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -44,6 +58,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
setHasOptionsMenu(true) setHasOptionsMenu(true)
preferences = context?.getSharedPreferences("temp", Context.MODE_PRIVATE)!!
} }
override fun onDestroy() { override fun onDestroy() {
...@@ -85,20 +100,76 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -85,20 +100,76 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}) })
} }
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.action_sort -> {
val dialogLayout = layoutInflater.inflate(R.layout.chatroom_sort_dialog, null)
val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY)
val groupByType = SharedPreferenceHelper.getBoolean(Constants.CHATROOM_GROUP_BY_TYPE_KEY, false)
val radioGroup = dialogLayout.findViewById<RadioGroup>(R.id.radio_group_sort)
val groupByTypeCheckBox = dialogLayout.findViewById<CheckBox>(R.id.checkbox_group_by_type)
radioGroup.check(when (sortType) {
0 -> R.id.radio_sort_alphabetical
else -> R.id.radio_sort_activity
})
radioGroup.setOnCheckedChangeListener({ _, checkedId ->
run {
SharedPreferenceHelper.putInt(Constants.CHATROOM_SORT_TYPE_KEY, when (checkedId) {
R.id.radio_sort_alphabetical -> 0
R.id.radio_sort_activity -> 1
else -> 1
})
presenter.updateSortedChatRooms()
invalidateQueryOnSearch()
}
})
groupByTypeCheckBox.isChecked = groupByType
groupByTypeCheckBox.setOnCheckedChangeListener({ _, isChecked ->
SharedPreferenceHelper.putBoolean(Constants.CHATROOM_GROUP_BY_TYPE_KEY, isChecked)
presenter.updateSortedChatRooms()
invalidateQueryOnSearch()
})
val dialogSort = AlertDialog.Builder(context)
.setTitle(R.string.dialog_sort_title)
.setView(dialogLayout)
.setPositiveButton("Done", { dialog, _ -> dialog.dismiss() })
dialogSort.show()
}
}
return super.onOptionsItemSelected(item)
}
private fun invalidateQueryOnSearch(){
searchView?.let {
if (!searchView!!.isIconified){
queryChatRoomsByName(searchView!!.query.toString())
}
}
}
override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) { override suspend fun updateChatRooms(newDataSet: List<ChatRoom>) {
activity?.apply { activity?.apply {
listJob?.cancel() listJob?.cancel()
listJob = launch(UI) { listJob = launch(UI) {
val adapter = recycler_view.adapter as ChatRoomsAdapter val adapter = recycler_view.adapter as SimpleSectionedRecyclerViewAdapter
// FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android.dev/issues/5a90d4718cb3c2fa63b3f557?time=last-seven-days // FIXME https://fabric.io/rocketchat3/android/apps/chat.rocket.android.dev/issues/5a90d4718cb3c2fa63b3f557?time=last-seven-days
// TODO - fix this bug to reenable DiffUtil // TODO - fix this bug to reenable DiffUtil
val diff = async(CommonPool) { val diff = async(CommonPool) {
DiffUtil.calculateDiff(RoomsDiffCallback(adapter.dataSet, newDataSet)) DiffUtil.calculateDiff(RoomsDiffCallback(adapter.baseAdapter.dataSet, newDataSet))
}.await() }.await()
if (isActive) { if (isActive) {
adapter.updateRooms(newDataSet) adapter.baseAdapter.updateRooms(newDataSet)
diff.dispatchUpdatesTo(adapter) diff.dispatchUpdatesTo(adapter)
//Set sections always after data set is updated
setSections()
} }
} }
} }
...@@ -108,11 +179,19 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -108,11 +179,19 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
override fun showLoading() = view_loading.setVisible(true) override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisible(false) override fun hideLoading() {
if (view_loading != null) {
view_loading.setVisible(false)
}
}
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message) override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
...@@ -152,11 +231,45 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView { ...@@ -152,11 +231,45 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end))) resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)))
recycler_view.itemAnimator = DefaultItemAnimator() recycler_view.itemAnimator = DefaultItemAnimator()
// TODO - use a ViewModel Mapper instead of using settings on the adapter // TODO - use a ViewModel Mapper instead of using settings on the adapter
recycler_view.adapter = ChatRoomsAdapter(this,
settingsRepository.get(serverInteractor.get()!!)!!) { chatRoom -> val baseAdapter = ChatRoomsAdapter(this,
presenter.loadChatRoom(chatRoom) settingsRepository.get(serverInteractor.get()!!), localRepository) { chatRoom -> presenter.loadChatRoom(chatRoom) }
sectionedAdapter = SimpleSectionedRecyclerViewAdapter(this, R.layout.item_chatroom_header, R.id.text_chatroom_header, baseAdapter!!)
recycler_view.adapter = sectionedAdapter
}
} }
private fun setSections() {
//Don't add section if not grouping by RoomType
if (!SharedPreferenceHelper.getBoolean(Constants.CHATROOM_GROUP_BY_TYPE_KEY, false)) {
sectionedAdapter?.clearSections()
return
}
val sections = ArrayList<SimpleSectionedRecyclerViewAdapter.Section>()
sectionedAdapter?.baseAdapter?.dataSet?.let {
var previousChatRoomType = ""
for ((position, chatRoom) in it.withIndex()) {
val type = chatRoom.type.toString()
if (type != previousChatRoomType) {
val title = when (type) {
RoomType.CHANNEL.toString() -> resources.getString(R.string.header_channel)
RoomType.PRIVATE_GROUP.toString() -> resources.getString(R.string.header_private_groups)
RoomType.DIRECT_MESSAGE.toString() -> resources.getString(R.string.header_direct_messages)
RoomType.LIVECHAT.toString() -> resources.getString(R.string.header_live_chats)
else -> resources.getString(R.string.header_unknown)
} }
sections.add(SimpleSectionedRecyclerViewAdapter.Section(position, title))
}
previousChatRoomType = chatRoom.type.toString()
}
}
val dummy = arrayOfNulls<SimpleSectionedRecyclerViewAdapter.Section>(sections.size)
sectionedAdapter?.setSections(sections.toArray(dummy))
} }
private fun queryChatRoomsByName(name: String?): Boolean { private fun queryChatRoomsByName(name: String?): Boolean {
......
package chat.rocket.android.chatrooms.ui
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import java.util.*
class SimpleSectionedRecyclerViewAdapter(private val context: Context, private val sectionResourceId: Int, private val textResourceId: Int,
val baseAdapter: ChatRoomsAdapter) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var isValid = true
private val sectionsHeaders = SparseArray<Section>()
init {
baseAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
isValid = baseAdapter.itemCount > 0
notifyDataSetChanged()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
isValid = baseAdapter.itemCount > 0
notifyItemRangeChanged(positionStart, itemCount)
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
isValid = baseAdapter.itemCount > 0
notifyItemRangeInserted(positionStart, itemCount)
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
isValid = baseAdapter.itemCount > 0
notifyItemRangeRemoved(positionStart, itemCount)
}
})
}
class SectionViewHolder(view: View, textResourceId: Int) : RecyclerView.ViewHolder(view) {
var title: TextView = view.findViewById<View>(textResourceId) as TextView
}
override fun onCreateViewHolder(parent: ViewGroup, typeView: Int): RecyclerView.ViewHolder {
return if (typeView == SECTION_TYPE) {
val view = LayoutInflater.from(context).inflate(sectionResourceId, parent, false)
SectionViewHolder(view, textResourceId)
} else {
baseAdapter.onCreateViewHolder(parent, typeView - 1)
}
}
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
if (isSectionHeaderPosition(position)) {
(viewHolder as SectionViewHolder).title.text = sectionsHeaders.get(position).title
} else {
baseAdapter.onBindViewHolder(viewHolder as ChatRoomsAdapter.ViewHolder, sectionedPositionToPosition(position))
}
}
override fun getItemViewType(position: Int): Int {
return if (isSectionHeaderPosition(position))
SECTION_TYPE
else
baseAdapter.getItemViewType(sectionedPositionToPosition(position)) + 1
}
class Section(internal var firstPosition: Int, var title: CharSequence) {
internal var sectionedPosition: Int = 0
}
fun setSections(sections: Array<Section>) {
sectionsHeaders.clear()
Arrays.sort(sections) { section1, section2 ->
when {
section1.firstPosition == section2.firstPosition -> 0
section1.firstPosition < section2.firstPosition -> -1
else -> 1
}
}
for ((offset, section) in sections.withIndex()) {
section.sectionedPosition = section.firstPosition + offset
sectionsHeaders.append(section.sectionedPosition, section)
}
notifyDataSetChanged()
}
fun clearSections(){
sectionsHeaders.clear()
notifyDataSetChanged()
}
fun positionToSectionedPosition(position: Int): Int {
var offset = 0
for (i in 0 until sectionsHeaders.size()) {
if (sectionsHeaders.valueAt(i).firstPosition > position) {
break
}
++offset
}
return position + offset
}
private fun sectionedPositionToPosition(sectionedPosition: Int): Int {
if (isSectionHeaderPosition(sectionedPosition)) {
return RecyclerView.NO_POSITION
}
var offset = 0
for (i in 0 until sectionsHeaders.size()) {
if (sectionsHeaders.valueAt(i).sectionedPosition > sectionedPosition) {
break
}
--offset
}
return sectionedPosition + offset
}
private fun isSectionHeaderPosition(position: Int): Boolean {
return sectionsHeaders.get(position) != null
}
override fun getItemId(position: Int): Long {
return when (isSectionHeaderPosition(position)) {
true -> (Integer.MAX_VALUE - sectionsHeaders.indexOfKey(position)).toLong()
false -> baseAdapter.getItemId(sectionedPositionToPosition(position))
}
}
override fun getItemCount(): Int {
return if (isValid) baseAdapter.itemCount + sectionsHeaders.size() else 0
}
companion object {
private const val SECTION_TYPE = 0
}
}
...@@ -4,6 +4,7 @@ import android.app.Application ...@@ -4,6 +4,7 @@ import android.app.Application
import chat.rocket.android.app.RocketChatApplication import chat.rocket.android.app.RocketChatApplication
import chat.rocket.android.dagger.module.ActivityBuilder import chat.rocket.android.dagger.module.ActivityBuilder
import chat.rocket.android.dagger.module.AppModule import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.dagger.module.ReceiverBuilder
import chat.rocket.android.dagger.module.ServiceBuilder import chat.rocket.android.dagger.module.ServiceBuilder
import chat.rocket.android.push.FirebaseTokenService import chat.rocket.android.push.FirebaseTokenService
import dagger.BindsInstance import dagger.BindsInstance
...@@ -12,7 +13,8 @@ import dagger.android.support.AndroidSupportInjectionModule ...@@ -12,7 +13,8 @@ import dagger.android.support.AndroidSupportInjectionModule
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@Component(modules = [AndroidSupportInjectionModule::class, AppModule::class, ActivityBuilder::class, ServiceBuilder::class]) @Component(modules = [AndroidSupportInjectionModule::class,
AppModule::class, ActivityBuilder::class, ServiceBuilder::class, ReceiverBuilder::class])
interface AppComponent { interface AppComponent {
@Component.Builder @Component.Builder
......
...@@ -16,6 +16,8 @@ import chat.rocket.android.main.di.MainModule ...@@ -16,6 +16,8 @@ import chat.rocket.android.main.di.MainModule
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.members.di.MembersFragmentProvider import chat.rocket.android.members.di.MembersFragmentProvider
import chat.rocket.android.profile.di.ProfileFragmentProvider import chat.rocket.android.profile.di.ProfileFragmentProvider
import chat.rocket.android.server.di.ChangeServerModule
import chat.rocket.android.server.ui.ChangeServerActivity
import chat.rocket.android.settings.password.di.PasswordFragmentProvider import chat.rocket.android.settings.password.di.PasswordFragmentProvider
import chat.rocket.android.settings.password.ui.PasswordActivity import chat.rocket.android.settings.password.ui.PasswordActivity
import dagger.Module import dagger.Module
...@@ -51,4 +53,8 @@ abstract class ActivityBuilder { ...@@ -51,4 +53,8 @@ abstract class ActivityBuilder {
@PerActivity @PerActivity
@ContributesAndroidInjector(modules = [PasswordFragmentProvider::class]) @ContributesAndroidInjector(modules = [PasswordFragmentProvider::class])
abstract fun bindPasswordActivity(): PasswordActivity abstract fun bindPasswordActivity(): PasswordActivity
@PerActivity
@ContributesAndroidInjector(modules = [ChangeServerModule::class])
abstract fun bindChangeServerActivity(): ChangeServerActivity
} }
\ No newline at end of file
package chat.rocket.android.dagger.module package chat.rocket.android.dagger.module
import android.app.Application import android.app.Application
import android.app.NotificationManager
import android.arch.persistence.room.Room import android.arch.persistence.room.Room
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.content.systemService
import chat.rocket.android.BuildConfig import chat.rocket.android.BuildConfig
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.app.RocketChatDatabase import chat.rocket.android.app.RocketChatDatabase
import chat.rocket.android.authentication.infraestructure.MemoryTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.dagger.qualifier.ForFresco import chat.rocket.android.dagger.qualifier.ForFresco
import chat.rocket.android.helper.FrescoAuthInterceptor import chat.rocket.android.helper.FrescoAuthInterceptor
import chat.rocket.android.helper.MessageParser import chat.rocket.android.helper.MessageParser
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.server.domain.* import chat.rocket.android.push.GroupedPush
import chat.rocket.android.server.infraestructure.* import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.ChatRoomsRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetPermissionsInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository
import chat.rocket.android.server.infraestructure.MemoryMessagesRepository
import chat.rocket.android.server.infraestructure.MemoryRoomRepository
import chat.rocket.android.server.infraestructure.MemoryUsersRepository
import chat.rocket.android.server.infraestructure.ServerDao
import chat.rocket.android.server.infraestructure.SharedPreferencesAccountsRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesSettingsRepository
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.util.AppJsonAdapterFactory import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.util.PlatformLogger import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.TokenRepository
import com.facebook.drawee.backends.pipeline.DraweeConfig import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.core.ImagePipelineConfig
...@@ -110,8 +130,8 @@ class AppModule { ...@@ -110,8 +130,8 @@ class AppModule {
@Provides @Provides
@ForFresco @ForFresco
@Singleton @Singleton
fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository): Interceptor { fun provideFrescoAuthIntercepter(tokenRepository: TokenRepository, currentServerInteractor: GetCurrentServerInteractor): Interceptor {
return FrescoAuthInterceptor(tokenRepository) return FrescoAuthInterceptor(tokenRepository, currentServerInteractor)
} }
@Provides @Provides
...@@ -144,8 +164,8 @@ class AppModule { ...@@ -144,8 +164,8 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideTokenRepository(): TokenRepository { fun provideTokenRepository(prefs: SharedPreferences, moshi: Moshi): TokenRepository {
return MemoryTokenRepository() return SharedPreferencesTokenRepository(prefs, moshi)
} }
@Provides @Provides
...@@ -192,7 +212,10 @@ class AppModule { ...@@ -192,7 +212,10 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideMoshi(): Moshi { fun provideMoshi(): Moshi {
return Moshi.Builder().add(AppJsonAdapterFactory.INSTANCE).build() return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE)
.build()
} }
@Provides @Provides
...@@ -240,4 +263,16 @@ class AppModule { ...@@ -240,4 +263,16 @@ class AppModule {
fun providePermissionInteractor(settingsRepository: SettingsRepository, serverRepository: CurrentServerRepository): GetPermissionsInteractor { fun providePermissionInteractor(settingsRepository: SettingsRepository, serverRepository: CurrentServerRepository): GetPermissionsInteractor {
return GetPermissionsInteractor(settingsRepository, serverRepository) return GetPermissionsInteractor(settingsRepository, serverRepository)
} }
@Provides
@Singleton
fun provideAccountsRepository(preferences: SharedPreferences, moshi: Moshi): AccountsRepository =
SharedPreferencesAccountsRepository(preferences, moshi)
@Provides
fun provideNotificationManager(context: Context): NotificationManager = context.systemService()
@Provides
@Singleton
fun provideGroupedPush() = GroupedPush()
} }
\ No newline at end of file
package chat.rocket.android.dagger.module
import chat.rocket.android.push.DeleteReceiver
import chat.rocket.android.push.di.DeleteReceiverProvider
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class ReceiverBuilder {
@ContributesAndroidInjector(modules = [DeleteReceiverProvider::class])
abstract fun bindDeleteReceiver(): DeleteReceiver
}
\ No newline at end of file
package chat.rocket.android.dagger.module package chat.rocket.android.dagger.module
import chat.rocket.android.push.FirebaseTokenService import chat.rocket.android.push.FirebaseTokenService
import chat.rocket.android.push.GcmListenerService
import chat.rocket.android.push.di.FirebaseTokenServiceProvider import chat.rocket.android.push.di.FirebaseTokenServiceProvider
import chat.rocket.android.push.di.GcmListenerServiceProvider
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
...@@ -9,4 +11,7 @@ import dagger.android.ContributesAndroidInjector ...@@ -9,4 +11,7 @@ import dagger.android.ContributesAndroidInjector
@ContributesAndroidInjector(modules = [FirebaseTokenServiceProvider::class]) @ContributesAndroidInjector(modules = [FirebaseTokenServiceProvider::class])
abstract fun bindFirebaseTokenService(): FirebaseTokenService abstract fun bindFirebaseTokenService(): FirebaseTokenService
@ContributesAndroidInjector(modules = [GcmListenerServiceProvider::class])
abstract fun bindGcmListenerService(): GcmListenerService
} }
\ No newline at end of file
package chat.rocket.android.helper
object Constants {
const val CHATROOM_SORT_TYPE_KEY: String = "chatroom_sort_type"
const val CHATROOM_GROUP_BY_TYPE_KEY: String = "chatroom_group_by_type"
const val CHATROOM_GROUP_FAVOURITES_KEY: String = "chatroom_group_favourites"
//Used to sort chat rooms
const val CHATROOM_CHANNEL = 0
const val CHATROOM_PRIVATE_GROUP = 1
const val CHATROOM_DM = 2
const val CHATROOM_LIVE_CHAT = 3
}
object ChatRoomsSortOrder {
const val ALPHABETICAL: Int = 0
const val ACTIVITY: Int = 1
}
\ No newline at end of file
package chat.rocket.android.helper package chat.rocket.android.helper
import chat.rocket.core.TokenRepository import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.TokenRepository
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
class FrescoAuthInterceptor(private val tokenRepository: TokenRepository) : Interceptor { class FrescoAuthInterceptor(
private val tokenRepository: TokenRepository,
private val currentServerInteractor: GetCurrentServerInteractor
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenRepository.get()
var request = chain.request() var request = chain.request()
currentServerInteractor.get()?.let { serverUrl ->
val token = tokenRepository.get(serverUrl)
token?.let {
return@let token?.let {
val url = request.url().newBuilder().apply { val url = request.url().newBuilder().apply {
addQueryParameter("rc_uid", token.userId) addQueryParameter("rc_uid", token.userId)
addQueryParameter("rc_token", token.authToken) addQueryParameter("rc_token", token.authToken)
...@@ -18,7 +24,7 @@ class FrescoAuthInterceptor(private val tokenRepository: TokenRepository) : Inte ...@@ -18,7 +24,7 @@ class FrescoAuthInterceptor(private val tokenRepository: TokenRepository) : Inte
url(url) url(url)
}.build() }.build()
} }
}
return chain.proceed(request) return chain.proceed(request)
} }
} }
\ No newline at end of file
...@@ -4,158 +4,113 @@ import android.app.Application ...@@ -4,158 +4,113 @@ import android.app.Application
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.* import android.graphics.Canvas
import android.graphics.drawable.Drawable import android.graphics.Paint
import android.graphics.RectF
import android.net.Uri import android.net.Uri
import android.support.customtabs.CustomTabsIntent import android.support.customtabs.CustomTabsIntent
import android.provider.Browser import android.provider.Browser
import android.support.v4.content.ContextCompat
import android.support.v4.content.res.ResourcesCompat import android.support.v4.content.res.ResourcesCompat
import android.text.Layout
import android.text.Spannable
import android.text.Spanned import android.text.Spanned
import android.text.TextUtils import android.text.style.ClickableSpan
import android.text.style.* import android.text.style.ReplacementSpan
import android.util.Patterns import android.util.Patterns
import android.view.View import android.view.View
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiRepository import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message
import org.commonmark.node.AbstractVisitor import org.commonmark.node.AbstractVisitor
import org.commonmark.node.BlockQuote import org.commonmark.node.Document
import org.commonmark.node.Text import org.commonmark.node.Text
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder import ru.noties.markwon.SpannableBuilder
import ru.noties.markwon.SpannableConfiguration import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.renderer.SpannableMarkdownVisitor import ru.noties.markwon.renderer.SpannableMarkdownVisitor
import timber.log.Timber import timber.log.Timber
import java.util.regex.Pattern
import javax.inject.Inject import javax.inject.Inject
class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) { class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) {
private val parser = Markwon.createParser() private val parser = Markwon.createParser()
private val regexUsername = Pattern.compile("([^\\S]|^)+(@[\\w.\\-]+)",
Pattern.MULTILINE or Pattern.CASE_INSENSITIVE)
private val selfReferList = listOf("@all", "@here")
/** /**
* Render a markdown text message to Spannable. * Render a markdown text message to Spannable.
* *
* @param text The text message containing markdown syntax. * @param message The [Message] object we're interested on rendering.
* @param quote An optional message to be quoted either by a quote or reply action. * @param selfUsername This user username.
* @param urls A list of urls to convert to markdown link syntax.
* *
* @return A Spannable with the parsed markdown. * @return A Spannable with the parsed markdown.
*/ */
fun renderMarkdown(text: String, quote: MessageViewModel? = null, selfUsername: String? = null): CharSequence { fun renderMarkdown(message: Message, selfUsername: String? = null): CharSequence {
val text = message.message
val builder = SpannableBuilder() val builder = SpannableBuilder()
val content = EmojiRepository.shortnameToUnicode(text, true) val content = EmojiRepository.shortnameToUnicode(text, true)
val parentNode = parser.parse(toLenientMarkdown(content)) val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(QuoteMessageBodyVisitor(context, configuration, builder)) parentNode.accept(SpannableMarkdownVisitor(configuration, builder))
quote?.apply {
var quoteNode = parser.parse("> $senderName $time")
parentNode.appendChild(quoteNode)
quoteNode.accept(QuoteMessageSenderVisitor(context, configuration, builder, senderName.length))
quoteNode = parser.parse("> ${toLenientMarkdown(quote.rawData.message)}")
quoteNode.accept(EmojiVisitor(builder))
quoteNode.accept(QuoteMessageBodyVisitor(context, configuration, builder))
}
parentNode.accept(LinkVisitor(builder)) parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(builder)) parentNode.accept(EmojiVisitor(configuration, builder))
val result = builder.text() message.mentions?.let {
applySpans(result, selfUsername) parentNode.accept(MentionVisitor(context, builder, it, selfUsername))
return result
}
private fun applySpans(text: CharSequence, currentUser: String?) {
if (text !is Spannable) return
applyMentionSpans(text, currentUser)
}
private fun applyMentionSpans(text: CharSequence, currentUser: String?) {
val matcher = regexUsername.matcher(text)
val result = text as Spannable
while (matcher.find()) {
val user = matcher.group(2)
val start = matcher.start(2)
//TODO: should check if username actually exists prior to applying.
with(context) {
val referSelf = when (user) {
in selfReferList -> true
"@$currentUser" -> true
else -> false
}
val mentionTextColor: Int
val mentionBgColor: Int
if (referSelf) {
mentionTextColor = ResourcesCompat.getColor(resources, R.color.white, theme)
mentionBgColor = ResourcesCompat.getColor(context.resources,
R.color.colorAccent, theme)
} else {
mentionTextColor = ResourcesCompat.getColor(resources, R.color.colorAccent,
theme)
mentionBgColor = ResourcesCompat.getColor(resources,
android.R.color.transparent, theme)
} }
val padding = resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat() return builder.text()
val radius = resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
val usernameSpan = MentionSpan(mentionBgColor, mentionTextColor, radius, padding,
referSelf)
result.setSpan(usernameSpan, start, start + user.length, 0)
}
}
} }
/** // Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
* Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
*/
private fun toLenientMarkdown(text: String): String { private fun toLenientMarkdown(text: String): String {
return text.trim().replace("\\*(.+)\\*".toRegex()) { "**${it.groupValues[1].trim()}**" } return text.trim().replace("\\*(.+)\\*".toRegex()) { "**${it.groupValues[1].trim()}**" }
.replace("\\~(.+)\\~".toRegex()) { "~~${it.groupValues[1].trim()}~~" } .replace("\\~(.+)\\~".toRegex()) { "~~${it.groupValues[1].trim()}~~" }
.replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" } .replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" }
} }
class QuoteMessageSenderVisitor(private val context: Context, class MentionVisitor(context: Context,
configuration: SpannableConfiguration,
private val builder: SpannableBuilder, private val builder: SpannableBuilder,
private val senderNameLength: Int) : SpannableMarkdownVisitor(configuration, builder) { private val mentions: List<SimpleUser>,
private val currentUser: String?) : AbstractVisitor() {
override fun visit(blockQuote: BlockQuote) { private val othersTextColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme)
// mark current length private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.white, context.theme)
val length = builder.length() private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val mentionPadding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
// pass to super to apply markdown private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
super.visit(blockQuote) override fun visit(t: Text) {
val text = t.literal
val res = context.resources val mentionsList = mentions.map { it.username }.toMutableList()
val timeOffsetStart = length + senderNameLength + 1 mentionsList.add("all")
builder.setSpan(QuoteMarginSpan(context.getDrawable(R.drawable.quote), 10), length, builder.length()) mentionsList.add("here")
builder.setSpan(StyleSpan(Typeface.BOLD), length, length + senderNameLength)
builder.setSpan(ForegroundColorSpan(Color.BLACK), length, builder.length()) mentionsList.toList().forEach {
// set time spans if (it != null) {
builder.setSpan(AbsoluteSizeSpan(res.getDimensionPixelSize(R.dimen.message_time_text_size)), val mentionMe = it == currentUser || it == "all" || it == "here"
timeOffsetStart, builder.length()) var offset = text.indexOf("@$it", 0, true)
builder.setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.darkGray)), while (offset > -1) {
timeOffsetStart, builder.length()) val textColor = if (mentionMe) myselfTextColor else othersTextColor
} val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor
} val usernameSpan = MentionSpan(backgroundColor, textColor, mentionRadius, mentionPadding,
mentionMe)
class EmojiVisitor(private val builder: SpannableBuilder) : AbstractVisitor() { // Add 1 to end offset to include the @.
override fun visit(text: Text) { val end = offset + it.length + 1
val spannable = EmojiParser.parse(text.literal) builder.setSpan(usernameSpan, offset, end, 0)
offset = text.indexOf("@$it", end, true)
}
}
}
}
}
class EmojiVisitor(configuration: SpannableConfiguration, private val builder: SpannableBuilder)
: SpannableMarkdownVisitor(configuration, builder) {
override fun visit(document: Document) {
val spannable = EmojiParser.parse(builder.text())
if (spannable is Spanned) { if (spannable is Spanned) {
val spans = spannable.getSpans(0, spannable.length, EmojiTypefaceSpan::class.java) val spans = spannable.getSpans(0, spannable.length, EmojiTypefaceSpan::class.java)
spans.forEach { spans.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it), 0) builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it), 0)
} }
} }
visitChildren(text)
} }
} }
...@@ -195,60 +150,6 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -195,60 +150,6 @@ class MessageParser @Inject constructor(val context: Application, private val co
} }
} }
class QuoteMessageBodyVisitor(private val context: Context,
configuration: SpannableConfiguration,
private val builder: SpannableBuilder) : SpannableMarkdownVisitor(configuration, builder) {
override fun visit(blockQuote: BlockQuote) {
// mark current length
val length = builder.length()
// pass to super to apply markdown
super.visit(blockQuote)
val padding = context.resources.getDimensionPixelSize(R.dimen.padding_quote)
builder.setSpan(QuoteMarginSpan(context.getDrawable(R.drawable.quote), padding), length,
builder.length())
}
}
class QuoteMarginSpan(quoteDrawable: Drawable, private var pad: Int) : LeadingMarginSpan, LineHeightSpan {
private val drawable: Drawable = quoteDrawable
override fun getLeadingMargin(first: Boolean): Int {
return drawable.intrinsicWidth + pad
}
override fun drawLeadingMargin(c: Canvas, p: Paint, x: Int, dir: Int,
top: Int, baseline: Int, bottom: Int,
text: CharSequence, start: Int, end: Int,
first: Boolean, layout: Layout) {
val st = (text as Spanned).getSpanStart(this)
val ix = x
val itop = layout.getLineTop(layout.getLineForOffset(st))
val dw = drawable.intrinsicWidth
val dh = drawable.intrinsicHeight
// XXX What to do about Paint?
drawable.setBounds(ix, itop, ix + dw, itop + layout.height)
drawable.draw(c)
}
override fun chooseHeight(text: CharSequence, start: Int, end: Int,
spanstartv: Int, v: Int,
fm: Paint.FontMetricsInt) {
if (end == (text as Spanned).getSpanEnd(this)) {
val ht = drawable.intrinsicHeight
var need = ht - (v + fm.descent - fm.ascent - spanstartv)
if (need > 0)
fm.descent += need
need = ht - (v + fm.bottom - fm.top - spanstartv)
if (need > 0)
fm.bottom += need
}
}
}
class MentionSpan(private val backgroundColor: Int, class MentionSpan(private val backgroundColor: Int,
private val textColor: Int, private val textColor: Int,
private val radius: Float, private val radius: Float,
...@@ -274,13 +175,11 @@ class MessageParser @Inject constructor(val context: Application, private val co ...@@ -274,13 +175,11 @@ class MessageParser @Inject constructor(val context: Application, private val co
bottom: Int, bottom: Int,
paint: Paint) { paint: Paint) {
val length = paint.measureText(text.subSequence(start, end).toString()) val length = paint.measureText(text.subSequence(start, end).toString())
val rect = RectF(x, top.toFloat(), x + length + padding * 2, val rect = RectF(x, top.toFloat(), x + length + padding * 2, bottom.toFloat())
bottom.toFloat()) paint.color = backgroundColor
paint.setColor(backgroundColor)
canvas.drawRoundRect(rect, radius, radius, paint) canvas.drawRoundRect(rect, radius, radius, paint)
paint.setColor(textColor) paint.color = textColor
canvas.drawText(text, start, end, x + padding, y.toFloat(), paint) canvas.drawText(text, start, end, x + padding, y.toFloat(), paint)
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.helper
object OauthHelper {
/**
* Returns the Github Oauth URL.
*
* @param clientId The GitHub client ID.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Github Oauth URL.
*/
fun getGithubOauthUrl(clientId: String, state: String): String {
return "https://github.com/login/oauth/authorize" +
"?client_id=$clientId" +
"&state=$state" +
"&scope=user:email"
}
/**
* Returns the Google Oauth URL.
*
* @param clientId The Google client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Google Oauth URL.
*/
fun getGoogleOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://accounts.google.com/o/oauth2/v2/auth" +
"?client_id=$clientId" +
"&redirect_uri=${UrlHelper.removeTrailingSlash(serverUrl)}/_oauth/google?close" +
"&state=$state" +
"&response_type=code" +
"&scope=email%20profile"
}
/**
* Returns the Linkedin Oauth URL.
*
* @param clientId The Linkedin client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Linkedin Oauth URL.
*/
fun getLinkedinOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://linkedin.com/oauth/v2/authorization" +
"?client_id=$clientId" +
"&redirect_uri=${UrlHelper.removeTrailingSlash(serverUrl)}/_oauth/linkedin?close" +
"&state=$state" +
"&response_type=code"
}
/**
* Returns the Gitlab Oauth URL.
*
* @param clientId The Gitlab client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Gitlab Oauth URL.
*/
fun getGitlabOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://gitlab.com/oauth/authorize" +
"?client_id=$clientId" +
"&redirect_uri=${UrlHelper.removeTrailingSlash(serverUrl)}/_oauth/gitlab?close" +
"&state=$state" +
"&response_type=code" +
"&scope=read_user"
}
}
\ No newline at end of file
package chat.rocket.android.helper
import android.content.SharedPreferences
import android.preference.PreferenceManager
import chat.rocket.android.app.RocketChatApplication
object SharedPreferenceHelper {
private var sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(RocketChatApplication.getAppContext())
private var editor: SharedPreferences.Editor? = sharedPreferences.edit()
//Add more methods for other types if needed
fun putInt(key: String, value: Int) {
editor!!.putInt(key, value).apply()
}
fun getInt(key: String, defaultValue: Int): Int {
return sharedPreferences.getInt(key, defaultValue)
}
fun putLong(key: String, value: Long) {
editor!!.putLong(key, value).apply()
}
fun getLong(key: String, defaultValue: Long): Long {
return sharedPreferences.getLong(key, defaultValue)
}
fun putString(key: String, value: String) {
editor!!.putString(key, value).apply()
}
fun getString(key: String, defaultValue: String): String? {
return sharedPreferences.getString(key, defaultValue)
}
fun putBoolean(key: String, value: Boolean) {
editor!!.putBoolean(key, value).apply()
}
fun getBoolean(key: String, defaultValue: Boolean): Boolean {
return sharedPreferences.getBoolean(key, defaultValue)
}
fun remove(key: String) {
editor!!.remove(key).apply()
}
}
\ No newline at end of file
...@@ -14,6 +14,16 @@ object UrlHelper { ...@@ -14,6 +14,16 @@ object UrlHelper {
fun getAvatarUrl(serverUrl: String, avatarName: String, format: String = "jpeg"): String = fun getAvatarUrl(serverUrl: String, avatarName: String, format: String = "jpeg"): String =
removeTrailingSlash(serverUrl) + "/avatar/" + removeTrailingSlash(avatarName) + "?format=$format" removeTrailingSlash(serverUrl) + "/avatar/" + removeTrailingSlash(avatarName) + "?format=$format"
/**
* Returns the server logo URL.
*
* @param serverUrl The server URL.
* @param favicon The faviconLarge from the server settings.
* @return The server logo URL.
*/
fun getServerLogoUrl(serverUrl: String, favicon: String): String =
removeTrailingSlash(serverUrl) + "/$favicon"
/** /**
* Returns the CAS URL. * Returns the CAS URL.
* *
......
...@@ -2,19 +2,28 @@ package chat.rocket.android.infrastructure ...@@ -2,19 +2,28 @@ package chat.rocket.android.infrastructure
interface LocalRepository { interface LocalRepository {
fun save(key: String, value: String?)
fun save(key: String, value: Boolean)
fun save(key: String, value: Int)
fun save(key: String, value: Long)
fun save(key: String, value: Float)
fun get(key: String): String?
fun getBoolean(key: String): Boolean
fun getFloat(key: String): Float
fun getInt(key: String): Int
fun getLong(key: String): Long
fun clear(key: String)
fun clearAllFromServer(server: String)
companion object { companion object {
const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN" const val KEY_PUSH_TOKEN = "KEY_PUSH_TOKEN"
const val MIGRATION_FINISHED_KEY = "MIGRATION_FINISHED_KEY"
const val TOKEN_KEY = "token_" const val TOKEN_KEY = "token_"
const val SETTINGS_KEY = "settings_" const val SETTINGS_KEY = "settings_"
const val USERNAME_KEY = "my_username" const val USERNAME_KEY = "my_username"
const val UNFINISHED_MSG_KEY = "unfinished_msg_" const val UNFINISHED_MSG_KEY = "unfinished_msg_"
const val CURRENT_USERNAME_KEY = "username_"
} }
fun save(key: String, value: String?)
fun get(key: String): String?
fun clear(key: String)
fun clearAllFromServer(server: String)
} }
fun LocalRepository.checkIfMyself(username: String) = get(LocalRepository.CURRENT_USERNAME_KEY) == username
\ No newline at end of file
...@@ -3,23 +3,32 @@ package chat.rocket.android.infrastructure ...@@ -3,23 +3,32 @@ package chat.rocket.android.infrastructure
import android.content.SharedPreferences import android.content.SharedPreferences
class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : LocalRepository { class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : LocalRepository {
override fun getBoolean(key: String) = preferences.getBoolean(key, false)
override fun save(key: String, value: String?) { override fun getFloat(key: String) = preferences.getFloat(key, -1f)
preferences.edit().putString(key, value).apply()
}
override fun get(key: String): String? { override fun getInt(key: String) = preferences.getInt(key, -1)
return preferences.getString(key, null)
}
override fun clear(key: String) { override fun getLong(key: String) = preferences.getLong(key, -1L)
preferences.edit().remove(key).apply()
} override fun save(key: String, value: Int) = preferences.edit().putInt(key, value).apply()
override fun save(key: String, value: Float) = preferences.edit().putFloat(key, value).apply()
override fun save(key: String, value: Long) = preferences.edit().putLong(key, value).apply()
override fun save(key: String, value: Boolean) = preferences.edit().putBoolean(key, value).apply()
override fun save(key: String, value: String?) = preferences.edit().putString(key, value).apply()
override fun get(key: String): String? = preferences.getString(key, null)
override fun clear(key: String) = preferences.edit().remove(key).apply()
override fun clearAllFromServer(server: String) { override fun clearAllFromServer(server: String) {
clear(LocalRepository.KEY_PUSH_TOKEN) clear(LocalRepository.KEY_PUSH_TOKEN)
clear(LocalRepository.TOKEN_KEY + server) clear(LocalRepository.TOKEN_KEY + server)
clear(LocalRepository.SETTINGS_KEY + server) clear(LocalRepository.SETTINGS_KEY + server)
clear(LocalRepository.USERNAME_KEY + server) clear(LocalRepository.CURRENT_USERNAME_KEY)
} }
} }
\ No newline at end of file
package chat.rocket.android.main.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
import chat.rocket.android.server.domain.model.Account
import kotlinx.android.synthetic.main.item_account.view.*
class AccountViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(account: Account) {
with(itemView) {
server_logo.setImageURI(account.serverLogo)
text_server_url.text = account.serverUrl
text_username.text = account.userName
}
}
}
\ No newline at end of file
package chat.rocket.android.main.adapter
import android.support.v7.widget.RecyclerView
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.util.extensions.inflate
class AccountsAdapter(
private val accounts: List<Account>,
private val selector: AccountSelector
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_ACCOUNT -> AccountViewHolder(parent.inflate(R.layout.item_account))
else -> AddAccountViewHolder(parent.inflate(R.layout.item_add_account))
}
}
override fun getItemCount() = accounts.size + 1
override fun getItemViewType(position: Int) =
if (position == accounts.size) VIEW_TYPE_ADD_ACCOUNT else VIEW_TYPE_ACCOUNT
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is AccountViewHolder -> bindAccountViewHolder(holder, position)
is AddAccountViewHolder -> bindAddAccountViewHolder(holder, position)
}
}
private fun bindAccountViewHolder(holder: AccountViewHolder, position: Int) {
val account = accounts[position]
holder.bind(account)
holder.itemView.setOnClickListener {
selector.onAccountSelected(account.serverUrl)
}
}
private fun bindAddAccountViewHolder(holder: AddAccountViewHolder, position: Int) {
holder.itemView.setOnClickListener {
selector.onAddedAccountSelected()
}
}
}
interface AccountSelector {
fun onAccountSelected(serverUrl: String)
fun onAddedAccountSelected()
}
private const val VIEW_TYPE_ACCOUNT = 0
private const val VIEW_TYPE_ADD_ACCOUNT = 1
\ No newline at end of file
package chat.rocket.android.main.adapter
import android.support.v7.widget.RecyclerView
import android.view.View
class AddAccountViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
\ No newline at end of file
...@@ -16,7 +16,7 @@ class MainModule { ...@@ -16,7 +16,7 @@ class MainModule {
@Provides @Provides
@PerActivity @PerActivity
fun provideMainNavigator(activity: MainActivity, context: Context) = MainNavigator(activity, context) fun provideMainNavigator(activity: MainActivity) = MainNavigator(activity)
@Provides @Provides
fun provideMainView(activity: MainActivity): MainView = activity fun provideMainView(activity: MainActivity): MainView = activity
......
...@@ -2,14 +2,16 @@ package chat.rocket.android.main.presentation ...@@ -2,14 +2,16 @@ package chat.rocket.android.main.presentation
import android.content.Context import android.content.Context
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.chatroom.ui.chatRoomIntent import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.ui.ProfileFragment import chat.rocket.android.profile.ui.ProfileFragment
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.settings.ui.SettingsFragment import chat.rocket.android.settings.ui.SettingsFragment
import chat.rocket.android.util.extensions.addFragment import chat.rocket.android.util.extensions.addFragment
class MainNavigator(internal val activity: MainActivity, internal val context: Context) { class MainNavigator(internal val activity: MainActivity) {
fun toChatList() { fun toChatList() {
activity.addFragment("ChatRoomsFragment", R.id.fragment_container) { activity.addFragment("ChatRoomsFragment", R.id.fragment_container) {
...@@ -35,8 +37,17 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C ...@@ -35,8 +37,17 @@ class MainNavigator(internal val activity: MainActivity, internal val context: C
isChatRoomReadOnly: Boolean, isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long, chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean) { isChatRoomSubscribed: Boolean) {
activity.startActivity(context.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType, activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed)) isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit) activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
} }
fun toNewServer(serverUrl: String? = null) {
activity.startActivity(activity.changeServerIntent(serverUrl))
activity.finish()
}
fun toServerScreen() {
activity.startActivity(activity.newServerIntent())
}
} }
\ No newline at end of file
package chat.rocket.android.main.presentation package chat.rocket.android.main.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.main.viewmodel.NavHeaderViewModelMapper
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.launchUI import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.disconnect
import chat.rocket.core.internal.rest.logout import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken import chat.rocket.core.internal.rest.unregisterPushToken
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MainPresenter @Inject constructor(private val view: MainView, class MainPresenter @Inject constructor(
private val view: MainView,
private val strategy: CancelStrategy, private val strategy: CancelStrategy,
private val navigator: MainNavigator, private val navigator: MainNavigator,
private val tokenRepository: TokenRepository,
private val serverInteractor: GetCurrentServerInteractor, private val serverInteractor: GetCurrentServerInteractor,
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
managerFactory: ConnectionManagerFactory, private val navHeaderMapper: NavHeaderViewModelMapper,
factory: RocketChatClientFactory) { private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInterector: RemoveAccountInterector,
private val factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor,
managerFactory: ConnectionManagerFactory
) : CheckServerPresenter(strategy, client = factory.create(serverInteractor.get()!!), view = view) {
private val currentServer = serverInteractor.get()!! private val currentServer = serverInteractor.get()!!
private val manager = managerFactory.create(currentServer) private val manager = managerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer) private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
fun toChatList() = navigator.toChatList() fun toChatList() = navigator.toChatList()
...@@ -31,6 +48,41 @@ class MainPresenter @Inject constructor(private val view: MainView, ...@@ -31,6 +48,41 @@ class MainPresenter @Inject constructor(private val view: MainView,
fun toSettings() = navigator.toSettings() fun toSettings() = navigator.toSettings()
fun loadCurrentInfo() {
checkServerInfo()
launchUI(strategy) {
try {
val me = client.me()
val model = navHeaderMapper.mapToViewModel(me)
saveAccount(model)
view.setupNavHeader(model, getAccountsInteractor.get())
} catch (ex: Exception) {
when (ex) {
is RocketChatAuthException -> {
logout()
}
else -> {
Timber.d(ex, "Error loading my information for navheader")
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
}
}
}
private suspend fun saveAccount(me: NavHeaderViewModel) {
val icon = settings.favicon()?.let {
UrlHelper.getServerLogoUrl(currentServer, it)
}
val account = Account(currentServer, icon, me.serverLogo, me.username, me.avatar)
saveAccountInteractor.save(account)
}
/** /**
* Logout from current server. * Logout from current server.
*/ */
...@@ -39,16 +91,25 @@ class MainPresenter @Inject constructor(private val view: MainView, ...@@ -39,16 +91,25 @@ class MainPresenter @Inject constructor(private val view: MainView,
try { try {
clearTokens() clearTokens()
client.logout() client.logout()
//TODO: Add the code to unsubscribe to all subscriptions.
client.disconnect()
view.onLogout()
} catch (exception: RocketChatException) { } catch (exception: RocketChatException) {
Timber.d(exception, "Error calling logout")
exception.message?.let { exception.message?.let {
view.showMessage(it) view.showMessage(it)
}.ifNull { }.ifNull {
view.showGenericErrorMessage() view.showGenericErrorMessage()
} }
} }
try {
disconnect()
removeAccountInterector.remove(currentServer)
tokenRepository.remove(currentServer)
navigator.toNewServer()
} catch (ex: Exception) {
Timber.d(ex, "Error cleaning up the session...")
}
navigator.toNewServer()
} }
} }
...@@ -57,7 +118,6 @@ class MainPresenter @Inject constructor(private val view: MainView, ...@@ -57,7 +118,6 @@ class MainPresenter @Inject constructor(private val view: MainView,
val pushToken = localRepository.get(LocalRepository.KEY_PUSH_TOKEN) val pushToken = localRepository.get(LocalRepository.KEY_PUSH_TOKEN)
if (pushToken != null) { if (pushToken != null) {
client.unregisterPushToken(pushToken) client.unregisterPushToken(pushToken)
localRepository.clear(LocalRepository.KEY_PUSH_TOKEN)
} }
localRepository.clearAllFromServer(currentServer) localRepository.clearAllFromServer(currentServer)
} }
...@@ -69,4 +129,22 @@ class MainPresenter @Inject constructor(private val view: MainView, ...@@ -69,4 +129,22 @@ class MainPresenter @Inject constructor(private val view: MainView,
fun disconnect() { fun disconnect() {
manager.disconnect() manager.disconnect()
} }
fun changeServer(serverUrl: String) {
if (currentServer != serverUrl) {
navigator.toNewServer(serverUrl)
} else {
view.closeServerSelection()
}
}
fun addNewServer() {
navigator.toServerScreen()
}
suspend fun refreshToken(token: String?) {
token?.let {
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
}
} }
\ No newline at end of file
package chat.rocket.android.main.presentation package chat.rocket.android.main.presentation
import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.behaviours.MessageView import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.model.Account
interface MainView : MessageView { interface MainView : MessageView, VersionCheckView {
fun setupNavHeader(model: NavHeaderViewModel, accounts: List<Account>)
/** fun closeServerSelection()
* User has successfully logged out from the current server.
**/
fun onLogout()
} }
\ No newline at end of file
package chat.rocket.android.main.ui package chat.rocket.android.main.ui
import android.app.Activity import android.app.Activity
import android.content.Intent import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.view.Gravity import android.view.Gravity
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import chat.rocket.android.BuildConfig
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.authentication.ui.AuthenticationActivity import chat.rocket.android.main.adapter.AccountSelector
import chat.rocket.android.main.adapter.AccountsAdapter
import chat.rocket.android.main.presentation.MainPresenter import chat.rocket.android.main.presentation.MainPresenter
import chat.rocket.android.main.presentation.MainView import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.viewmodel.NavHeaderViewModel
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.util.extensions.fadeIn
import chat.rocket.android.util.extensions.fadeOut
import chat.rocket.android.util.extensions.rotateBy
import chat.rocket.android.util.extensions.showToast import chat.rocket.android.util.extensions.showToast
import com.google.android.gms.gcm.GoogleCloudMessaging
import com.google.android.gms.iid.InstanceID
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
...@@ -19,6 +30,10 @@ import dagger.android.HasActivityInjector ...@@ -19,6 +30,10 @@ import dagger.android.HasActivityInjector
import dagger.android.support.HasSupportFragmentInjector import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.app_bar.* import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.nav_header.view.*
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupportFragmentInjector { class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupportFragmentInjector {
...@@ -26,13 +41,21 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp ...@@ -26,13 +41,21 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> @Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject lateinit var presenter: MainPresenter @Inject lateinit var presenter: MainPresenter
private var isFragmentAdded: Boolean = false private var isFragmentAdded: Boolean = false
private var expanded = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this) AndroidInjection.inject(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
launch(CommonPool) {
val token = InstanceID.getInstance(this@MainActivity).getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
Timber.d("GCM token: $token")
presenter.refreshToken(token)
}
presenter.connect() presenter.connect()
presenter.loadCurrentInfo()
setupToolbar() setupToolbar()
setupNavigationView() setupNavigationView()
} }
...@@ -52,11 +75,66 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp ...@@ -52,11 +75,66 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
} }
} }
override fun onLogout() { override fun setupNavHeader(model: NavHeaderViewModel, accounts: List<Account>) {
finish() Timber.d("Setting up nav header: $model")
val intent = Intent(this, AuthenticationActivity::class.java) val headerLayout = view_navigation.getHeaderView(0)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) headerLayout.text_name.text = model.username
startActivity(intent) headerLayout.text_server.text = model.server
headerLayout.image_avatar.setImageURI(model.avatar)
headerLayout.server_logo.setImageURI(model.serverLogo)
setupAccountsList(headerLayout, accounts)
}
override fun closeServerSelection() {
view_navigation.getHeaderView(0).account_container.performClick()
}
override fun alertNotRecommendedVersion() {
AlertDialog.Builder(this)
.setMessage(getString(R.string.msg_ver_not_recommended, BuildConfig.RECOMMENDED_SERVER_VERSION))
.setPositiveButton(R.string.msg_ok, null)
.create()
.show()
}
override fun blockAndAlertNotRequiredVersion() {
AlertDialog.Builder(this)
.setMessage(getString(R.string.msg_ver_not_minimum, BuildConfig.REQUIRED_SERVER_VERSION))
.setOnDismissListener { presenter.logout() }
.setPositiveButton(R.string.msg_ok, null)
.create()
.show()
}
private fun setupAccountsList(header: View, accounts: List<Account>) {
accounts_list.layoutManager = LinearLayoutManager(this)
accounts_list.adapter = AccountsAdapter(accounts, object : AccountSelector {
override fun onAccountSelected(serverUrl: String) {
presenter.changeServer(serverUrl)
}
override fun onAddedAccountSelected() {
presenter.addNewServer()
}
})
header.account_container.setOnClickListener {
header.account_expand.rotateBy(180f)
if (expanded) {
accounts_list.fadeOut()
} else {
accounts_list.fadeIn()
}
expanded = !expanded
}
header.image_avatar.setOnClickListener {
view_navigation.menu.findItem(R.id.action_profile).isChecked = true
presenter.toUserProfile()
drawer_layout.closeDrawer(Gravity.START)
}
} }
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) = showToast(resId)
......
package chat.rocket.android.main.viewmodel
data class NavHeaderViewModel(
val username: String,
val server: String,
val avatar: String?,
val serverLogo: String?
)
\ No newline at end of file
package chat.rocket.android.main.viewmodel
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.server.domain.*
import chat.rocket.core.model.Myself
import javax.inject.Inject
class NavHeaderViewModelMapper @Inject constructor(serverInteractor: GetCurrentServerInteractor,
getSettingsInteractor: GetSettingsInteractor) {
private val currentServer = serverInteractor.get()!!
private var settings: PublicSettings = getSettingsInteractor.get(currentServer)
fun mapToViewModel(me: Myself): NavHeaderViewModel {
val username = mapUsername(me)
val thumb = me.username?.let { UrlHelper.getAvatarUrl(currentServer, it) }
val image = settings.wideTile() ?: settings.faviconLarge()
val logo = image?.let { UrlHelper.getServerLogoUrl(currentServer, it) }
return NavHeaderViewModel(username, currentServer, thumb, logo)
}
private fun mapUsername(me: Myself): String {
val username = me.username
val realName = me.name
val senderName = if (settings.useRealName()) realName else username
return senderName ?: username.toString()
}
}
\ No newline at end of file
...@@ -6,6 +6,7 @@ import android.support.v7.app.AppCompatActivity ...@@ -6,6 +6,7 @@ import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import chat.rocket.android.R import chat.rocket.android.R
...@@ -83,16 +84,31 @@ class MembersFragment : Fragment(), MembersView { ...@@ -83,16 +84,31 @@ class MembersFragment : Fragment(), MembersView {
} else { } else {
adapter.appendData(dataSet) adapter.appendData(dataSet)
} }
if (this is ChatRoomActivity) {
this.showRoomTypeIcon(false)
} }
} }
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
(activity as ChatRoomActivity).showRoomTypeIcon(true)
return super.onOptionsItemSelected(item)
}
return super.onOptionsItemSelected(item)
}
override fun showLoading() = view_loading.setVisible(true) override fun showLoading() = view_loading.setVisible(true)
override fun hideLoading() = view_loading.setVisible(false) override fun hideLoading() = view_loading.setVisible(false)
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message) override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
...@@ -56,8 +56,8 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -56,8 +56,8 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
text_email.textContent = email text_email.textContent = email
text_avatar_url.textContent = "" text_avatar_url.textContent = ""
currentName = username currentName = name
currentUsername = name currentUsername = username
currentEmail = email currentEmail = email
currentAvatar = avatarUrl currentAvatar = avatarUrl
...@@ -66,7 +66,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -66,7 +66,9 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
listenToChanges() listenToChanges()
} }
override fun showProfileUpdateSuccessfullyMessage() = showMessage(getString(R.string.msg_profile_update_successfully)) override fun showProfileUpdateSuccessfullyMessage() {
showMessage(getString(R.string.msg_profile_update_successfully))
}
override fun showLoading() { override fun showLoading() {
enableUserInput(false) enableUserInput(false)
...@@ -74,13 +76,19 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback { ...@@ -74,13 +76,19 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
} }
override fun hideLoading() { override fun hideLoading() {
if (view_loading != null) {
view_loading.setVisible(false) view_loading.setVisible(false)
}
enableUserInput(true) enableUserInput(true)
} }
override fun showMessage(resId: Int) = showToast(resId) override fun showMessage(resId: Int) {
showToast(resId)
}
override fun showMessage(message: String) = showToast(message) override fun showMessage(message: String) {
showToast(message)
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error)) override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
......
package chat.rocket.android.push
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import dagger.android.AndroidInjection
import javax.inject.Inject
/**
* BroadcastReceiver for dismissed notifications.
*/
class DeleteReceiver : BroadcastReceiver() {
@Inject
lateinit var groupedPushes: GroupedPush
override fun onReceive(context: Context, intent: Intent) {
AndroidInjection.inject(this, context)
val notId = intent.extras?.getInt(EXTRA_NOT_ID)
val host = intent.extras?.getString(EXTRA_HOSTNAME)
if (host != null && notId != null) {
clearNotificationsByHostAndNotificationId(host, notId)
}
}
/**
* Clear notifications by the host they belong to and its unique id.
*/
fun clearNotificationsByHostAndNotificationId(host: String, notificationId: Int) {
if (groupedPushes.hostToPushMessageList.isNotEmpty()) {
val notifications = groupedPushes.hostToPushMessageList[host]
notifications?.let {
notifications.removeAll {
it.notificationId.toInt() == notificationId
}
}
}
}
}
\ No newline at end of file
...@@ -2,6 +2,8 @@ package chat.rocket.android.push ...@@ -2,6 +2,8 @@ package chat.rocket.android.push
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.infrastructure.LocalRepository import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.common.RocketChatException import chat.rocket.common.RocketChatException
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.registerPushToken import chat.rocket.core.internal.rest.registerPushToken
...@@ -16,14 +18,16 @@ import javax.inject.Inject ...@@ -16,14 +18,16 @@ import javax.inject.Inject
class FirebaseTokenService : FirebaseInstanceIdService() { class FirebaseTokenService : FirebaseInstanceIdService() {
@Inject @Inject
lateinit var client: RocketChatClient lateinit var factory: RocketChatClientFactory
@Inject
lateinit var getCurrentServerInteractor: GetCurrentServerInteractor
@Inject @Inject
lateinit var localRepository: LocalRepository lateinit var localRepository: LocalRepository
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
AndroidInjection.inject(this); AndroidInjection.inject(this)
} }
override fun onTokenRefresh() { override fun onTokenRefresh() {
...@@ -31,11 +35,14 @@ class FirebaseTokenService : FirebaseInstanceIdService() { ...@@ -31,11 +35,14 @@ class FirebaseTokenService : FirebaseInstanceIdService() {
// default push gateway. We should register this project's own project sender id into it. // default push gateway. We should register this project's own project sender id into it.
val gcmToken = InstanceID.getInstance(this) val gcmToken = InstanceID.getInstance(this)
.getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null) .getToken(getString(R.string.gcm_sender_id), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null)
val currentServer = getCurrentServerInteractor.get()!!
val client = factory.create(currentServer)
gcmToken?.let { gcmToken?.let {
localRepository.save(LocalRepository.KEY_PUSH_TOKEN, gcmToken) localRepository.save(LocalRepository.KEY_PUSH_TOKEN, gcmToken)
launch { launch {
try { try {
Timber.d("Registering push token: $gcmToken for ${client.url}")
client.registerPushToken(gcmToken) client.registerPushToken(gcmToken)
} catch (ex: RocketChatException) { } catch (ex: RocketChatException) {
Timber.e(ex) Timber.e(ex)
......
...@@ -2,12 +2,22 @@ package chat.rocket.android.push ...@@ -2,12 +2,22 @@ package chat.rocket.android.push
import android.os.Bundle import android.os.Bundle
import com.google.android.gms.gcm.GcmListenerService import com.google.android.gms.gcm.GcmListenerService
import dagger.android.AndroidInjection
import javax.inject.Inject
class GcmListenerService : GcmListenerService() { class GcmListenerService : GcmListenerService() {
@Inject
lateinit var pushManager: PushManager
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onMessageReceived(from: String?, data: Bundle?) { override fun onMessageReceived(from: String?, data: Bundle?) {
data?.let { data?.let {
PushManager.handle(this, data) pushManager.handle(data)
} }
} }
} }
\ No newline at end of file
package chat.rocket.android.push
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Singleton
typealias TupleGroupIdMessageCount = Pair<Int, AtomicInteger>
class GroupedPush {
// Notifications received from the same server are grouped in a single bundled notification.
// This map associates a host to a group id.
val groupMap = HashMap<String, TupleGroupIdMessageCount>()
// Map a hostname to a list of push messages that pertain to it.
val hostToPushMessageList = HashMap<String, MutableList<PushMessage>>()
}
\ No newline at end of file
...@@ -5,7 +5,6 @@ import android.app.Notification ...@@ -5,7 +5,6 @@ import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.RingtoneManager import android.media.RingtoneManager
...@@ -17,36 +16,34 @@ import android.support.v4.app.NotificationManagerCompat ...@@ -17,36 +16,34 @@ import android.support.v4.app.NotificationManagerCompat
import android.support.v4.app.RemoteInput import android.support.v4.app.RemoteInput
import android.text.Html import android.text.Html
import android.text.Spanned import android.text.Spanned
import android.util.Log
import chat.rocket.android.BuildConfig
import chat.rocket.android.R import chat.rocket.android.R
import chat.rocket.android.main.ui.MainActivity import chat.rocket.android.main.ui.MainActivity
import org.json.JSONObject import chat.rocket.android.server.domain.GetAccountInteractor
import java.io.Serializable import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.siteName
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.common.model.RoomType
import com.squareup.moshi.Json
import com.squareup.moshi.Moshi
import kotlinx.coroutines.experimental.runBlocking
import se.ansman.kotshi.JsonSerializable
import timber.log.Timber
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlin.collections.HashMap import javax.inject.Inject
typealias TupleGroupIdMessageCount = Pair<Int, AtomicInteger>
/** /**
* Refer to: https://github.com/RocketChat/Rocket.Chat.Android/blob/9e846b7fde8fe0c74b9e0117c37ce49293308db5/app/src/main/java/chat/rocket/android/push/PushManager.kt * Refer to: https://github.com/RocketChat/Rocket.Chat.Android/blob/9e846b7fde8fe0c74b9e0117c37ce49293308db5/app/src/main/java/chat/rocket/android/push/PushManager.kt
* for old source code. * for old source code.
*/ */
object PushManager { class PushManager @Inject constructor(
const val EXTRA_NOT_ID = "chat.rocket.android.EXTRA_NOT_ID" private val groupedPushes: GroupedPush,
const val EXTRA_HOSTNAME = "chat.rocket.android.EXTRA_HOSTNAME" private val manager: NotificationManager,
const val EXTRA_PUSH_MESSAGE = "chat.rocket.android.EXTRA_PUSH_MESSAGE" private val moshi: Moshi,
const val EXTRA_ROOM_ID = "chat.rocket.android.EXTRA_ROOM_ID" private val getAccountInteractor: GetAccountInteractor,
private const val REPLY_LABEL = "REPLY" private val getSettingsInteractor: GetSettingsInteractor,
private const val REMOTE_INPUT_REPLY = "REMOTE_INPUT_REPLY" private val context: Context
) {
// Notifications received from the same server are grouped in a single bundled notification.
// This map associates a host to a group id.
private val groupMap = HashMap<String, TupleGroupIdMessageCount>()
// Map a hostname to a list of push messages that pertain to it.
private val hostToPushMessageList = HashMap<String, MutableList<PushMessage>>()
private val randomizer = Random() private val randomizer = Random()
/** /**
...@@ -54,187 +51,108 @@ object PushManager { ...@@ -54,187 +51,108 @@ object PushManager {
* on the *data* param bundle received. * on the *data* param bundle received.
*/ */
@Synchronized @Synchronized
fun handle(context: Context, data: Bundle) { fun handle(data: Bundle) = runBlocking {
val appContext = context.applicationContext
val message = data["message"] as String? val message = data["message"] as String?
val image = data["image"] as String?
val ejson = data["ejson"] as String? val ejson = data["ejson"] as String?
val title = data["title"] as String?
val notId = data["notId"] as String? ?: randomizer.nextInt().toString() val notId = data["notId"] as String? ?: randomizer.nextInt().toString()
val image = data["image"] as String?
val style = data["style"] as String? val style = data["style"] as String?
val summaryText = data["summaryText"] as String? val summaryText = data["summaryText"] as String?
val count = data["count"] as String? val count = data["count"] as String?
val title = data["title"] as String?
if (ejson == null || message == null || title == null) { try {
return val adapter = moshi.adapter<PushInfo>(PushInfo::class.java)
} val info = adapter.fromJson(ejson)
val lastPushMessage = PushMessage(title, message, image, ejson, count, notId, summaryText, style) val pushMessage = PushMessage(title!!, message!!, info!!, image, count, notId, summaryText, style)
// We should use Timber here Timber.d("Received push message: $pushMessage")
if (BuildConfig.DEBUG) {
Log.d(PushMessage::class.java.simpleName, lastPushMessage.toString())
}
showNotification(appContext, lastPushMessage)
}
/**
* Clear all messages received to a given host the user is signed-in.
*/
fun clearNotificationsByHost(host: String) {
hostToPushMessageList.remove(host)
}
/**
* Remove a notification solely by it's unique id.
*/
fun clearNotificationsByNotificationId(notificationId: Int) {
if (hostToPushMessageList.isNotEmpty()) {
for (entry in hostToPushMessageList.entries) {
entry.value.removeAll {
it.notificationId.toInt() == notificationId
}
}
}
}
/**
* Clear notifications by the host they belong to and its unique id.
*/
fun clearNotificationsByHostAndNotificationId(host: String?, notificationId: Int?) {
if (host == null || notificationId == null) {
return
}
if (hostToPushMessageList.isNotEmpty()) {
val notifications = hostToPushMessageList[host]
notifications?.let {
notifications.removeAll {
it.notificationId.toInt() == notificationId
}
}
}
}
private fun getGroupForHost(host: String): TupleGroupIdMessageCount { showNotification(pushMessage)
val size = groupMap.size } catch (ex: Exception) {
var group = groupMap.get(host) Timber.d(ex, "Error parsing PUSH message: $data")
if (group == null) { ex.printStackTrace()
group = TupleGroupIdMessageCount(size + 1, AtomicInteger(0))
groupMap.put(host, group)
} }
return group
} }
@SuppressLint("NewApi") @SuppressLint("NewApi")
internal fun showNotification(context: Context, lastPushMessage: PushMessage) { private suspend fun showNotification(pushMessage: PushMessage) {
if (lastPushMessage.host == null || lastPushMessage.message == null || lastPushMessage.title == null) { if (!hasAccount(pushMessage.info.host)) {
Timber.d("ignoring push message: $pushMessage")
return return
} }
val manager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notId = lastPushMessage.notificationId.toInt() val notId = pushMessage.notificationId.toInt()
val host = lastPushMessage.host val host = pushMessage.info.host
val groupTuple = getGroupForHost(host) val groupTuple = getGroupForHost(host)
groupTuple.second.incrementAndGet() groupTuple.second.incrementAndGet()
val notIdListForHostname: MutableList<PushMessage>? = hostToPushMessageList.get(host) val notIdListForHostname: MutableList<PushMessage>? = groupedPushes.hostToPushMessageList.get(host)
if (notIdListForHostname == null) { if (notIdListForHostname == null) {
hostToPushMessageList.put(host, arrayListOf(lastPushMessage)) groupedPushes.hostToPushMessageList[host] = arrayListOf(pushMessage)
} else { } else {
notIdListForHostname.add(0, lastPushMessage) notIdListForHostname.add(0, pushMessage)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val notification = createSingleNotificationForNougatAndAbove(context, lastPushMessage)
val groupNotification = createGroupNotificationForNougatAndAbove(context, lastPushMessage)
notification?.let {
manager.notify(notId, notification)
} }
groupNotification?.let { val notification = createSingleNotification(pushMessage)
manager.notify(groupTuple.first, groupNotification) val pushMessageList = groupedPushes.hostToPushMessageList[host]
}
} else {
val notification = createSingleNotification(context, lastPushMessage)
val pushMessageList = hostToPushMessageList.get(host)
notification?.let { notification?.let {
NotificationManagerCompat.from(context).notify(notId, notification) manager.notify(notId, notification)
} }
pushMessageList?.let { pushMessageList?.let {
if (pushMessageList.size > 1) { if (pushMessageList.size > 1) {
val groupNotification = createGroupNotification(context, lastPushMessage) val groupNotification = createGroupNotification(pushMessage)
groupNotification?.let { groupNotification?.let {
NotificationManagerCompat.from(context).notify(groupTuple.first, groupNotification) NotificationManagerCompat.from(context).notify(groupTuple.first, groupNotification)
} }
} }
} }
} }
}
internal fun createGroupNotification(context: Context, lastPushMessage: PushMessage): Notification? { private fun getGroupForHost(host: String): TupleGroupIdMessageCount {
with(lastPushMessage) { val size = groupedPushes.groupMap.size
if (host == null || message == null || title == null) { var group = groupedPushes.groupMap[host]
return null if (group == null) {
group = TupleGroupIdMessageCount(size + 1, AtomicInteger(0))
groupedPushes.groupMap[host] = group
}
return group
} }
val id = lastPushMessage.notificationId.toInt()
val contentIntent = getContentIntent(context, id, lastPushMessage)
val deleteIntent = getDismissIntent(context, lastPushMessage)
val builder = NotificationCompat.Builder(context)
.setWhen(createdAt)
.setContentTitle(title.fromHtml())
.setContentText(message.fromHtml())
.setGroup(host)
.setGroupSummary(true)
.setContentIntent(contentIntent)
.setDeleteIntent(deleteIntent)
.setMessageNotification()
//TODO: Get Site_Name PublicSetting from cache private suspend fun hasAccount(host: String): Boolean {
val subText = "Rocket.Chat" return getAccountInteractor.get(host) != null
if (subText.isNotEmpty()) {
builder.setSubText(subText)
} }
@SuppressLint("NewApi")
@RequiresApi(Build.VERSION_CODES.N)
private fun createGroupNotification(pushMessage: PushMessage): Notification? {
with(pushMessage) {
val host = info.host
val builder = createBaseNotificationBuilder(pushMessage, grouped = true)
.setGroupSummary(true)
if (style == null || style == "inbox") { if (style == null || style == "inbox") {
val pushMessageList = hostToPushMessageList.get(host) val pushMessageList = groupedPushes.hostToPushMessageList[host]
pushMessageList?.let { pushMessageList?.let {
val messageCount = pushMessageList.size val count = pushMessageList.filter {
val summary = summaryText?.replace("%n%", messageCount.toString()) it.title == title
?.fromHtml() ?: "$messageCount new messages" }.size
builder.setNumber(messageCount)
if (messageCount > 1) { builder.setContentTitle(getTitle(count, title))
val firstPush = pushMessageList[0]
val singleConversation = pushMessageList.filter {
firstPush.sender?.username != it.sender?.username
}.isEmpty()
val inbox = NotificationCompat.InboxStyle() val inbox = NotificationCompat.InboxStyle()
.setBigContentTitle(if (singleConversation) title else summary) .setBigContentTitle(getTitle(count, title))
for (push in pushMessageList) { for (push in pushMessageList) {
if (singleConversation) {
inbox.addLine(push.message) inbox.addLine(push.message)
} else {
inbox.addLine("<font color='black'>${push.title}</font> <font color='gray'>${push.message}</font>".fromHtml())
}
} }
builder.setStyle(inbox) builder.setStyle(inbox)
} else {
val firstMsg = pushMessageList[0]
if (firstMsg.host == null || firstMsg.message == null || firstMsg.title == null) {
return null
}
val bigText = NotificationCompat.BigTextStyle()
.bigText(firstMsg.message.fromHtml())
.setBigContentTitle(firstMsg.title.fromHtml())
builder.setStyle(bigText)
}
} }
} else { } else {
val bigText = NotificationCompat.BigTextStyle() val bigText = NotificationCompat.BigTextStyle()
...@@ -250,147 +168,69 @@ object PushManager { ...@@ -250,147 +168,69 @@ object PushManager {
@SuppressLint("NewApi") @SuppressLint("NewApi")
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
internal fun createGroupNotificationForNougatAndAbove(context: Context, lastPushMessage: PushMessage): Notification? { private fun createSingleNotification(pushMessage: PushMessage): Notification? {
with(lastPushMessage) { with(pushMessage) {
if (host == null || message == null || title == null) { val host = info.host
return null
}
val manager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val id = notificationId.toInt()
val contentIntent = getContentIntent(context, id, lastPushMessage, grouped = true)
val deleteIntent = getDismissIntent(context, lastPushMessage)
val builder = Notification.Builder(context) val builder = createBaseNotificationBuilder(pushMessage)
.setWhen(createdAt) .setGroupSummary(false)
.setContentTitle(title.fromHtml())
.setContentText(message.fromHtml())
.setGroup(host)
.setGroupSummary(true)
.setContentIntent(contentIntent)
.setDeleteIntent(deleteIntent)
.setMessageNotification(context)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (style == null || "inbox" == style) {
builder.setChannelId(host) val pushMessageList = groupedPushes.hostToPushMessageList.get(host)
val groupChannel = NotificationChannel(host, host, NotificationManager.IMPORTANCE_HIGH)
groupChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
groupChannel.enableLights(false)
groupChannel.enableVibration(true)
groupChannel.setShowBadge(true)
manager.createNotificationChannel(groupChannel)
}
//TODO: Get Site_Name PublicSetting from cache pushMessageList?.let {
val subText = "Rocket.Chat" val userMessages = pushMessageList.filter {
if (subText.isNotEmpty()) { it.notificationId == pushMessage.notificationId
builder.setSubText(subText)
} }
if (style == null || style == "inbox") {
val pushMessageList = hostToPushMessageList.get(host)
pushMessageList?.let {
val count = pushMessageList.filter { val count = pushMessageList.filter {
it.title == title it.title == title
}.size }.size
builder.setContentTitle(getTitle(count, title)) builder.setContentTitle(getTitle(count, title))
val inbox = Notification.InboxStyle() if (count > 1) {
.setBigContentTitle(getTitle(count, title)) val inbox = NotificationCompat.InboxStyle()
inbox.setBigContentTitle(getTitle(count, title))
for (push in pushMessageList) { for (push in userMessages) {
inbox.addLine(push.message) inbox.addLine(push.message)
} }
builder.setStyle(inbox) builder.setStyle(inbox)
}
} else { } else {
val bigText = Notification.BigTextStyle() val bigTextStyle = NotificationCompat.BigTextStyle()
.bigText(message.fromHtml()) .bigText(message.fromHtml())
.setBigContentTitle(title.fromHtml()) builder.setStyle(bigTextStyle)
builder.setStyle(bigText)
}
return builder.build()
}
}
internal fun createSingleNotification(context: Context, lastPushMessage: PushMessage): Notification? {
with(lastPushMessage) {
if (host == null || message == null || title == null) {
return null
}
val id = notificationId.toInt()
val contentIntent = getContentIntent(context, id, lastPushMessage)
val deleteIntent = getDismissIntent(context, lastPushMessage)
val builder = NotificationCompat.Builder(context)
.setWhen(createdAt)
.setContentTitle(title.fromHtml())
.setContentText(message.fromHtml())
.setGroupSummary(false)
.setGroup(host)
.setDeleteIntent(deleteIntent)
.setContentIntent(contentIntent)
.setMessageNotification()
//TODO: Get Site_Name PublicSetting from cache
val subText = "Rocket.Chat"
if (subText.isNotEmpty()) {
builder.setSubText(subText)
} }
val pushMessageList = hostToPushMessageList.get(host)
pushMessageList?.let {
val lastPushMsg = pushMessageList.last()
if (lastPushMsg.host == null || lastPushMsg.message == null || lastPushMsg.title == null) {
return null
}
if (pushMessageList.isNotEmpty()) {
val messageCount = pushMessageList.size
val bigText = NotificationCompat.BigTextStyle()
.bigText(lastPushMsg.message.fromHtml())
.setBigContentTitle(lastPushMsg.title.fromHtml())
builder.setStyle(bigText).setNumber(messageCount)
} }
} else {
val bigTextStyle = NotificationCompat.BigTextStyle()
.bigText(message.fromHtml())
builder.setStyle(bigTextStyle)
} }
return builder.build() return builder.build()
} }
} }
@SuppressLint("NewApi") @RequiresApi(Build.VERSION_CODES.O)
@RequiresApi(Build.VERSION_CODES.N) private fun createBaseNotificationBuilder(pushMessage: PushMessage, grouped: Boolean = false): NotificationCompat.Builder {
internal fun createSingleNotificationForNougatAndAbove(context: Context, lastPushMessage: PushMessage): Notification? { return with(pushMessage) {
val manager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
with(lastPushMessage) {
if (host == null || message == null || title == null) {
return null
}
val id = notificationId.toInt() val id = notificationId.toInt()
val contentIntent = getContentIntent(context, id, lastPushMessage) val host = info.host
val deleteIntent = getDismissIntent(context, lastPushMessage) val contentIntent = getContentIntent(context, id, pushMessage, grouped)
val deleteIntent = getDismissIntent(context, pushMessage)
val builder = Notification.Builder(context) val builder = NotificationCompat.Builder(context, host)
.setWhen(createdAt) .setWhen(info.createdAt)
.setContentTitle(title.fromHtml()) .setContentTitle(title.fromHtml())
.setContentText(message.fromHtml()) .setContentText(message.fromHtml())
.setGroup(host) .setGroup(host)
.setGroupSummary(false)
.setDeleteIntent(deleteIntent) .setDeleteIntent(deleteIntent)
.setContentIntent(contentIntent) .setContentIntent(contentIntent)
.setMessageNotification(context) .setMessageNotification()
.addReplyAction(context, lastPushMessage)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(host)
val channel = NotificationChannel(host, host, NotificationManager.IMPORTANCE_HIGH) val channel = NotificationChannel(host, host, NotificationManager.IMPORTANCE_HIGH)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.enableLights(false) channel.enableLights(false)
...@@ -400,47 +240,18 @@ object PushManager { ...@@ -400,47 +240,18 @@ object PushManager {
} }
//TODO: Get Site_Name PublicSetting from cache //TODO: Get Site_Name PublicSetting from cache
val subText = "Rocket.Chat" val subText = getSiteName(host)
if (subText.isNotEmpty()) { if (subText.isNotEmpty()) {
builder.setSubText(subText) builder.setSubText(subText)
} }
if (style == null || "inbox" == style) { return@with builder
val pushMessageList = hostToPushMessageList.get(host)
pushMessageList?.let {
val userMessages = pushMessageList.filter {
it.notificationId == lastPushMessage.notificationId
}
val count = pushMessageList.filter {
it.title == title
}.size
builder.setContentTitle(getTitle(count, title))
if (count > 1) {
val inbox = Notification.InboxStyle()
inbox.setBigContentTitle(getTitle(count, title))
for (push in userMessages) {
inbox.addLine(push.message)
}
builder.setStyle(inbox)
} else {
val bigTextStyle = Notification.BigTextStyle()
.bigText(message.fromHtml())
builder.setStyle(bigTextStyle)
}
} }
} else {
val bigTextStyle = Notification.BigTextStyle()
.bigText(message.fromHtml())
builder.setStyle(bigTextStyle)
} }
return builder.build() private fun getSiteName(host: String): String {
} val settings = getSettingsInteractor.get(host)
return settings.siteName() ?: "Rocket.Chat"
} }
private fun getTitle(messageCount: Int, title: String): CharSequence { private fun getTitle(messageCount: Int, title: String): CharSequence {
...@@ -450,18 +261,16 @@ object PushManager { ...@@ -450,18 +261,16 @@ object PushManager {
private fun getDismissIntent(context: Context, pushMessage: PushMessage): PendingIntent { private fun getDismissIntent(context: Context, pushMessage: PushMessage): PendingIntent {
val deleteIntent = Intent(context, DeleteReceiver::class.java) val deleteIntent = Intent(context, DeleteReceiver::class.java)
.putExtra(EXTRA_NOT_ID, pushMessage.notificationId.toInt()) .putExtra(EXTRA_NOT_ID, pushMessage.notificationId.toInt())
.putExtra(EXTRA_HOSTNAME, pushMessage.host) .putExtra(EXTRA_HOSTNAME, pushMessage.info.host)
return PendingIntent.getBroadcast(context, pushMessage.notificationId.toInt(), deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getBroadcast(context, pushMessage.notificationId.toInt(), deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
private fun getContentIntent(context: Context, notificationId: Int, pushMessage: PushMessage, grouped: Boolean = false): PendingIntent { private fun getContentIntent(context: Context, notificationId: Int, pushMessage: PushMessage, grouped: Boolean = false): PendingIntent {
val notificationIntent = Intent(context, MainActivity::class.java) val notificationIntent = context.changeServerIntent(pushMessage.info.host)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) // TODO - add support to go directly to the chatroom
.putExtra(EXTRA_NOT_ID, notificationId) /*if (!grouped) {
.putExtra(EXTRA_HOSTNAME, pushMessage.host) notificationIntent.putExtra(EXTRA_ROOM_ID, pushMessage.info.roomId)
if (!grouped) { }*/
notificationIntent.putExtra(EXTRA_ROOM_ID, pushMessage.rid)
}
return PendingIntent.getActivity(context, randomizer.nextInt(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) return PendingIntent.getActivity(context, randomizer.nextInt(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
...@@ -472,18 +281,18 @@ object PushManager { ...@@ -472,18 +281,18 @@ object PushManager {
//Notification.Builder extensions //Notification.Builder extensions
@RequiresApi(Build.VERSION_CODES.N) @RequiresApi(Build.VERSION_CODES.N)
private fun Notification.Builder.addReplyAction(ctx: Context, pushMessage: PushMessage): Notification.Builder { private fun Notification.Builder.addReplyAction(pushMessage: PushMessage): Notification.Builder {
val replyRemoteInput = android.app.RemoteInput.Builder(REMOTE_INPUT_REPLY) val replyRemoteInput = android.app.RemoteInput.Builder(REMOTE_INPUT_REPLY)
.setLabel(REPLY_LABEL) .setLabel(REPLY_LABEL)
.build() .build()
//TODO: Implement this when we have sendMessage call //TODO: Implement this when we have sendMessage call
// val replyIntent = Intent(ctx, ReplyReceiver::class.java) // val replyIntent = Intent(context, ReplyReceiver::class.java)
// replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Serializable) // replyIntent.putExtra(EXTRA_PUSH_MESSAGE, pushMessage as Serializable)
// val pendingIntent = PendingIntent.getBroadcast( // val pendingIntent = PendingIntent.getBroadcast(
// ctx, randomizer.nextInt(), replyIntent, 0) // context, randomizer.nextInt(), replyIntent, 0)
// val replyAction = // val replyAction =
// Notification.Action.Builder( // Notification.Action.Builder(
// Icon.createWithResource(ctx, R.drawable.ic_reply), REPLY_LABEL, pendingIntent) // Icon.createWithResource(context, R.drawable.ic_reply), REPLY_LABEL, pendingIntent)
// .addRemoteInput(replyRemoteInput) // .addRemoteInput(replyRemoteInput)
// .setAllowGeneratedReplies(true) // .setAllowGeneratedReplies(true)
// .build() // .build()
...@@ -491,25 +300,8 @@ object PushManager { ...@@ -491,25 +300,8 @@ object PushManager {
return this return this
} }
@RequiresApi(Build.VERSION_CODES.N)
private fun Notification.Builder.setMessageNotification(ctx: Context): Notification.Builder {
val alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val res = ctx.resources
val smallIcon = res.getIdentifier(
"rocket_chat_notification", "drawable", ctx.packageName)
with(this, {
setAutoCancel(true)
setShowWhen(true)
setColor(res.getColor(R.color.colorPrimary, ctx.theme))
setSmallIcon(smallIcon)
setSound(alarmSound)
})
return this
}
// NotificationCompat.Builder extensions // NotificationCompat.Builder extensions
private fun NotificationCompat.Builder.addReplyAction(pushMessage: PushMessage): NotificationCompat.Builder { private fun NotificationCompat.Builder.addReplyAction(pushMessage: PushMessage): NotificationCompat.Builder {
val context = this.mContext
val replyRemoteInput = RemoteInput.Builder(REMOTE_INPUT_REPLY) val replyRemoteInput = RemoteInput.Builder(REMOTE_INPUT_REPLY)
.setLabel(REPLY_LABEL) .setLabel(REPLY_LABEL)
.build() .build()
...@@ -529,76 +321,66 @@ object PushManager { ...@@ -529,76 +321,66 @@ object PushManager {
private fun NotificationCompat.Builder.setMessageNotification(): NotificationCompat.Builder { private fun NotificationCompat.Builder.setMessageNotification(): NotificationCompat.Builder {
val alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val ctx = this.mContext val res = context.resources
val res = ctx.resources
val smallIcon = res.getIdentifier( val smallIcon = res.getIdentifier(
"rocket_chat_notification", "drawable", ctx.packageName) "rocket_chat_notification", "drawable", context.packageName)
with(this, { with(this, {
setAutoCancel(true) setAutoCancel(true)
setShowWhen(true) setShowWhen(true)
color = ctx.resources.getColor(R.color.colorPrimary) color = context.resources.getColor(R.color.colorPrimary)
setDefaults(Notification.DEFAULT_ALL) setDefaults(Notification.DEFAULT_ALL)
setSmallIcon(smallIcon) setSmallIcon(smallIcon)
setSound(alarmSound) setSound(alarmSound)
}) })
return this return this
} }
}
internal data class PushMessage( data class PushMessage(
val title: String? = null, val title: String,
val message: String? = null, val message: String,
val info: PushInfo,
val image: String? = null, val image: String? = null,
val ejson: String? = null,
val count: String? = null, val count: String? = null,
val notificationId: String, val notificationId: String,
val summaryText: String? = null, val summaryText: String? = null,
val style: String? = null) : Serializable { val style: String? = null
val host: String? )
val rid: String?
val type: String? @JsonSerializable
val channelName: String? data class PushInfo(
val sender: Sender? @Json(name = "host") val hostname: String,
@Json(name = "rid") val roomId: String,
val type: RoomType,
val name: String?,
val sender: PushSender?
) {
val createdAt: Long val createdAt: Long
get() = System.currentTimeMillis()
init { val host by lazy {
val json = if (ejson == null) JSONObject() else JSONObject(ejson) sanitizeUrl(hostname)
host = json.optString("host", null)
rid = json.optString("rid", null)
type = json.optString("type", null)
channelName = json.optString("name", null)
val senderJson = json.optString("sender", null)
if (senderJson != null && senderJson != "null") {
sender = Sender(senderJson)
} else {
sender = null
}
createdAt = System.currentTimeMillis()
} }
data class Sender(val sender: String) : Serializable { private fun sanitizeUrl(baseUrl: String): String {
val _id: String? var url = baseUrl.trim()
val username: String? while (url.endsWith('/')) {
val name: String? url = url.dropLast(1)
init {
val json = JSONObject(sender)
_id = json.optString("_id", null)
username = json.optString("username", null)
name = json.optString("name", null)
}
}
} }
/** return url
* BroadcastReceiver for dismissed notifications.
*/
class DeleteReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val notId = intent?.extras?.getInt(EXTRA_NOT_ID)
val host = intent?.extras?.getString(EXTRA_HOSTNAME)
if (host != null && notId != null) {
clearNotificationsByHostAndNotificationId(host, notId)
}
}
} }
} }
@JsonSerializable
data class PushSender(
@Json(name = "_id") val id: String,
val username: String?,
val name: String?
)
const val EXTRA_NOT_ID = "chat.rocket.android.EXTRA_NOT_ID"
const val EXTRA_HOSTNAME = "chat.rocket.android.EXTRA_HOSTNAME"
const val EXTRA_PUSH_MESSAGE = "chat.rocket.android.EXTRA_PUSH_MESSAGE"
const val EXTRA_ROOM_ID = "chat.rocket.android.EXTRA_ROOM_ID"
private const val REPLY_LABEL = "REPLY"
private const val REMOTE_INPUT_REPLY = "REMOTE_INPUT_REPLY"
package chat.rocket.android.push.di
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.push.DeleteReceiver
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class DeleteReceiverProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideDeleteReceiver(): DeleteReceiver
}
\ No newline at end of file
package chat.rocket.android.push.di
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.push.GcmListenerService
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class GcmListenerServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideGcmListenerService(): GcmListenerService
}
\ No newline at end of file
package chat.rocket.android.server.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerActivity
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
@Module
class ChangeServerModule {
@Provides
@PerActivity
fun provideChangeServerNavigator(activity: ChangeServerActivity) = ChangeServerNavigator(activity)
@Provides
@PerActivity
fun ChangeServerView(activity: ChangeServerActivity): ChangeServerView {
return activity
}
@Provides
fun provideLifecycleOwner(activity: ChangeServerActivity): LifecycleOwner = activity
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy = CancelStrategy(owner, jobs)
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.android.server.domain.model.Account
interface AccountsRepository {
suspend fun save(account: Account)
suspend fun load(): List<Account>
suspend fun remove(serverUrl: String)
}
\ No newline at end of file
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 ->
url == account.serverUrl
}
}
\ No newline at end of file
package chat.rocket.android.server.domain
import javax.inject.Inject
class GetAccountsInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun get() = repository.load()
}
\ No newline at end of file
package chat.rocket.android.server.domain;
import java.util.List;
import io.reactivex.Scheduler;
import io.reactivex.Single;
public class GetServersInteractor {
private final ServersRepository repository;
private final Scheduler executionScheduler;
public GetServersInteractor(ServersRepository repository, Scheduler executionScheduler) {
this.repository = repository;
this.executionScheduler = executionScheduler;
}
}
...@@ -17,11 +17,12 @@ class RefreshSettingsInteractor @Inject constructor(private val factory: RocketC ...@@ -17,11 +17,12 @@ class RefreshSettingsInteractor @Inject constructor(private val factory: RocketC
ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR, ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR,
ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, ACCOUNT_GITLAB, ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, ACCOUNT_GITLAB,
SITE_URL, SITE_NAME, FAVICON_512, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS, SITE_URL, SITE_NAME, FAVICON_512, FAVICON_196, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS,
FAVORITE_ROOMS, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE, UPLOAD_WHITELIST_MIMETYPES, FAVORITE_ROOMS, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE, UPLOAD_WHITELIST_MIMETYPES,
HIDE_USER_JOIN, HIDE_USER_LEAVE, HIDE_USER_JOIN, HIDE_USER_LEAVE,
HIDE_TYPE_AU, HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ALLOW_MESSAGE_DELETING, HIDE_TYPE_AU, HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ALLOW_MESSAGE_DELETING,
ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS) ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS,
WIDE_TILE_310)
suspend fun refresh(server: String) { suspend fun refresh(server: String) {
withContext(CommonPool) { withContext(CommonPool) {
......
package chat.rocket.android.server.domain
import javax.inject.Inject
class RemoveAccountInterector @Inject constructor(val repository: AccountsRepository) {
suspend fun remove(serverUrl: String) {
repository.remove(serverUrl)
}
}
\ No newline at end of file
package chat.rocket.android.server.domain
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)
}
\ No newline at end of file
package chat.rocket.android.server.domain;
public class SaveServerInteractor {
}
package chat.rocket.android.server.domain
import chat.rocket.android.server.domain.model.Server
import chat.rocket.android.server.infraestructure.ServerEntity
import io.reactivex.Completable
import io.reactivex.Single
interface ServersRepository {
val servers: Single<List<Server>>
fun saveServer(server: Server): Completable
fun updateServer(server: Server): Completable
}
\ No newline at end of file
...@@ -29,7 +29,9 @@ const val ACCOUNT_GITLAB = "Accounts_OAuth_Gitlab" ...@@ -29,7 +29,9 @@ const val ACCOUNT_GITLAB = "Accounts_OAuth_Gitlab"
const val SITE_URL = "Site_Url" const val SITE_URL = "Site_Url"
const val SITE_NAME = "Site_Name" const val SITE_NAME = "Site_Name"
const val FAVICON_196 = "Assets_favicon_192"
const val FAVICON_512 = "Assets_favicon_512" const val FAVICON_512 = "Assets_favicon_512"
const val WIDE_TILE_310 = "Assets_tile_310_wide"
const val USE_REALNAME = "UI_Use_Real_Name" const val USE_REALNAME = "UI_Use_Real_Name"
const val ALLOW_ROOM_NAME_SPECIAL_CHARS = "UI_Allow_room_names_with_special_chars" const val ALLOW_ROOM_NAME_SPECIAL_CHARS = "UI_Allow_room_names_with_special_chars"
const val FAVORITE_ROOMS = "Favorite_Rooms" const val FAVORITE_ROOMS = "Favorite_Rooms"
...@@ -69,6 +71,9 @@ fun PublicSettings.isGitlabAuthenticationEnabled(): Boolean = this[ACCOUNT_GITLA ...@@ -69,6 +71,9 @@ fun PublicSettings.isGitlabAuthenticationEnabled(): Boolean = this[ACCOUNT_GITLA
fun PublicSettings.isWordpressAuthenticationEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true fun PublicSettings.isWordpressAuthenticationEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true
fun PublicSettings.faviconLarge(): String? = this[FAVICON_512]?.value as String?
fun PublicSettings.favicon(): String? = this[FAVICON_196]?.value as String?
fun PublicSettings.wideTile(): String? = this[WIDE_TILE_310]?.value as String?
// Message settings // Message settings
fun PublicSettings.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true fun PublicSettings.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
...@@ -90,4 +95,5 @@ fun PublicSettings.uploadMaxFileSize(): Int { ...@@ -90,4 +95,5 @@ fun PublicSettings.uploadMaxFileSize(): Int {
return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE return this[UPLOAD_MAX_FILE_SIZE]?.value?.let { it as Int } ?: Int.MAX_VALUE
} }
fun PublicSettings.baseUrl(): String? = this[SITE_URL]?.value as String fun PublicSettings.baseUrl(): String? = this[SITE_URL]?.value as String?
\ No newline at end of file fun PublicSettings.siteName(): String? = this[SITE_NAME]?.value as String?
\ No newline at end of file
package chat.rocket.android.server.domain
interface TokenRepository : chat.rocket.core.TokenRepository {
fun remove(url: String)
fun clear()
}
\ No newline at end of file
package chat.rocket.android.server.domain.model
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class Account(
val serverUrl: String,
val serverLogo: String?,
val serverBg: String?,
val userName: String,
val avatar: String?
)
\ No newline at end of file
...@@ -19,4 +19,6 @@ class ConnectionManagerFactory @Inject constructor(private val factory: RocketCh ...@@ -19,4 +19,6 @@ class ConnectionManagerFactory @Inject constructor(private val factory: RocketCh
cache[url] = manager cache[url] = manager
return manager return manager
} }
fun get(url: String) = cache[url]
} }
\ No newline at end of file
...@@ -2,16 +2,16 @@ package chat.rocket.android.server.infraestructure ...@@ -2,16 +2,16 @@ package chat.rocket.android.server.infraestructure
import chat.rocket.common.util.PlatformLogger import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient import chat.rocket.core.RocketChatClient
import chat.rocket.core.TokenRepository import chat.rocket.android.server.domain.TokenRepository
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class RocketChatClientFactory @Inject constructor(val okHttpClient: OkHttpClient, class RocketChatClientFactory @Inject constructor(private val okHttpClient: OkHttpClient,
val repository: TokenRepository, private val repository: TokenRepository,
val logger: PlatformLogger) { private val logger: PlatformLogger) {
private val cache = HashMap<String, RocketChatClient>() private val cache = HashMap<String, RocketChatClient>()
fun create(url: String): RocketChatClient { fun create(url: String): RocketChatClient {
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.ServersRepository
import chat.rocket.android.server.domain.model.Server
import io.reactivex.Completable
import io.reactivex.Single
class RoomServersRepository : ServersRepository {
override val servers: Single<List<Server>>
get() = TODO("not implemented")
override fun saveServer(server: Server): Completable {
TODO("not implemented")
}
override fun updateServer(server: Server): Completable {
TODO("not implemented")
}
}
package chat.rocket.android.server.infraestructure
import android.content.SharedPreferences
import androidx.content.edit
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.model.Account
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext
class SharedPreferencesAccountsRepository(
private val preferences: SharedPreferences,
private val moshi: Moshi
) : AccountsRepository {
override suspend fun save(newAccount: Account) {
withContext(CommonPool) {
val accounts = load()
val newList = accounts.filter { account -> newAccount.serverUrl != account.serverUrl }
.toMutableList()
newList.add(0, newAccount)
save(newList)
}
}
override suspend fun load(): List<Account> = withContext(CommonPool) {
val json = preferences.getString(ACCOUNTS_KEY, "[]")
val type = Types.newParameterizedType(List::class.java, Account::class.java)
val adapter = moshi.adapter<List<Account>>(type)
adapter.fromJson(json) ?: emptyList()
}
override suspend fun remove(serverUrl: String) {
withContext(CommonPool) {
val accounts = load()
val newList = accounts.filter { account -> serverUrl != account.serverUrl }
.toMutableList()
save(newList)
}
}
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))
}
}
}
private const val ACCOUNTS_KEY = "ACCOUNTS_KEY"
\ No newline at end of file
...@@ -15,9 +15,7 @@ class SharedPreferencesSettingsRepository(private val localRepository: LocalRepo ...@@ -15,9 +15,7 @@ class SharedPreferencesSettingsRepository(private val localRepository: LocalRepo
} }
override fun get(url: String): PublicSettings { override fun get(url: String): PublicSettings {
val settings = localRepository.get("$SETTINGS_KEY$url")!! val settings = localRepository.get("$SETTINGS_KEY$url")
settings.let { return if (settings == null) hashMapOf() else adapter.fromJson(settings) ?: hashMapOf()
return adapter.fromJson(it)!!
}
} }
} }
\ No newline at end of file
package chat.rocket.android.server.presentation
import android.content.Intent
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.ui.ChangeServerActivity
class ChangeServerNavigator (internal val activity: ChangeServerActivity) {
fun toServerScreen() {
activity.startActivity(activity.newServerIntent())
activity.finish()
}
fun toChatRooms() {
activity.startActivity(Intent(activity, MainActivity::class.java))
activity.finish()
}
}
\ No newline at end of file
package chat.rocket.android.server.presentation
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.util.ifNull
import javax.inject.Inject
class ChangeServerPresenter @Inject constructor(
private val view: ChangeServerView,
private val navigator: ChangeServerNavigator,
private val strategy: CancelStrategy,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
private val getAccountInteractor: GetAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val settingsRepository: SettingsRepository,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val connectionManager: ConnectionManagerFactory
) {
fun loadServer(newUrl: String?) {
launchUI(strategy) {
view.showProgress()
var url = newUrl
if (url == null) { // Try to load next server on the list...
val accounts = getAccountsInteractor.get()
url = accounts.firstOrNull()?.serverUrl
}
url?.let { serverUrl ->
val token = tokenRepository.get(serverUrl)
if (token == null) {
view.showInvalidCredentials()
view.hideProgress()
navigator.toServerScreen()
return@launchUI
}
val settings = settingsRepository.get(serverUrl)
if (settings == null) {
// TODO - reload settings...
}
// Call disconnect on the old url if any...
getCurrentServerInteractor.get()?.let { url ->
connectionManager.get(url)?.disconnect()
}
// Save the current username.
getAccountInteractor.get(serverUrl)?.let { account ->
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, account.userName)
}
saveCurrentServerInteractor.save(serverUrl)
view.hideProgress()
navigator.toChatRooms()
}.ifNull {
view.hideProgress()
navigator.toServerScreen()
}
}
}
}
\ No newline at end of file
package chat.rocket.android.server.presentation
interface ChangeServerView {
fun showInvalidCredentials()
fun showProgress()
fun hideProgress()
}
\ No newline at end of file
package chat.rocket.android.server.presentation
import chat.rocket.android.BuildConfig
import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.util.VersionInfo
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.serverInfo
import timber.log.Timber
abstract class CheckServerPresenter constructor(private val strategy: CancelStrategy,
private val client: RocketChatClient,
private val view: VersionCheckView) {
internal fun checkServerInfo() {
launchUI(strategy) {
val serverInfo = client.serverInfo()
val thisServerVersion = serverInfo.version
val isRequiredVersion = isRequiredServerVersion(thisServerVersion)
val isRecommendedVersion = isRecommendedServerVersion(thisServerVersion)
if (isRequiredVersion) {
if (isRecommendedVersion) {
Timber.i("Your version is nice! (Requires: 0.62.0, Yours: $thisServerVersion)")
} else {
view.alertNotRecommendedVersion()
}
} else {
if (!isRecommendedVersion) {
view.blockAndAlertNotRequiredVersion()
Timber.i("Oops. Looks like your server is out-of-date! Minimum server version required ${BuildConfig.REQUIRED_SERVER_VERSION}!")
}
}
}
}
private fun isRequiredServerVersion(version: String): Boolean {
return isMinimumVersion(version, getVersionDistilled(BuildConfig.REQUIRED_SERVER_VERSION))
}
private fun isRecommendedServerVersion(version: String): Boolean {
return isMinimumVersion(version, getVersionDistilled(BuildConfig.RECOMMENDED_SERVER_VERSION))
}
private fun isMinimumVersion(version: String, required: VersionInfo): Boolean {
val thisVersion = getVersionDistilled(version)
with(thisVersion) {
if (major < required.major) {
return false
} else if (major > required.major) {
return true
}
if (minor < required.minor) {
return false
} else if (minor > required.minor) {
return true
}
return update >= required.update
}
}
private fun getVersionDistilled(version: String): VersionInfo {
var split = version.split("-")
if (split.isEmpty()) {
return VersionInfo(0, 0, 0, null, "0.0.0")
}
val ver = split[0]
var release: String? = null
if (split.size > 1) {
release = split[1]
}
split = ver.split(".")
val major = getVersionNumber(split, 0)
val minor = getVersionNumber(split, 1)
val update = getVersionNumber(split, 2)
return VersionInfo(
major = major,
minor = minor,
update = update,
release = release,
full = version)
}
private fun getVersionNumber(split: List<String>, index: Int): Int {
return try {
split.getOrNull(index)?.toInt() ?: 0
} catch (ex: NumberFormatException) {
0
}
}
}
\ No newline at end of file
package chat.rocket.android.server.ui
import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.server.presentation.ChangeServerPresenter
import chat.rocket.android.server.presentation.ChangeServerView
import chat.rocket.android.util.extensions.showToast
import dagger.android.AndroidInjection
import javax.inject.Inject
class ChangeServerActivity : AppCompatActivity(), ChangeServerView {
@Inject lateinit var presenter: ChangeServerPresenter
var progress: ProgressDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
val serverUrl: String? = intent.getStringExtra(INTENT_SERVER_URL)
presenter.loadServer(serverUrl)
}
override fun showInvalidCredentials() {
showToast("Missing credentials for this server")
}
override fun showProgress() {
progress = ProgressDialog.show(this, "Rocket.Chat", "Changing Server")
}
override fun hideProgress() {
progress?.dismiss()
}
}
private const val INTENT_SERVER_URL = "INTENT_SERVER_URL"
private const val INTENT_CHAT_ROOM_NAME = "INTENT_CHAT_ROOM_NAME"
private const val INTENT_CHAT_ROOM_TYPE = "INTENT_CHAT_ROOM_TYPE"
fun Context.changeServerIntent(serverUrl: String?): Intent {
return Intent(this, ChangeServerActivity::class.java).apply {
serverUrl?.let { url ->
putExtra(INTENT_SERVER_URL, url)
}
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
\ No newline at end of file
package chat.rocket.android.util
data class VersionInfo(
val major: Int,
val minor: Int,
val update: Int = 0,
val release: String?,
val full: String
)
\ No newline at end of file
...@@ -29,12 +29,12 @@ fun View.fadeIn(startValue: Float = 0f, finishValue: Float = 1f, duration: Long ...@@ -29,12 +29,12 @@ fun View.fadeIn(startValue: Float = 0f, finishValue: Float = 1f, duration: Long
animate() animate()
.alpha(startValue) .alpha(startValue)
.setDuration(duration) .setDuration(duration / 2)
.setInterpolator(DecelerateInterpolator()) .setInterpolator(DecelerateInterpolator())
.withEndAction({ .withEndAction({
animate() animate()
.alpha(finishValue) .alpha(finishValue)
.setDuration(duration) .setDuration(duration / 2)
.setInterpolator(AccelerateInterpolator()).start() .setInterpolator(AccelerateInterpolator()).start()
}).start() }).start()
......
package chat.rocket.android.util.extensions
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.registerPushToken
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
suspend fun RocketChatClient.registerPushToken(
token: String,
accounts: List<Account>,
factory: RocketChatClientFactory
) {
launch(CommonPool) {
accounts.forEach { account ->
try {
factory.create(account.serverUrl).registerPushToken(token)
} catch (ex: Exception) {
Timber.d(ex, "Error registering Push token for ${account.serverUrl}")
ex.printStackTrace()
}
}
}
}
\ No newline at end of file
...@@ -3,12 +3,15 @@ package chat.rocket.android.util.extensions ...@@ -3,12 +3,15 @@ package chat.rocket.android.util.extensions
import android.text.Spannable import android.text.Spannable
import android.text.Spanned import android.text.Spanned
import android.text.TextUtils import android.text.TextUtils
import android.util.Base64
import android.util.Patterns import android.util.Patterns
import android.widget.EditText import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import chat.rocket.android.widget.emoji.EmojiParser import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import org.json.JSONObject
import ru.noties.markwon.Markwon import ru.noties.markwon.Markwon
import java.net.URLDecoder
import java.security.SecureRandom import java.security.SecureRandom
fun String.ifEmpty(value: String): String { fun String.ifEmpty(value: String): String {
...@@ -33,7 +36,23 @@ fun EditText.erase() { ...@@ -33,7 +36,23 @@ fun EditText.erase() {
} }
} }
fun String.isEmailValid(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches() fun String.isEmail(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun String.encodeToBase64(): String {
return Base64.encodeToString(this.toByteArray(charset("UTF-8")), Base64.NO_WRAP)
}
fun String.decodeFromBase64(): String {
return Base64.decode(this, Base64.DEFAULT).toString(charset("UTF-8"))
}
fun String.decodeUrl(): String {
return URLDecoder.decode(this, "UTF-8")
}
fun String.toJsonObject(): JSONObject {
return JSONObject(this)
}
fun generateRandomString(stringLength: Int): String { fun generateRandomString(stringLength: Int): String {
val base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" val base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
......
...@@ -27,7 +27,8 @@ fun View.isVisible(): Boolean { ...@@ -27,7 +27,8 @@ fun View.isVisible(): Boolean {
return visibility == View.VISIBLE return visibility == View.VISIBLE
} }
fun ViewGroup.inflate(@LayoutRes resource: Int): View = LayoutInflater.from(context).inflate(resource, this, false) fun ViewGroup.inflate(@LayoutRes resource: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(resource, this, attachToRoot)
fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) { fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance() val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
...@@ -36,10 +37,12 @@ fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> ...@@ -36,10 +37,12 @@ fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () ->
.commit() .commit()
} }
fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int, newInstance: () -> Fragment) { fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int,
newInstance: () -> Fragment) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance() val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right) .setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left,
R.anim.enter_from_left, R.anim.exit_to_right)
.replace(layoutId, fragment, tag) .replace(layoutId, fragment, tag)
.addToBackStack(tag) .addToBackStack(tag)
.commit() .commit()
...@@ -52,13 +55,17 @@ fun Activity.hideKeyboard() { ...@@ -52,13 +55,17 @@ fun Activity.hideKeyboard() {
} }
} }
fun Activity.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration) fun Activity.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) =
showToast(getString(resource), duration)
fun Activity.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = Toast.makeText(this, message, duration).show() fun Activity.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) =
Toast.makeText(this, message, duration).show()
fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) = showToast(getString(resource), duration) fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) =
showToast(getString(resource), duration)
fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = activity!!.showToast(message, duration) fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) =
activity?.showToast(message, duration)
fun RecyclerView.isAtBottom(): Boolean { fun RecyclerView.isAtBottom(): Boolean {
val manager: RecyclerView.LayoutManager? = layoutManager val manager: RecyclerView.LayoutManager? = layoutManager
......
...@@ -13,7 +13,7 @@ import chat.rocket.android.R ...@@ -13,7 +13,7 @@ import chat.rocket.android.R
import kotlinx.android.synthetic.main.activity_web_view.* import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.* import kotlinx.android.synthetic.main.app_bar.*
fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent { fun Context.casWebViewIntent(webPageUrl: String, casToken: String): Intent {
return Intent(this, CasWebViewActivity::class.java).apply { return Intent(this, CasWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl) putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_CAS_TOKEN, casToken) putExtra(INTENT_CAS_TOKEN, casToken)
...@@ -21,7 +21,7 @@ fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent { ...@@ -21,7 +21,7 @@ fun Context.webViewIntent(webPageUrl: String, casToken: String): Intent {
} }
private const val INTENT_WEB_PAGE_URL = "web_page_url" private const val INTENT_WEB_PAGE_URL = "web_page_url"
private const val INTENT_CAS_TOKEN = "cas_token" const val INTENT_CAS_TOKEN = "cas_token"
class CasWebViewActivity : AppCompatActivity() { class CasWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String private lateinit var webPageUrl: String
...@@ -49,14 +49,14 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -49,14 +49,14 @@ class CasWebViewActivity : AppCompatActivity() {
if (web_view.canGoBack()) { if (web_view.canGoBack()) {
web_view.goBack() web_view.goBack()
} else { } else {
finishActivity(false) closeView()
} }
} }
private fun setupToolbar() { private fun setupToolbar() {
toolbar.title = getString(R.string.title_authentication) toolbar.title = getString(R.string.title_authentication)
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp) toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
toolbar.setNavigationOnClickListener { finishActivity(false) } toolbar.setNavigationOnClickListener { closeView() }
} }
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
...@@ -64,16 +64,16 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -64,16 +64,16 @@ class CasWebViewActivity : AppCompatActivity() {
web_view.settings.javaScriptEnabled = true web_view.settings.javaScriptEnabled = true
web_view.webViewClient = object : WebViewClient() { web_view.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
// The user can be already logged in the CAS, so check if the URL contains the "ticket" word // The user may have already been logged in the CAS, so check if the URL contains the "ticket" word
// (that means he/she is successful authenticated and we don't need to wait until the page is finished. // (that means the user is successful authenticated and we don't need to wait until the page is fully loaded).
if (url.contains("ticket")) { if (url.contains("ticket")) {
finishActivity(true) closeView(Activity.RESULT_OK)
} }
} }
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
if (url.contains("ticket")) { if (url.contains("ticket")) {
finishActivity(true) closeView(Activity.RESULT_OK)
} else { } else {
view_loading.hide() view_loading.hide()
} }
...@@ -82,13 +82,9 @@ class CasWebViewActivity : AppCompatActivity() { ...@@ -82,13 +82,9 @@ class CasWebViewActivity : AppCompatActivity() {
web_view.loadUrl(webPageUrl) web_view.loadUrl(webPageUrl)
} }
private fun finishActivity(setResultOk: Boolean) { private fun closeView(activityResult: Int = Activity.RESULT_CANCELED) {
if (setResultOk) { setResult(activityResult, Intent().putExtra(INTENT_CAS_TOKEN, casToken))
setResult(Activity.RESULT_OK, Intent().putExtra(INTENT_CAS_TOKEN, casToken))
finish() finish()
} else {
super.onBackPressed()
}
overridePendingTransition(R.anim.hold, R.anim.slide_down) overridePendingTransition(R.anim.hold, R.anim.slide_down)
} }
} }
\ No newline at end of file
package chat.rocket.android.webview.oauth.ui
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.net.toUri
import chat.rocket.android.R
import chat.rocket.android.util.extensions.decodeUrl
import chat.rocket.android.util.extensions.toJsonObject
import kotlinx.android.synthetic.main.activity_web_view.*
import kotlinx.android.synthetic.main.app_bar.*
import org.json.JSONObject
fun Context.oauthWebViewIntent(webPageUrl: String, state: String): Intent {
return Intent(this, OauthWebViewActivity::class.java).apply {
putExtra(INTENT_WEB_PAGE_URL, webPageUrl)
putExtra(INTENT_STATE, state)
}
}
private const val INTENT_WEB_PAGE_URL = "web_page_url"
private const val INTENT_STATE = "state"
private const val JSON_CREDENTIAL_TOKEN = "credentialToken"
private const val JSON_CREDENTIAL_SECRET = "credentialSecret"
const val INTENT_OAUTH_CREDENTIAL_TOKEN = "credential_token"
const val INTENT_OAUTH_CREDENTIAL_SECRET = "credential_secret"
// Shows a WebView to the user authenticate with your Gitlab credentials.
class OauthWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
private lateinit var state: String
private var isWebViewSetUp: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web_view)
webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL)
requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" }
state = intent.getStringExtra(INTENT_STATE)
requireNotNull(state) { "no state provided in Intent extras" }
setupToolbar()
}
override fun onResume() {
super.onResume()
if (!isWebViewSetUp) {
setupWebView()
isWebViewSetUp = true
}
}
override fun onBackPressed() {
if (web_view.canGoBack()) {
web_view.goBack()
} else {
closeView()
}
}
private fun setupToolbar() {
with(toolbar) {
title = getString(R.string.title_authentication)
setNavigationIcon(R.drawable.ic_close_white_24dp)
setNavigationOnClickListener { closeView() }
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
with(web_view.settings) {
javaScriptEnabled = true
// TODO Remove this workaround that is required to make Google OAuth to work. We should use Custom Tabs instead. See https://github.com/RocketChat/Rocket.Chat.Android/issues/968
if (webPageUrl.contains("google")) {
userAgentString = "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/43.0.2357.65 Mobile Safari/535.19"
}
}
web_view.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
if (url.contains(JSON_CREDENTIAL_TOKEN) && url.contains(JSON_CREDENTIAL_SECRET)) {
if (isStateValid(url)) {
val jsonResult = url.decodeUrl()
.substringAfter("#")
.toJsonObject()
val credentialToken = getCredentialToken(jsonResult)
val credentialSecret = getCredentialSecret(jsonResult)
if (credentialToken.isNotEmpty() && credentialSecret.isNotEmpty()) {
closeView(Activity.RESULT_OK, credentialToken, credentialSecret)
}
}
}
view_loading.hide()
}
}
web_view.loadUrl(webPageUrl)
}
// If the states matches, then try to get the code, otherwise the request was created by a third party and the process should be aborted.
private fun isStateValid(url: String): Boolean =
url.substringBefore("#").toUri().getQueryParameter(INTENT_STATE) == state
private fun getCredentialToken(json: JSONObject): String =
json.optString(JSON_CREDENTIAL_TOKEN)
private fun getCredentialSecret(json: JSONObject): String =
json.optString(JSON_CREDENTIAL_SECRET)
private fun closeView(activityResult: Int = Activity.RESULT_CANCELED, credentialToken: String? = null, credentialSecret: String? = null) {
setResult(activityResult, Intent().putExtra(INTENT_OAUTH_CREDENTIAL_TOKEN, credentialToken).putExtra(INTENT_OAUTH_CREDENTIAL_SECRET, credentialSecret))
finish()
overridePendingTransition(R.anim.hold, R.anim.slide_down)
}
}
\ No newline at end of file
...@@ -6,5 +6,6 @@ interface CompletionStrategy { ...@@ -6,5 +6,6 @@ interface CompletionStrategy {
fun getItem(prefix: String, position: Int): SuggestionModel fun getItem(prefix: String, position: Int): SuggestionModel
fun autocompleteItems(prefix: String): List<SuggestionModel> fun autocompleteItems(prefix: String): List<SuggestionModel>
fun addAll(list: List<SuggestionModel>) fun addAll(list: List<SuggestionModel>)
fun addPinned(list: List<SuggestionModel>)
fun size(): Int fun size(): Int
} }
\ No newline at end of file
...@@ -2,14 +2,19 @@ package chat.rocket.android.widget.autocompletion.strategy.regex ...@@ -2,14 +2,19 @@ package chat.rocket.android.widget.autocompletion.strategy.regex
import chat.rocket.android.widget.autocompletion.model.SuggestionModel import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy import chat.rocket.android.widget.autocompletion.strategy.CompletionStrategy
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter.Companion.RESULT_COUNT_UNLIMITED
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
internal class StringMatchingCompletionStrategy(private val threshold: Int = -1) : CompletionStrategy { internal class StringMatchingCompletionStrategy(private val threshold: Int = RESULT_COUNT_UNLIMITED) : CompletionStrategy {
private val list = CopyOnWriteArrayList<SuggestionModel>() private val list = CopyOnWriteArrayList<SuggestionModel>()
private val pinnedList = mutableListOf<SuggestionModel>()
init {
check(threshold >= RESULT_COUNT_UNLIMITED)
}
override fun autocompleteItems(prefix: String): List<SuggestionModel> { override fun autocompleteItems(prefix: String): List<SuggestionModel> {
val result = list.filter { val partialResult = list.filter {
it.searchList.forEach { word -> it.searchList.forEach { word ->
if (word.contains(prefix, ignoreCase = true)) { if (word.contains(prefix, ignoreCase = true)) {
return@filter true return@filter true
...@@ -17,13 +22,23 @@ internal class StringMatchingCompletionStrategy(private val threshold: Int = -1) ...@@ -17,13 +22,23 @@ internal class StringMatchingCompletionStrategy(private val threshold: Int = -1)
} }
false false
}.sortedByDescending { it.pinned } }.sortedByDescending { it.pinned }
return if (threshold == SuggestionsAdapter.UNLIMITED_RESULT_COUNT) result else result.take(threshold) return if (threshold == RESULT_COUNT_UNLIMITED)
partialResult.toList()
else {
val result = partialResult.take(threshold).toMutableList()
result.addAll(pinnedList)
result.toList()
}
} }
override fun addAll(list: List<SuggestionModel>) { override fun addAll(list: List<SuggestionModel>) {
this.list.addAllAbsent(list) this.list.addAllAbsent(list)
} }
override fun addPinned(list: List<SuggestionModel>) {
this.pinnedList.addAll(list)
}
override fun getItem(prefix: String, position: Int): SuggestionModel { override fun getItem(prefix: String, position: Int): SuggestionModel {
return list[position] return list[position]
} }
......
...@@ -27,5 +27,9 @@ class TrieCompletionStrategy : CompletionStrategy { ...@@ -27,5 +27,9 @@ class TrieCompletionStrategy : CompletionStrategy {
} }
} }
override fun addPinned(list: List<SuggestionModel>) {
}
override fun size() = items.size override fun size() = items.size
} }
\ No newline at end of file
...@@ -13,7 +13,7 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>( ...@@ -13,7 +13,7 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
threshold: Int = MAX_RESULT_COUNT) : RecyclerView.Adapter<VH>() { threshold: Int = MAX_RESULT_COUNT) : RecyclerView.Adapter<VH>() {
companion object { companion object {
// Any number of results. // Any number of results.
const val UNLIMITED_RESULT_COUNT = -1 const val RESULT_COUNT_UNLIMITED = -1
// Trigger suggestions only if on the line start. // Trigger suggestions only if on the line start.
const val CONSTRAINT_BOUND_TO_START = 0 const val CONSTRAINT_BOUND_TO_START = 0
// Trigger suggestions from anywhere. // Trigger suggestions from anywhere.
...@@ -21,12 +21,14 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>( ...@@ -21,12 +21,14 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
// Maximum number of results to display by default. // Maximum number of results to display by default.
private const val MAX_RESULT_COUNT = 5 private const val MAX_RESULT_COUNT = 5
} }
private var itemType: Type? = null private var itemType: Type? = null
private var itemClickListener: ItemClickListener? = null private var itemClickListener: ItemClickListener? = null
// Called to gather results when no results have previously matched. // Called to gather results when no results have previously matched.
private var providerExternal: ((query: String) -> Unit)? = null private var providerExternal: ((query: String) -> Unit)? = null
private var pinnedSuggestions: List<SuggestionModel>? = null
// Maximum number of results/suggestions to display. // Maximum number of results/suggestions to display.
private var resultsThreshold: Int = if (threshold > 0) threshold else UNLIMITED_RESULT_COUNT private var resultsThreshold: Int = if (threshold > 0) threshold else RESULT_COUNT_UNLIMITED
// The strategy used for suggesting completions. // The strategy used for suggesting completions.
private val strategy: CompletionStrategy = StringMatchingCompletionStrategy(resultsThreshold) private val strategy: CompletionStrategy = StringMatchingCompletionStrategy(resultsThreshold)
// Current input term to look up for suggestions. // Current input term to look up for suggestions.
...@@ -53,6 +55,15 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>( ...@@ -53,6 +55,15 @@ abstract class SuggestionsAdapter<VH : BaseSuggestionViewHolder>(
return strategy.autocompleteItems(currentTerm)[position] return strategy.autocompleteItems(currentTerm)[position]
} }
/**
* Set suggestions that should always appear when prompted.
*
* @param suggestions The list of suggestions that will be pinned.
*/
fun setPinnedSuggestions(suggestions: List<SuggestionModel>) {
this.strategy.addPinned(suggestions)
}
fun autocomplete(newTerm: String) { fun autocomplete(newTerm: String) {
this.currentTerm = newTerm.toLowerCase().trim() this.currentTerm = newTerm.toLowerCase().trim()
} }
......
...@@ -23,23 +23,19 @@ import chat.rocket.android.R ...@@ -23,23 +23,19 @@ import chat.rocket.android.R
import chat.rocket.android.widget.autocompletion.model.SuggestionModel import chat.rocket.android.widget.autocompletion.model.SuggestionModel
import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter.Companion.CONSTRAINT_BOUND_TO_START import chat.rocket.android.widget.autocompletion.ui.SuggestionsAdapter.Companion.CONSTRAINT_BOUND_TO_START
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
/** // This is a special index that means we're not at an autocompleting state.
* This is a special index that means we're not at an autocompleting state.
*/
private const val NO_STATE_INDEX = 0 private const val NO_STATE_INDEX = 0
class SuggestionsView : FrameLayout, TextWatcher { class SuggestionsView : FrameLayout, TextWatcher {
private val recyclerView: RecyclerView private val recyclerView: RecyclerView
private val registeredTokens = CopyOnWriteArrayList<String>()
// Maps tokens to their respective adapters. // Maps tokens to their respective adapters.
private val adaptersByToken = hashMapOf<String, SuggestionsAdapter<out BaseSuggestionViewHolder>>() private val adaptersByToken = hashMapOf<String, SuggestionsAdapter<out BaseSuggestionViewHolder>>()
private val externalProvidersByToken = hashMapOf<String, ((query: String) -> Unit)>() private val externalProvidersByToken = hashMapOf<String, ((query: String) -> Unit)>()
private val localProvidersByToken = hashMapOf<String, HashMap<String, List<SuggestionModel>>>() private val localProvidersByToken = hashMapOf<String, HashMap<String, List<SuggestionModel>>>()
private var editor: WeakReference<EditText>? = null private var editor: WeakReference<EditText>? = null
private var completionStartIndex = AtomicInteger(NO_STATE_INDEX) private var completionOffset = AtomicInteger(NO_STATE_INDEX)
private var maxHeight: Int = 0 private var maxHeight: Int = 0
companion object { companion object {
...@@ -66,7 +62,7 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -66,7 +62,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
// If we have a deletion. // If we have a deletion.
if (after == 0) { if (after == 0) {
val deleted = s.subSequence(start, start + count).toString() val deleted = s.subSequence(start, start + count).toString()
if (adaptersByToken.containsKey(deleted) && completionStartIndex.get() > NO_STATE_INDEX) { if (adaptersByToken.containsKey(deleted) && completionOffset.get() > NO_STATE_INDEX) {
// We have removed the '@', '#' or any other action token so halt completion. // We have removed the '@', '#' or any other action token so halt completion.
cancelSuggestions(true) cancelSuggestions(true)
} }
...@@ -77,6 +73,11 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -77,6 +73,11 @@ class SuggestionsView : FrameLayout, TextWatcher {
// If we don't have any adapter bound to any token bail out. // If we don't have any adapter bound to any token bail out.
if (adaptersByToken.isEmpty()) return if (adaptersByToken.isEmpty()) return
if (editor?.get() != null && editor?.get()?.selectionStart ?: 0 <= completionOffset.get()) {
completionOffset.set(NO_STATE_INDEX)
collapse()
}
val new = s.subSequence(start, start + count).toString() val new = s.subSequence(start, start + count).toString()
if (adaptersByToken.containsKey(new)) { if (adaptersByToken.containsKey(new)) {
val constraint = adapter(new).constraint val constraint = adapter(new).constraint
...@@ -84,8 +85,8 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -84,8 +85,8 @@ class SuggestionsView : FrameLayout, TextWatcher {
return return
} }
swapAdapter(getAdapterForToken(new)!!) swapAdapter(getAdapterForToken(new)!!)
completionStartIndex.compareAndSet(NO_STATE_INDEX, start + 1) completionOffset.compareAndSet(NO_STATE_INDEX, start + 1)
editor?.let { this.editor?.let {
// Disable keyboard suggestions when autocompleting. // Disable keyboard suggestions when autocompleting.
val editText = it.get() val editText = it.get()
if (editText != null) { if (editText != null) {
...@@ -97,13 +98,13 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -97,13 +98,13 @@ class SuggestionsView : FrameLayout, TextWatcher {
if (new.startsWith(" ")) { if (new.startsWith(" ")) {
// just halts the completion execution // just halts the completion execution
cancelSuggestions(false) cancelSuggestions(true)
return return
} }
val prefixEndIndex = editor?.get()?.selectionStart ?: NO_STATE_INDEX val prefixEndIndex = this.editor?.get()?.selectionStart ?: NO_STATE_INDEX
if (prefixEndIndex == NO_STATE_INDEX || prefixEndIndex < completionStartIndex.get()) return if (prefixEndIndex == NO_STATE_INDEX || prefixEndIndex < completionOffset.get()) return
val prefix = s.subSequence(completionStartIndex.get(), editor?.get()?.selectionStart ?: completionStartIndex.get()).toString() val prefix = s.subSequence(completionOffset.get(), this.editor?.get()?.selectionStart ?: completionOffset.get()).toString()
recyclerView.adapter?.let { recyclerView.adapter?.let {
it as SuggestionsAdapter it as SuggestionsAdapter
// we need to look up only after the '@' // we need to look up only after the '@'
...@@ -157,7 +158,7 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -157,7 +158,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
val adapter = adapter(token) val adapter = adapter(token)
localProvidersByToken.getOrPut(token, { hashMapOf() }) localProvidersByToken.getOrPut(token, { hashMapOf() })
.put(adapter.term(), list) .put(adapter.term(), list)
if (completionStartIndex.get() > NO_STATE_INDEX && adapter.itemCount == 0) expand() if (completionOffset.get() > NO_STATE_INDEX && adapter.itemCount == 0) expand()
adapter.addItems(list) adapter.addItems(list)
} }
return this return this
...@@ -199,7 +200,7 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -199,7 +200,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
// Reset completion start index only if we've deleted the token that triggered completion or // Reset completion start index only if we've deleted the token that triggered completion or
// we finished the completion process. // we finished the completion process.
if (haltCompletion) { if (haltCompletion) {
completionStartIndex.set(NO_STATE_INDEX) completionOffset.set(NO_STATE_INDEX)
} }
collapse() collapse()
// Re-enable keyboard suggestions. // Re-enable keyboard suggestions.
...@@ -212,7 +213,7 @@ class SuggestionsView : FrameLayout, TextWatcher { ...@@ -212,7 +213,7 @@ class SuggestionsView : FrameLayout, TextWatcher {
private fun insertSuggestionOnEditor(item: SuggestionModel) { private fun insertSuggestionOnEditor(item: SuggestionModel) {
editor?.get()?.let { editor?.get()?.let {
val suggestionText = item.text val suggestionText = item.text
it.text.replace(completionStartIndex.get(), it.selectionStart, "$suggestionText ") it.text.replace(completionOffset.get(), it.selectionStart, "$suggestionText ")
} }
} }
......
...@@ -14,15 +14,21 @@ class EmojiParser { ...@@ -14,15 +14,21 @@ class EmojiParser {
*/ */
fun parse(text: CharSequence): CharSequence { fun parse(text: CharSequence): CharSequence {
val unicodedText = EmojiRepository.shortnameToUnicode(text, true) val unicodedText = EmojiRepository.shortnameToUnicode(text, true)
val spannableString = SpannableString.valueOf(unicodedText) var spannable = SpannableString.valueOf(unicodedText)
// Look for groups of emojis, set a CustomTypefaceSpan with the emojione font val typeface = EmojiRepository.cachedTypeface
val length = spannableString.length // Look for groups of emojis, set a EmojiTypefaceSpan with the emojione font.
val length = spannable.length
var inEmoji = false var inEmoji = false
var emojiStart = 0 var emojiStart = 0
var offset = 0 var offset = 0
while (offset < length) { while (offset < length) {
val codepoint = unicodedText.codePointAt(offset) val codepoint = unicodedText.codePointAt(offset)
val count = Character.charCount(codepoint) val count = Character.charCount(codepoint)
// Skip control characters.
if (codepoint == 0x2028) {
offset += count
continue
}
if (codepoint >= 0x200) { if (codepoint >= 0x200) {
if (!inEmoji) { if (!inEmoji) {
emojiStart = offset emojiStart = offset
...@@ -30,18 +36,25 @@ class EmojiParser { ...@@ -30,18 +36,25 @@ class EmojiParser {
inEmoji = true inEmoji = true
} else { } else {
if (inEmoji) { if (inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface), spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
inEmoji = false inEmoji = false
} }
offset += count offset += count
if (offset >= length && inEmoji) { if (offset >= length && inEmoji) {
spannableString.setSpan(EmojiTypefaceSpan("sans-serif", EmojiRepository.cachedTypeface), spannable.setSpan(EmojiTypefaceSpan("sans-serif", typeface),
emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) emojiStart, offset, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
} }
} }
return spannableString return spannable
}
private fun calculateSurrogatePairs(scalar: Int): Pair<Int, Int> {
val temp: Int = (scalar - 0x10000) / 0x400
val s1: Int = Math.floor(temp.toDouble()).toInt() + 0xD800
val s2: Int = ((scalar - 0x10000) % 0x400) + 0xDC00
return Pair(s1, s2)
} }
} }
} }
\ No newline at end of file
...@@ -54,6 +54,10 @@ object EmojiRepository { ...@@ -54,6 +54,10 @@ object EmojiRepository {
*/ */
fun getAll() = ALL_EMOJIS fun getAll() = ALL_EMOJIS
// fun findEmojiByUnicode(unicode: Int) {
// ALL_EMOJIS.find { it.unicode == }
// }
/** /**
* Get all emojis for a given category. * Get all emojis for a given category.
* *
...@@ -119,10 +123,7 @@ object EmojiRepository { ...@@ -119,10 +123,7 @@ object EmojiRepository {
var result: String = input.toString() var result: String = input.toString()
while (matcher.find()) { while (matcher.find()) {
val unicode = shortNameToUnicode.get(":${matcher.group(1)}:") val unicode = shortNameToUnicode.get(":${matcher.group(1)}:") ?: continue
if (unicode == null) {
continue
}
if (supported) { if (supported) {
result = result.replace(":" + matcher.group(1) + ":", unicode) result = result.replace(":" + matcher.group(1) + ":", unicode)
...@@ -159,9 +160,7 @@ object EmojiRepository { ...@@ -159,9 +160,7 @@ object EmojiRepository {
private fun buildStringListFromJsonArray(array: JSONArray): List<String> { private fun buildStringListFromJsonArray(array: JSONArray): List<String> {
val list = ArrayList<String>(array.length()) val list = ArrayList<String>(array.length())
for (i in 0..array.length() - 1) { (0 until array.length()).mapTo(list) { array.getString(it) }
list.add(array.getString(i))
}
return list return list
} }
......
...@@ -17,22 +17,22 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac ...@@ -17,22 +17,22 @@ class EmojiTypefaceSpan(family: String, private val newType: Typeface) : Typefac
private fun applyCustomTypeFace(paint: Paint, tf: Typeface) { private fun applyCustomTypeFace(paint: Paint, tf: Typeface) {
val oldStyle: Int val oldStyle: Int
val old = paint.getTypeface() val old = paint.typeface
if (old == null) { if (old == null) {
oldStyle = 0 oldStyle = 0
} else { } else {
oldStyle = old.getStyle() oldStyle = old.style
} }
val fake = oldStyle and tf.style.inv() val fake = oldStyle and tf.style.inv()
if (fake and Typeface.BOLD != 0) { if (fake and Typeface.BOLD != 0) {
paint.setFakeBoldText(true) paint.isFakeBoldText = true
} }
if (fake and Typeface.ITALIC != 0) { if (fake and Typeface.ITALIC != 0) {
paint.setTextSkewX(-0.25f) paint.textSkewX = -0.25f
} }
paint.setTypeface(tf) paint.typeface = tf
} }
} }
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:endColor="#00000000"
android:centerColor="#30000000"
android:startColor="#C0000000"
android:type="linear" />
</shape>
\ No newline at end of file
...@@ -3,9 +3,7 @@ ...@@ -3,9 +3,7 @@
android:height="24dp" android:height="24dp"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF2F343D" android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector> </vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="1055.0303"
android:viewportWidth="1055.0303">
<group
android:translateX="281.28394"
android:translateY="271.51514">
<path
android:fillColor="#CC3333"
android:pathData="M491.3,255.3c0,-24.1 -7.2,-47.2 -21.4,-68.7c-12.8,-19.3 -30.7,-36.4 -53.2,-50.7c-43.5,-27.8 -100.6,-43.1 -160.9,-43.1c-20.1,0 -40,1.7 -59.2,5.1c-11.9,-11.2 -25.9,-21.2 -40.7,-29.2c-79,-38.3 -144.6,-0.9 -144.6,-0.9s60.9,50.1 51,93.9c-27.3,27 -42,59.6 -42,93.6c0,0.1 0,0.2 0,0.3c0,0.1 0,0.2 0,0.3c0,33.9 14.8,66.6 42,93.6c9.9,43.9 -51,93.9 -51,93.9s65.5,37.4 144.6,-0.9c14.8,-8 28.8,-18 40.7,-29.2c19.2,3.4 39.1,5.1 59.2,5.1c60.3,0 117.4,-15.3 160.9,-43.1c22.5,-14.4 40.4,-31.5 53.2,-50.7c14.2,-21.5 21.4,-44.6 21.4,-68.7c0,-0.1 0,-0.2 0,-0.3C491.3,255.6 491.3,255.4 491.3,255.3z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M255.9,124.2c113.9,0 206.3,59 206.3,131.8c0,72.8 -92.4,131.8 -206.3,131.8c-25.4,0 -49.7,-2.9 -72.1,-8.3c-22.8,27.4 -73,65.6 -121.7,53.3c15.9,-17 39.4,-45.8 34.3,-93.2c-29.2,-22.7 -46.8,-51.8 -46.8,-83.5C49.6,183.2 142,124.2 255.9,124.2" />
<path
android:fillColor="#CC3333"
android:pathData="M255.9,256m-27.4,0a27.4,27.4 0,1 1,54.8 0a27.4,27.4 0,1 1,-54.8 0" />
<path
android:fillColor="#CC3333"
android:pathData="M351.2,256m-27.4,0a27.4,27.4 0,1 1,54.8 0a27.4,27.4 0,1 1,-54.8 0" />
<path
android:fillColor="#CC3333"
android:pathData="M160.6,256m-27.4,0a27.4,27.4 0,1 1,54.8 0a27.4,27.4 0,1 1,-54.8 0" />
<path
android:fillColor="#CCCCCC"
android:pathData="M255.8,372.8c-25.4,0 -56.2,-4.9 -78.7,-9.5c-20.1,21 -53.7,52.7 -99.6,50.3c-5.7,8.6 -10.2,13.5 -15.5,19.2c48.7,12.3 98.9,-25.8 121.7,-53.3c22.4,5.4 46.7,8.3 72.1,8.3c113,0 204.8,-58.1 206.3,-130C460.7,320.1 368.8,372.8 255.8,372.8z" />
<path
android:fillColor="#00000000"
android:pathData="M172,350.9"
android:strokeColor="#000000"
android:strokeWidth="1" />
<path
android:fillColor="#00000000"
android:pathData="M200.4,422.9"
android:strokeColor="#000000"
android:strokeWidth="1" />
</group>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z" />
</vector>
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/darkGray" /> <solid android:color="@color/colorPrimary" />
<size <size
android:width="4dp" android:width="4dp"
android:height="4dp" /> android:height="4dp" />
<corners android:radius="8dp" />
</shape> </shape>
\ No newline at end of file
...@@ -23,11 +23,26 @@ ...@@ -23,11 +23,26 @@
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</LinearLayout> </LinearLayout>
<FrameLayout
android:id="@+id/navigation_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start">
<android.support.design.widget.NavigationView <android.support.design.widget.NavigationView
android:id="@+id/view_navigation" android:id="@+id/view_navigation"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" app:menu="@menu/navigation"
app:menu="@menu/navigation" /> app:headerLayout="@layout/nav_header" />
<android.support.v7.widget.RecyclerView
android:id="@+id/accounts_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/nav_header_height"
android:elevation="20dp"
android:background="@color/white"
android:alpha="0"
android:visibility="gone"/>
</FrameLayout>
</android.support.v4.widget.DrawerLayout> </android.support.v4.widget.DrawerLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<RadioGroup
android:id="@+id/radio_group_sort"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RadioButton
android:id="@+id/radio_sort_alphabetical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/dialog_sort_by_alphabet"
android:textSize="18sp" />
<RadioButton
android:id="@+id/radio_sort_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/dialog_sort_by_activity"
android:textSize="18sp" />
</RadioGroup>
<CheckBox
android:id="@+id/checkbox_group_by_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:padding="8dp"
android:text="@string/dialog_group_by_type"
android:textSize="18sp" />
<!--TODO Add checkbox for "Group favourites after sdk support"-->
</LinearLayout>
\ No newline at end of file
...@@ -113,6 +113,7 @@ ...@@ -113,6 +113,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_facebook" android:contentDescription="@string/msg_content_description_log_in_using_facebook"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_facebook" android:src="@drawable/ic_facebook"
...@@ -124,6 +125,7 @@ ...@@ -124,6 +125,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_github" android:contentDescription="@string/msg_content_description_log_in_using_github"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_github" android:src="@drawable/ic_github"
...@@ -135,6 +137,7 @@ ...@@ -135,6 +137,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_google" android:contentDescription="@string/msg_content_description_log_in_using_google"
android:foreground="?android:attr/selectableItemBackground" android:foreground="?android:attr/selectableItemBackground"
android:src="@drawable/ic_google" android:src="@drawable/ic_google"
...@@ -146,6 +149,7 @@ ...@@ -146,6 +149,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_linkedin" android:contentDescription="@string/msg_content_description_log_in_using_linkedin"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_linkedin" android:src="@drawable/ic_linkedin"
...@@ -157,6 +161,7 @@ ...@@ -157,6 +161,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_meteor" android:contentDescription="@string/msg_content_description_log_in_using_meteor"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_meteor" android:src="@drawable/ic_meteor"
...@@ -168,6 +173,7 @@ ...@@ -168,6 +173,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_twitter" android:contentDescription="@string/msg_content_description_log_in_using_twitter"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_twitter" android:src="@drawable/ic_twitter"
...@@ -179,6 +185,7 @@ ...@@ -179,6 +185,7 @@
android:layout_width="290dp" android:layout_width="290dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:clickable="false"
android:contentDescription="@string/msg_content_description_log_in_using_gitlab" android:contentDescription="@string/msg_content_description_log_in_using_gitlab"
android:foreground="?android:attr/selectableItemBackgroundBorderless" android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_gitlab" android:src="@drawable/ic_gitlab"
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
android:digits="0123456789abcdefghijklmnopqrstuvwxyz.-/:" android:digits="0123456789abcdefghijklmnopqrstuvwxyz.-/:"
android:inputType="textUri" android:inputType="textUri"
android:paddingEnd="0dp" android:paddingEnd="0dp"
android:paddingStart="2dp" /> android:paddingStart="4dp" />
<TextView <TextView
android:id="@+id/text_server_protocol" android:id="@+id/text_server_protocol"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground" >
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/server_logo"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:actualImageScaleType="centerInside" />
<TextView
android:id="@+id/text_server_url"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="@id/server_logo"
app:layout_constraintStart_toEndOf="@id/server_logo"
app:layout_constraintEnd_toEndOf="parent"
tools:text="https://open.rocket.chat" />
<TextView
android:id="@+id/text_username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="@id/server_logo"
app:layout_constraintStart_toEndOf="@id/server_logo"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Lucio Maciel"/>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground" >
<ImageView
android:id="@+id/server_logo"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginBottom="8dp"
android:src="@drawable/ic_add_24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:text="@string/action_add_account"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/server_logo"
app:layout_constraintEnd_toEndOf="parent" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/darkGray" />
<TextView
android:id="@+id/text_chatroom_header"
style="@style/ChatRooms.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:text="@string/chatroom_header" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/darkGray" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/message_item_top_and_bottom_padding">
<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:layout_marginStart="56dp"
android:background="@drawable/quote_vertical_bar"
app:layout_constraintBottom_toTopOf="@+id/recycler_view_reactions"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/top_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="horizontal"
app:layout_constraintLeft_toRightOf="@+id/quote_bar"
app:layout_constraintTop_toBottomOf="@id/new_messages_notif">
<TextView
android:id="@+id/text_sender"
style="@style/Sender.Name.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorPrimary"
tools:text="Ronald Perkins" />
<TextView
android:id="@+id/text_message_time"
style="@style/Timestamp.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
tools:text="11:45 PM" />
</LinearLayout>
<TextView
android:id="@+id/text_content"
style="@style/Message.Quote.TextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/top_container"
app:layout_constraintTop_toBottomOf="@+id/top_container"
tools:text="This is a multiline chat message from Bertie that will take more than just one line of text. I have sure that everything is amazing!" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/quote_bar"
app:layout_constraintTop_toBottomOf="@+id/text_content" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -6,6 +6,17 @@ ...@@ -6,6 +6,17 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/colorPrimary"> android:background="@color/colorPrimary">
<View
android:id="@+id/quote_bar"
android:layout_width="4dp"
android:layout_height="0dp"
android:background="@drawable/quote_vertical_bar"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/image_view_action_cancel_quote"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/text_view_action_text" android:id="@+id/text_view_action_text"
android:layout_width="0dp" android:layout_width="0dp"
......
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/server_logo"
android:layout_width="0dp"
android:layout_height="0dp"
android:foreground="@drawable/black_gradient"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:actualImageScaleType="centerCrop"
tools:src="@tools:sample/backgrounds/scenic" />
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_avatar"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:roundedCornerRadius="3dp"
tools:src="@tools:sample/avatars" />
<android.support.constraint.ConstraintLayout
android:id="@+id/account_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginTop="16dp"
android:padding="4dp"
android:elevation="2dp"
android:background="?selectableItemBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/image_avatar"
app:layout_constraintTop_toBottomOf="@+id/image_avatar">
<TextView
android:id="@+id/text_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="@color/white"
app:layout_constraintEnd_toStartOf="@+id/account_expand"
app:layout_constraintStart_toStartOf="parent"
tools:text="Lucio Maciel" />
<TextView
android:id="@+id/text_server"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="@color/white"
app:layout_constraintEnd_toStartOf="@+id/account_expand"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_name"
tools:text="https://open.rocket.chat" />
<ImageView
android:id="@+id/account_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:src="@drawable/ic_expand_more_24dp"
android:tint="@color/whitesmoke"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
...@@ -6,9 +6,9 @@ ...@@ -6,9 +6,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="2dp" android:layout_marginBottom="2dp"
android:layout_marginEnd="2dp" android:layout_marginEnd="2dp"
android:layout_marginLeft="4dp" android:layout_marginLeft="8dp"
android:layout_marginRight="2dp" android:layout_marginRight="2dp"
android:layout_marginStart="4dp" android:layout_marginStart="8dp"
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:background="@color/suggestion_background_color"> android:background="@color/suggestion_background_color">
...@@ -22,7 +22,8 @@ ...@@ -22,7 +22,8 @@
android:id="@+id/image_avatar" android:id="@+id/image_avatar"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_margin="4dp" android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
app:roundedCornerRadius="3dp" app:roundedCornerRadius="3dp"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
......
...@@ -9,4 +9,10 @@ ...@@ -9,4 +9,10 @@
app:actionViewClass="android.support.v7.widget.SearchView" app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" /> app:showAsAction="ifRoom|collapseActionView" />
<item
android:id="@+id/action_sort"
android:icon="@drawable/ic_sort"
android:title="@string/menu_chatroom_sort"
app:showAsAction="always" />
</menu> </menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
\ No newline at end of file
<resources>
<!-- Titles -->
<string name="title_sign_in_your_server">अपने सर्वर में साइन इन करें</string>
<string name="title_log_in">लॉग इन करें</string>
<string name="title_sign_up">साइन अप करें</string>
<string name="title_authentication">प्रमाणीकरण</string>
<string name="title_legal_terms">कानूनी शर्तें</string>
<string name="title_chats">चैट</string>
<string name="title_profile">प्रोफाइल</string>
<string name="title_members">सदस्य (%d)</string>
<string name="title_settings">सेटिंग्स</string>
<string name="title_password">पासवर्ड बदलें</string>
<string name="title_update_profile">प्रोफ़ाइल अपडेट करें</string>
<!-- Actions -->
<string name="action_connect">जुडिये</string>
<string name="action_login_or_sign_up">लॉग इन करने या खाता बनाने के लिए इस बटन को टैप करें</string>
<string name="action_terms_of_service">सेवा की शर्तें</string>
<string name="action_privacy_policy">गोपनीयता नीति</string>
<string name="action_search">खोजें</string>
<string name="action_update">अद्यतन करें</string>
<string name="action_settings">सेटिंग्स</string>
<string name="action_logout">लोग आउट करें</string>
<string name="action_files">फ़ाइलें</string>
<string name="action_confirm_password">पासवर्ड परिवर्तन की पुष्टि करें</string>
<string name="action_join_chat">चैट में शामिल हों</string>
<string name="action_add_account">खाता जोड़ो</string>
<!-- Settings List -->
<string-array name="settings_actions">
<item name="item_password">पासवर्ड बदलें</item>
</string-array>
<!-- Regular information messages -->
<string name="msg_no_internet_connection">कोई इंटरनेट कनेक्शन नहीं है</string>
<string name="msg_generic_error">क्षमा करें, एक त्रुटि हुई है, कृपया पुनः प्रयास करें</string>
<string name="msg_no_data_to_display">डेटा प्रदर्शित करने के लिए उपलब्ध नहीं हैं</string>
<string name="msg_profile_update_successfully">प्रोफ़ाइल सफलतापूर्वक अपडेट हो गया है</string>
<string name="msg_username">यूजरनेम</string>
<string name="msg_username_or_email">यूजरनेम या ईमेल</string>
<string name="msg_password">पासवर्ड</string>
<string name="msg_name">नाम</string>
<string name="msg_email">ईमेल</string>
<string name="msg_avatar_url">अवतार यूआरएल</string>
<string name="msg_or_continue_using_social_accounts">या सामाजिक खाते का उपयोग करना जारी रखें</string>
<string name="msg_new_user">नया उपयोगकर्ता? %1$s</string>
<string name="msg_new_user_agreement">आगे बढ़कर आप हमारे %1$s और %2$s से सहमत हो रहे हैं</string>
<string name="msg_2fa_code">कोड 2FA</string>
<string name="msg_yesterday">कल</string>
<string name="msg_message">संदेश</string>
<string name="msg_this_room_is_read_only">यह रूम केवल पढ़ने के लिए है</string>
<string name="msg_invalid_2fa_code">अमान्य 2FA कोड</string>
<string name="msg_invalid_file">अवैध फाइल</string>
<string name="msg_invalid_server_url">अमान्य सर्वर यूआरL</string>
<string name="msg_content_description_log_in_using_facebook">Facebook द्वारा लॉगिन करें</string>
<string name="msg_content_description_log_in_using_github">Github द्वारा लॉगिन करें</string>
<string name="msg_content_description_log_in_using_google">Google द्वारा लॉगिन करें</string>
<string name="msg_content_description_log_in_using_linkedin">Linkedin द्वारा लॉगिन करें</string>
<string name="msg_content_description_log_in_using_meteor">Meteor द्वारा लॉगिन करें</string>
<string name="msg_content_description_log_in_using_twitter">Twitter द्वारा लॉगिन करें</string>
<string name="msg_content_description_log_in_using_gitlab">Gitlab द्वारा लॉगिन करें</string>
<string name="msg_content_description_send_message">मेसेज भेजें</string>
<string name="msg_content_description_show_attachment_options">अटैचमेंट विकल्प दिखाएं</string>
<string name="msg_you">आप</string>
<string name="msg_unknown">अनजान</string>
<string name="msg_email_address">ई-मेल एड्रेस</string>
<string name="msg_utc_offset">यूटीसी ऑफ़सेट</string>
<string name="msg_new_password">नया पासवर्ड दर्ज करें</string>
<string name="msg_confirm_password">नए पासवर्ड की पुष्टि करें</string>
<string name="msg_preview_video">वीडियो</string>
<string name="msg_preview_audio">ऑडियो</string>
<string name="msg_preview_photo">तस्वीरें</string>
<string name="msg_unread_messages">अपठित संदेश</string>
<string name="msg_no_messages_yet">अभी तक कोई पोस्ट नहीं</string>
<string name="msg_requires_username">उपयोगकर्ता नाम आवश्यक है: कृपया, वेब संस्करण के माध्यम से एक उपयोगकर्ता नाम बनाएं और लॉग इन करने के लिए वापस आएँ</string>
<string name="msg_ok">OK</string>
<string name="msg_ver_not_recommended">
ऐसा लगता है कि आपका सर्वर संस्करण अनुशंसित संस्करण %1$s के नीचे है।\nआप अभी भी लॉगिन कर सकते हैं लेकिन आप अप्रत्याशित व्यवहार का अनुभव कर सकते हैं
</string>
<string name="msg_ver_not_minimum">
ऐसा लगता है कि आपका सर्वर संस्करण न्यूनतम आवश्यक संस्करण %1$s से कम है।\nकृपया लॉगिन करने के लिए अपने सर्वर को अपग्रेड करें!
</string>
<!-- System messages -->
<string name="message_room_name_changed">%2$s ने रूम का नाम बदलकर %1$s किया</string>
<string name="message_user_added_by">उपयोगकर्ता %1$s द्वारा %2$s को जोड़ा गया</string>
<string name="message_user_removed_by">उपयोगकर्ता %1$s द्वारा %2$s को निकाला गया</string>
<string name="message_user_left">ने चैनल को छोड़ दिया है</string>
<string name="message_user_joined_channel">चैनल में शामिल हो गया है</string>
<string name="message_welcome">%s का स्वागत करते हैं</string>
<string name="message_removed">संदेश हटाया गया</string>
<string name="message_pinned">एक संदेश पिन किया:</string>
<!-- Message actions -->
<string name="action_msg_reply">जवाब दें</string>
<string name="action_msg_edit">संपादन करें</string>
<string name="action_msg_copy">कॉपी</string>
<string name="action_msg_quote">उद्धरण</string>
<string name="action_msg_delete">मिटायें</string>
<string name="action_msg_pin">संदेश को पिन करें</string>
<string name="action_msg_unpin">संदेश को पिन से हटाएँ</string>
<string name="action_msg_star">संदेश को स्टार करें</string>
<string name="action_msg_share">शेयर करें</string>
<string name="action_title_editing">संपादन संदेश</string>
<string name="action_msg_add_reaction">प्रतिक्रिया जोड़ें</string>
<!-- Permission messages -->
<string name="permission_editing_not_allowed">संपादन की अनुमति नहीं है</string>
<string name="permission_deleting_not_allowed">हटाने की अनुमति नहीं है</string>
<string name="permission_pinning_not_allowed">पिनि करने की अनुमति नहीं है</string>
<!-- Members List -->
<string name="title_members_list">सदस्यों की सूची</string>
<!-- Pinned Messages -->
<string name="title_pinned_messages">पिन किए गए संदेश</string>
<!-- Upload Messages -->
<string name="max_file_size_exceeded">फ़ाइल का आकार %1$d बाइट्स ने %2$d बाइट्स के अधिकतम अपलोड आकार को पार कर लिया है</string>
<!-- Socket status -->
<string name="status_connected">कनेक्टेड</string>
<string name="status_disconnected">डिस्कनेक्टेड</string>
<string name="status_connecting">कनेक्टिंग</string>
<string name="status_authenticating">प्रमाणीकरण</string>
<string name="status_disconnecting">डिसकनेक्टिंग</string>
<string name="status_waiting">%d सेकेंड में कनेक्ट हो रहा है</string>
<!--Suggestions-->
<string name="suggest_all_description">इस कमरे में सभी को सूचित करें</string>
<string name="suggest_here_description">इस कमरे में सक्रिय उपयोगकर्ताओं को सूचित करना</string>
<!-- Slash Commands -->
<string name="Slash_Gimme_Description">आपके संदेश से पहले ༼ つ ◕_◕ ༽つ दिखाता है</string>
<string name="Slash_LennyFace_Description">आपके संदेश के बाद ( ͡° ͜ʖ ͡°) दिखाता है</string>
<string name="Slash_Shrug_Description">आपके संदेश के बाद ¯\_(ツ)_/¯ दिखाता है</string>
<string name="Slash_Tableflip_Description">(╯°□°)╯︵ ┻━┻ दिखाता है</string>
<string name="Slash_TableUnflip_Description">┬─┬ ノ( ゜-゜ノ) दिखाता है</string>
<string name="Create_A_New_Channel">एक नया चैनल बनाएं</string>
<string name="Show_the_keyboard_shortcut_list">कीबोर्ड शॉर्टकट सूची दिखाएं</string>
<string name="Invite_user_to_join_channel_all_from">इस चैनल में शामिल होने के लिए [#channel] से सभी उपयोगकर्ताओं को आमंत्रित करें</string>
<string name="Invite_user_to_join_channel_all_to">[# चैनल] में शामिल होने के लिए सभी उपयोगकर्ताओं को इस चैनल पर आमंत्रित करें</string>
<string name="Archive">संग्रहित करें</string>
<string name="Remove_someone_from_room">रूम से किसी को निकालें</string>
<string name="Leave_the_current_channel">मौजूदा चैनल को छोड़ दें</string>
<string name="Displays_action_text">Displays action text</string>
<string name="Direct_message_someone">किसी को प्रत्यक्ष संदेश भेजें</string>
<string name="Mute_someone_in_room">रूम में किसी को म्यूट करें</string>
<string name="Unmute_someone_in_room">रूम में किसी को अनम्यूट करें</string>
<string name="Invite_user_to_join_channel">इस चैनल में शामिल होने के लिए एक उपयोगकर्ता को आमंत्रित करें</string>
<string name="Unarchive">असंग्रहित करें</string>
<string name="Join_the_given_channel">दिए गए चैनल से जुड़ें</string>
<string name="Guggy_Command_Description">दिए गए पाठ पर आधारित gif उत्पन्न करता</string>
<string name="Slash_Topic_Description">विषय सेट करें</string>
<!-- Emoji message-->
<string name="msg_no_recent_emoji"> कोई नया इमोजी नहीं</string>
<!-- Sorting and grouping-->
<string name="menu_chatroom_sort">क्रम</string>
<string name="dialog_sort_title">द्वारा सॉर्ट करें</string>
<string name="dialog_sort_by_alphabet">वर्णानुक्रम</string>
<string name="dialog_sort_by_activity">गतिविधि</string>
<string name="dialog_group_by_type">प्रकार के आधार पर समूह</string>
<string name="dialog_group_favourites">पसंदीदा समूह</string>
<string name="chatroom_header">हैडर</string>
<!--ChatRooms Headers-->
<string name="header_channel">चैनलों</string>
<string name="header_private_groups">निजी समूहों</string>
<string name="header_direct_messages">प्रत्यक्ष संदेश</string>
<string name="header_live_chats">Live Chats</string>
<string name="header_unknown">अज्ञात</string>
</resources>
\ No newline at end of file
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<string name="action_files">Arquivos</string> <string name="action_files">Arquivos</string>
<string name="action_confirm_password">Confirme a nova senha</string> <string name="action_confirm_password">Confirme a nova senha</string>
<string name="action_join_chat">Entrar no Chat</string> <string name="action_join_chat">Entrar no Chat</string>
<string name="action_add_account">Adicionar Conta</string>
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
...@@ -69,6 +69,18 @@ ...@@ -69,6 +69,18 @@
<string name="msg_new_password">Informe a nova senha</string> <string name="msg_new_password">Informe a nova senha</string>
<string name="msg_confirm_password">Confirme a nova senha</string> <string name="msg_confirm_password">Confirme a nova senha</string>
<string name="msg_unread_messages">Mensagens não lidas</string> <string name="msg_unread_messages">Mensagens não lidas</string>
<string name="msg_preview_video">Vídeo</string>
<string name="msg_preview_audio">Audio</string>
<string name="msg_preview_photo">Foto</string>
<string name="msg_no_messages_yet">Nenhuma mensagem ainda</string>
<string name="msg_requires_username">Nome de usuário requerido: Por favor, crie um nome de usuário através da versão web e volte para fazer login.</string>
<string name="msg_ok">OK</string>
<string name="msg_ver_not_recommended">
Parece que a versão do seu servidor está abaixo da recomendada %1$s.\nVocê ainda assim pode logar e continuar mas podem ocorrer alguns problemas inesperados.
</string>
<string name="msg_ver_not_minimum">
Parece que a versão do seu servidor está abaixo da mínima requerida %1$s.\nPor favor, atualize seus servidores antes de continuar!
</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string> <string name="message_room_name_changed">Nome da sala alterado para: %1$s por %2$s</string>
...@@ -115,6 +127,10 @@ ...@@ -115,6 +127,10 @@
<string name="status_disconnecting">desconectando</string> <string name="status_disconnecting">desconectando</string>
<string name="status_waiting">conectando em %d segundos</string> <string name="status_waiting">conectando em %d segundos</string>
<!--Suggestions-->
<string name="suggest_all_description">Notifica todos nesta sala</string>
<string name="suggest_here_description">Notifica usuários ativos nesta sala</string>
<!-- Slash Commands --> <!-- Slash Commands -->
<string name="Slash_Gimme_Description">Exibir ༼ つ ◕_◕ ༽つ antes de sua mensagem</string> <string name="Slash_Gimme_Description">Exibir ༼ つ ◕_◕ ༽つ antes de sua mensagem</string>
<string name="Slash_LennyFace_Description">Exibir ( ͡° ͜ʖ ͡°) depois de sua mensagem</string> <string name="Slash_LennyFace_Description">Exibir ( ͡° ͜ʖ ͡°) depois de sua mensagem</string>
...@@ -140,4 +156,20 @@ ...@@ -140,4 +156,20 @@
<!-- Emoji message--> <!-- Emoji message-->
<string name="msg_no_recent_emoji">Nenhum emoji recente</string> <string name="msg_no_recent_emoji">Nenhum emoji recente</string>
<!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Ordenar</string>
<string name="dialog_sort_title">Ordenar por</string>
<string name="dialog_sort_by_alphabet">Alfabeticamente</string>
<string name="dialog_sort_by_activity">Atividade</string>
<string name="dialog_group_by_type">Agrupar por tipo</string>
<string name="dialog_group_favourites">Grupos favoritos</string>
<string name="chatroom_header">Cabeçalho</string>
<!--ChatRooms Headers-->
<string name="header_channel">Canais</string>
<string name="header_private_groups">Grupos Privados</string>
<string name="header_direct_messages">Mensagens diretas</string>
<string name="header_live_chats">Live Chats</string>
<string name="header_unknown">Desconhecido</string>
</resources> </resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#15293F</color>
<!-- Main colors --> <!-- Main colors -->
<color name="colorPrimary">#FF303030</color> <!-- Material Grey 850 --> <color name="colorPrimary">#FF303030</color> <!-- Material Grey 850 -->
...@@ -17,6 +16,8 @@ ...@@ -17,6 +16,8 @@
<color name="colorUserStatusAway">#FDD236</color> <color name="colorUserStatusAway">#FDD236</color>
<color name="colorUserStatusOffline">#d9d9d9</color> <color name="colorUserStatusOffline">#d9d9d9</color>
<color name="ic_launcher_background">#FFFFFF</color>
<color name="colorDrawableTintGrey">#9FA2A8</color> <color name="colorDrawableTintGrey">#9FA2A8</color>
<color name="colorDividerMessageComposer">#D8D8D8</color> <color name="colorDividerMessageComposer">#D8D8D8</color>
...@@ -34,6 +35,8 @@ ...@@ -34,6 +35,8 @@
<color name="actionMenuColor">#FF727272</color> <color name="actionMenuColor">#FF727272</color>
<color name="whitesmoke">#FFf1f1f1</color> <color name="whitesmoke">#FFf1f1f1</color>
<color name="translucent_white">#70F1F1F1</color>
<color name="colorEmojiIcon">#FF767676</color> <color name="colorEmojiIcon">#FF767676</color>
<!-- Suggestions --> <!-- Suggestions -->
......
...@@ -35,5 +35,6 @@ ...@@ -35,5 +35,6 @@
<!-- Autocomplete Popup --> <!-- Autocomplete Popup -->
<dimen name="popup_max_height">150dp</dimen> <dimen name="popup_max_height">150dp</dimen>
<dimen name="suggestions_box_max_height">250dp</dimen> <dimen name="suggestions_box_max_height">250dp</dimen>
<dimen name="nav_header_height">160dp</dimen>
</resources> </resources>
\ No newline at end of file
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
<string name="action_files">Files</string> <string name="action_files">Files</string>
<string name="action_confirm_password">Confirm Password Change</string> <string name="action_confirm_password">Confirm Password Change</string>
<string name="action_join_chat">Join Chat</string> <string name="action_join_chat">Join Chat</string>
<string name="action_add_account">Add Account</string>
<!-- Settings List --> <!-- Settings List -->
<string-array name="settings_actions"> <string-array name="settings_actions">
...@@ -70,6 +71,17 @@ ...@@ -70,6 +71,17 @@
<string name="msg_new_password">Enter New Password</string> <string name="msg_new_password">Enter New Password</string>
<string name="msg_confirm_password">Confirm New Password</string> <string name="msg_confirm_password">Confirm New Password</string>
<string name="msg_unread_messages">Unread messages</string> <string name="msg_unread_messages">Unread messages</string>
<string name="msg_preview_video">Video</string>
<string name="msg_preview_audio">Audio</string>
<string name="msg_preview_photo">Photo</string>
<string name="msg_no_messages_yet">No messages yet</string>
<string name="msg_requires_username">Username required: Please, create an username through the web version and come back to log in.</string>
<string name="msg_ok">OK</string>
<string name="msg_ver_not_recommended">
Looks like your server version is below the recommended version %1$s.\nYou can still login but you may experience unexpected behaviors.</string>
<string name="msg_ver_not_minimum">
Looks like your server version is below the minimum required version %1$s.\nPlease upgrade your server to login!
</string>
<!-- System messages --> <!-- System messages -->
<string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string> <string name="message_room_name_changed">Room name changed to: %1$s by %2$s</string>
...@@ -116,6 +128,10 @@ ...@@ -116,6 +128,10 @@
<string name="status_disconnecting">disconnecting</string> <string name="status_disconnecting">disconnecting</string>
<string name="status_waiting">connecting in %d seconds</string> <string name="status_waiting">connecting in %d seconds</string>
<!--Suggestions-->
<string name="suggest_all_description">Notify all in this room</string>
<string name="suggest_here_description">Notify active users in this room</string>
<!-- Slash Commands --> <!-- Slash Commands -->
<string name="Slash_Gimme_Description">Displays ༼ つ ◕_◕ ༽つ before your message</string> <string name="Slash_Gimme_Description">Displays ༼ つ ◕_◕ ༽つ before your message</string>
<string name="Slash_LennyFace_Description">Displays ( ͡° ͜ʖ ͡°) after your message</string> <string name="Slash_LennyFace_Description">Displays ( ͡° ͜ʖ ͡°) after your message</string>
...@@ -141,4 +157,20 @@ ...@@ -141,4 +157,20 @@
<!-- Emoji message--> <!-- Emoji message-->
<string name="msg_no_recent_emoji">No recent emoji</string> <string name="msg_no_recent_emoji">No recent emoji</string>
<!-- Sorting and grouping-->
<string name="menu_chatroom_sort">Sort</string>
<string name="dialog_sort_title">Sort by</string>
<string name="dialog_sort_by_alphabet">Alphabetical</string>
<string name="dialog_sort_by_activity">Activity</string>
<string name="dialog_group_by_type">Group by type</string>
<string name="dialog_group_favourites">Group favourites</string>
<string name="chatroom_header">Header</string>
<!--ChatRooms Headers-->
<string name="header_channel">Channels</string>
<string name="header_private_groups">Private Groups</string>
<string name="header_direct_messages">Direct Messages</string>
<string name="header_live_chats">Live Chats</string>
<string name="header_unknown">Unknown</string>
</resources> </resources>
\ No newline at end of file
...@@ -71,6 +71,10 @@ ...@@ -71,6 +71,10 @@
<item name="android:paddingStart">@dimen/edit_text_margin</item> <item name="android:paddingStart">@dimen/edit_text_margin</item>
</style> </style>
<style name="ChatRooms.Header" parent="TextAppearance.AppCompat.Headline">
<item name="android:textSize">16sp</item>
</style>
<style name="ChatRoom.Name.TextView" parent="TextAppearance.AppCompat.Title"> <style name="ChatRoom.Name.TextView" parent="TextAppearance.AppCompat.Title">
<item name="android:ellipsize">end</item> <item name="android:ellipsize">end</item>
<item name="android:maxLines">1</item> <item name="android:maxLines">1</item>
...@@ -88,6 +92,10 @@ ...@@ -88,6 +92,10 @@
<item name="android:textColor">@color/colorPrimaryText</item> <item name="android:textColor">@color/colorPrimaryText</item>
</style> </style>
<style name="Message.Quote.TextView" parent="Message.TextView">
<item name="android:textColor">@color/colorPrimaryText</item>
</style>
<style name="Timestamp.TextView" parent="TextAppearance.AppCompat.Caption"> <style name="Timestamp.TextView" parent="TextAppearance.AppCompat.Caption">
<item name="android:textSize">10sp</item> <item name="android:textSize">10sp</item>
</style> </style>
......
...@@ -10,11 +10,12 @@ buildscript { ...@@ -10,11 +10,12 @@ buildscript {
} }
dependencies { dependencies {
classpath "com.android.tools.build:gradle:3.0.1" classpath 'com.android.tools.build:gradle:3.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"
classpath 'com.google.gms:google-services:3.2.0' classpath 'com.google.gms:google-services:3.2.0'
classpath 'io.fabric.tools:gradle:1.25.1' classpath 'io.fabric.tools:gradle:1.25.1'
classpath "io.realm:realm-gradle-plugin:5.0.0"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
......
...@@ -4,14 +4,14 @@ ext { ...@@ -4,14 +4,14 @@ ext {
compileSdk : 27, compileSdk : 27,
targetSdk : 27, targetSdk : 27,
buildTools : '27.0.3', buildTools : '27.0.3',
kotlin : '1.2.30', kotlin : '1.2.31',
coroutine : '0.22', coroutine : '0.22.5',
dokka : '0.9.15', dokka : '0.9.16',
// Main dependencies // Main dependencies
support : '27.0.2', support : '27.1.0',
constraintLayout : '1.0.2', constraintLayout : '1.0.2',
androidKtx : '0.1', androidKtx : '0.2',
dagger : '2.14.1', dagger : '2.14.1',
exoPlayer : '2.6.0', exoPlayer : '2.6.0',
playServices : '11.8.0', playServices : '11.8.0',
...@@ -19,12 +19,12 @@ ext { ...@@ -19,12 +19,12 @@ ext {
rxKotlin : '2.2.0', rxKotlin : '2.2.0',
rxAndroid : '2.0.2', rxAndroid : '2.0.2',
moshi : '1.6.0-SNAPSHOT', moshi : '1.6.0-SNAPSHOT',
okhttp : '3.9.1', okhttp : '3.10.0',
timber : '4.6.1', timber : '4.7.0',
threeTenABP : '1.0.5', threeTenABP : '1.0.5',
rxBinding : '2.0.0', rxBinding : '2.0.0',
fresco : '1.8.1', fresco : '1.8.1',
kotshi : '0.3.0', kotshi : '1.0.2',
frescoImageViewer : '0.5.1', frescoImageViewer : '0.5.1',
markwon : '1.0.3', markwon : '1.0.3',
sheetMenu : '1.3.3', sheetMenu : '1.3.3',
...@@ -37,7 +37,6 @@ ext { ...@@ -37,7 +37,6 @@ ext {
expresso : '3.0.1', expresso : '3.0.1',
mockito : '2.10.0' mockito : '2.10.0'
] ]
libraries = [ libraries = [
kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jre8:${versions.kotlin}", kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jre8:${versions.kotlin}",
coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutine}", coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutine}",
...@@ -50,6 +49,7 @@ ext { ...@@ -50,6 +49,7 @@ ext {
constraintLayout : "com.android.support.constraint:constraint-layout:${versions.constraintLayout}", constraintLayout : "com.android.support.constraint:constraint-layout:${versions.constraintLayout}",
cardView : "com.android.support:cardview-v7:${versions.support}", cardView : "com.android.support:cardview-v7:${versions.support}",
flexbox : "com.google.android:flexbox:${versions.flexbox}", flexbox : "com.google.android:flexbox:${versions.flexbox}",
customTabs : "com.android.support:customtabs:${versions.support}",
androidKtx : "androidx.core:core-ktx:${versions.androidKtx}", androidKtx : "androidx.core:core-ktx:${versions.androidKtx}",
...@@ -100,4 +100,5 @@ ext { ...@@ -100,4 +100,5 @@ ext {
roomTest : "android.arch.persistence.room:testing:${versions.room}", roomTest : "android.arch.persistence.room:testing:${versions.room}",
truth : "com.google.truth:truth:$versions.truth", truth : "com.google.truth:truth:$versions.truth",
] ]
} }
\ No newline at end of file
...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME ...@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
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