Unverified Commit 22c57a15 authored by Rafael Kellermann Streit's avatar Rafael Kellermann Streit Committed by GitHub

Merge pull request #1621 from RocketChat/beta

[RELEASE] 2.6.0 release
parents 797a06fc 658cee9a
......@@ -64,7 +64,7 @@ jobs:
- store_artifacts:
path: app/build/reports/
destination: reports
build-apk:
build-play-apk:
docker:
- image: circleci/android:api-27-alpha
environment:
......@@ -93,7 +93,40 @@ jobs:
- run:
name: Build APK
command: |
./gradlew assembleRelease --info --console=plain --stacktrace
./gradlew assemblePlayRelease --info --console=plain --stacktrace
- store_artifacts:
path: app/build/outputs/apk
destination: apks
build-foss-apk:
docker:
- image: circleci/android:api-27-alpha
environment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- run:
name: restore files from ENV
command: |
echo $ROCKET_JKS_BASE64 | base64 --decode > Rocket.jks
echo $ROCKET_PLAY_JSON | base64 --decode > app/rocket-chat.json
- run:
name: checkout Rocket.Chat.Kotlin.SDK
command: git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git ../Rocket.Chat.Kotlin.SDK
- restore_cache:
key: kotlin-sdk-{{ .Revision }}
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- run:
name: Download Dependencies
command: ./gradlew androidDependencies --quiet --console=plain
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- run:
name: Build APK
command: |
./gradlew assembleFossRelease --info --console=plain --stacktrace
- store_artifacts:
path: app/build/outputs/apk
destination: apks
......@@ -112,6 +145,9 @@ workflows:
- develop
- develop-2.x
- master
- build-apk:
- build-play-apk:
requires:
- build-kotlin-sdk
- build-foss-apk:
requires:
- build-kotlin-sdk
#!/bin/bash
CURRENT_DIR=$(pwd)
# The SDK dir should be 2 directories up in the tree, so we use dirname 2 times
# to get the common parent dir of the SDK and the app
GIT=$(which git)
cd ../..
tmp=$(pwd)
SDK_DIR="$tmp/Rocket.Chat.Kotlin.SDK"
cd "${CURRENT_DIR}"
if [ "$#" -eq 1 ] && [ ! -z "$1" ]; then
# if in an argument is given this is the (relative) path to SDK_DIR
SDK_DIR=$(readlink -f $1)
else
# The SDK dir should be 2 directories up in the tree, so we use dirname 2 times
# to get the common parent dir of the SDK and the app
cd ../..
tmp=$(pwd)
SDK_DIR="$tmp/Rocket.Chat.Kotlin.SDK"
cd "${CURRENT_DIR}"
fi
echo "CURRENT DIR: $CURRENT_DIR"
echo "SDK DIR: $SDK_DIR"
......@@ -99,4 +105,4 @@ cp -v "${SDK_DIR}"/core/build/libs/core-0.1-SNAPSHOT.jar "${CURRENT_DIR}"/libs/c
echo "$SHA" > "${SDK_DIR}"/.last_commit_hash
exit 0
\ No newline at end of file
exit 0
def taskRequests = getGradle().getStartParameter().getTaskRequests().toString()
def isPlay = !(taskRequests.contains("Foss") || taskRequests.contains("foss"))
apply plugin: 'com.android.application'
apply plugin: 'io.fabric'
if (isPlay) { apply plugin: 'io.fabric' }
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: "com.github.ben-manes.versions"
android {
compileSdkVersion versions.compileSdk
......@@ -12,11 +16,15 @@ android {
applicationId "chat.rocket.android"
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
versionCode 2036
versionName "2.5.1"
versionCode 2042
versionName "2.6.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
def gitSha = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim()
def buildTime = new GregorianCalendar().format("MM-dd-yyyy' 'h:mm:ss a z")
buildConfigField "String", "GIT_SHA", "\"${gitSha}\""
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
......@@ -57,6 +65,20 @@ android {
}
}
flavorDimensions "type"
productFlavors {
// includes proprietary libs
play {
dimension "type"
}
// only foss
foss {
dimension "type"
}
}
packagingOptions {
exclude 'META-INF/core.kotlin_module'
exclude 'META-INF/main.kotlin_module'
......@@ -78,26 +100,27 @@ dependencies {
implementation libraries.appCompat
implementation libraries.recyclerview
implementation libraries.material
implementation libraries.constraintlayout
implementation libraries.cardview
implementation libraries.flexbox
implementation libraries.browser
implementation libraries.androidKtx
implementation libraries.fragmentsKtx
implementation libraries.dagger
implementation libraries.daggerSupport
kapt libraries.daggerProcessor
kapt libraries.daggerAndroidApt
implementation libraries.fcm
implementation libraries.playServicesAuth
implementation libraries.flexbox
implementation libraries.material
implementation libraries.room
kapt libraries.roomProcessor
implementation libraries.lifecycleExtensions
kapt libraries.lifecycleCompiler
implementation libraries.viewmodelKtx
implementation libraries.workmanager
implementation libraries.rxKotlin
implementation libraries.rxAndroid
......@@ -115,6 +138,8 @@ dependencies {
implementation libraries.frescoWebP
implementation libraries.frescoAnimatedWebP
implementation libraries.glide
kapt libraries.kotshiCompiler
implementation libraries.kotshiApi
......@@ -124,11 +149,14 @@ dependencies {
implementation libraries.aVLoadingIndicatorView
implementation "com.github.luciofm:livedata-ktx:b1e8bbc25a"
implementation libraries.livedataKtx
implementation('com.crashlytics.sdk.android:crashlytics:2.9.2@aar') {
transitive = true
}
// Proprietary libraries
playImplementation libraries.fcm
playImplementation libraries.firebaseAnalytics
playImplementation libraries.playServicesAuth
playImplementation('com.crashlytics.sdk.android:crashlytics:2.9.5@aar') { transitive = true }
playImplementation('com.crashlytics.sdk.android:answers:1.4.3@aar') { transitive = true }
testImplementation libraries.junit
testImplementation libraries.truth
......@@ -148,13 +176,16 @@ androidExtensions {
// FIXME - build and install the sdk into the app/libs directory
// We were having some issues with the kapt generated files from the sdk when importing as a module
def sdk_location=project.properties['sdk_location'] ?: ""
task compileSdk(type:Exec) {
if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
commandLine 'cmd', '/c', 'build-sdk.sh'
commandLine 'cmd', '/c', 'build-sdk.sh', sdk_location
} else {
commandLine './build-sdk.sh'
commandLine './build-sdk.sh', sdk_location
}
}
preBuild.dependsOn compileSdk
apply plugin: 'com.google.gms.google-services'
if (isPlay) {
apply plugin: 'com.google.gms.google-services'
}
#Thu Feb 15 15:50:42 BRST 2018
#Wed Aug 01 21:56:00 EDT 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
......@@ -24,35 +24,44 @@ class ChatRoomFragmentTest {
@Before
fun stubAllExternalIntents() {
val activityIntent = InstrumentationRegistry.getTargetContext().chatRoomIntent("id", "name", "type", false, 0L)
val activityIntent = InstrumentationRegistry.getTargetContext()
.chatRoomIntent("id", "name", "type", false, 0L)
activityRule.launchActivity(activityIntent)
intending(not(isInternal())).respondWith(ActivityResult(Activity.RESULT_OK, null))
}
@Test
fun showFileSelection_nonNullFiltersAreApplied() {
val fragment = activityRule.activity.supportFragmentManager.findFragmentByTag(ChatRoomActivity.TAG_CHAT_ROOM_FRAGMENT) as ChatRoomFragment
val fragment =
activityRule.activity.supportFragmentManager.findFragmentByTag("ChatRoomFragment") as ChatRoomFragment
val filters = arrayOf("image/*")
fragment.showFileSelection(filters)
intended(allOf(
intended(
allOf(
hasAction(Intent.ACTION_GET_CONTENT),
hasType("*/*"),
hasCategories(setOf(Intent.CATEGORY_OPENABLE)),
hasExtra(Intent.EXTRA_MIME_TYPES, filters)))
hasExtra(Intent.EXTRA_MIME_TYPES, filters)
)
)
}
@Test
fun showFileSelection_nullFiltersAreNotApplied() {
val fragment = activityRule.activity.supportFragmentManager.findFragmentByTag(ChatRoomActivity.TAG_CHAT_ROOM_FRAGMENT) as ChatRoomFragment
val fragment =
activityRule.activity.supportFragmentManager.findFragmentByTag("ChatRoomFragment") as ChatRoomFragment
fragment.showFileSelection(null)
intended(allOf(
intended(
allOf(
hasAction(Intent.ACTION_GET_CONTENT),
hasType("*/*"),
hasCategories(setOf(Intent.CATEGORY_OPENABLE)),
not(hasExtraWithKey(Intent.EXTRA_MIME_TYPES))))
not(hasExtraWithKey(Intent.EXTRA_MIME_TYPES))
)
)
}
}
\ No newline at end of file
package chat.rocket.android.analytics
import chat.rocket.android.analytics.event.AuthenticationEvent
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.analytics.event.SubscriptionTypeEvent
class AnswersAnalytics : Analytics {
override fun logLogin(event: AuthenticationEvent, loginSucceeded: Boolean) {
// Do absolutely nothing
}
override fun logSignUp(event: AuthenticationEvent, signUpSucceeded: Boolean) {
// Do absolutely nothing
}
override fun logScreenView(event: ScreenViewEvent) {
// Do absolutely nothing
}
override fun logMessageSent(event: SubscriptionTypeEvent, serverUrl: String) {
// Do absolutely nothing
}
override fun logMediaUploaded(event: SubscriptionTypeEvent, mimeType: String) {
// Do absolutely nothing
}
override fun logReaction(event: SubscriptionTypeEvent) {
// Do absolutely nothing
}
override fun logServerSwitch(serverUrl: String, serverCount: Int) {
// Do absolutely nothing
}
}
package chat.rocket.android.analytics
import android.content.Context
import chat.rocket.android.analytics.event.AuthenticationEvent
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.analytics.event.SubscriptionTypeEvent
import javax.inject.Inject
class GoogleAnalyticsForFirebase @Inject constructor(val context: Context) :
Analytics {
override fun logLogin(event: AuthenticationEvent, loginSucceeded: Boolean) {
// Do absolutely nothing
}
override fun logSignUp(event: AuthenticationEvent, signUpSucceeded: Boolean) {
// Do absolutely nothing
}
override fun logScreenView(event: ScreenViewEvent) {
// Do absolutely nothing
}
override fun logMessageSent(event: SubscriptionTypeEvent, serverUrl: String) {
// Do absolutely nothing
}
override fun logMediaUploaded(event: SubscriptionTypeEvent, mimeType: String) {
// Do absolutely nothing
}
override fun logReaction(event: SubscriptionTypeEvent) {
// Do absolutely nothing
}
override fun logServerSwitch(serverUrl: String, serverCount: Int) {
// Do absolutely nothing
}
}
package chat.rocket.android.dagger.module
import chat.rocket.android.chatroom.di.MessageServiceProvider
import chat.rocket.android.chatroom.service.MessageService
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class ServiceBuilder {
@ContributesAndroidInjector(modules = [MessageServiceProvider::class])
abstract fun bindMessageService(): MessageService
}
package chat.rocket.android.helper
import android.app.Activity
import android.content.Intent
import androidx.fragment.app.FragmentActivity
fun FragmentActivity.saveCredentials(id: String, password: String) {
}
fun Activity.requestStoredCredentials(): Pair<String, String>? = null
fun getCredentials(data: Intent): Pair<String, String>? = null
fun hasCredentialsSupport() = false
\ No newline at end of file
package chat.rocket.android.helper
import timber.log.Timber
import android.util.Log
class CrashlyticsTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) {
Log.println(priority, tag, message)
if (throwable != null) {
Log.e(tag,throwable.toString())
}
}
}
package chat.rocket.android.push
fun refreshPushToken() {
}
package chat.rocket.android.util
import android.content.Context
fun setupFabric(context: Context) {
//Do absolutely nothing
}
package chat.rocket.android.util
fun invalidateFirebaseToken(token: String) {
//Do absolutely nothing
}
\ No newline at end of file
......@@ -95,23 +95,6 @@
</intent-filter>
</receiver>
<service
android:name=".push.FirebaseTokenService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<service
android:name=".push.FirebaseMessagingService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service
android:name=".chatroom.service.MessageService"
android:exported="true"
......@@ -122,4 +105,4 @@
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
</application>
</manifest>
\ No newline at end of file
</manifest>
app/src/main/ic_launcher-web.png

39.1 KB | W: | H:

app/src/main/ic_launcher-web.png

63.8 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
package chat.rocket.android.about.di
import chat.rocket.android.about.ui.AboutFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class AboutFragmentProvider {
@ContributesAndroidInjector()
abstract fun provideAboutFragment(): AboutFragment
}
......@@ -7,43 +7,56 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.main.ui.MainActivity
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.fragment_about.*
import javax.inject.Inject
internal const val TAG_ABOUT_FRAGMENT = "AboutFragment"
class AboutFragment : Fragment() {
@Inject
lateinit var analyticsManager: AnalyticsManager
companion object {
fun newInstance() = AboutFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_about, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
setupViews()
analyticsManager.logScreenView(ScreenViewEvent.About)
}
private fun setupViews() {
text_version_name.text = getString(R.string.msg_version, BuildConfig.VERSION_NAME)
text_build_number.text = getString(R.string.msg_build, BuildConfig.VERSION_CODE)
text_version_name.text = BuildConfig.VERSION_NAME
text_build_number.text = getString(
R.string.msg_build, BuildConfig.VERSION_CODE,
BuildConfig.GIT_SHA, BuildConfig.FLAVOR
)
}
private fun setupToolbar() {
val toolbar = (activity as MainActivity).toolbar
toolbar.title = getString(R.string.title_about)
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
toolbar.setNavigationOnClickListener {
this.activity?.onBackPressed()
with((activity as MainActivity).toolbar) {
title = getString(R.string.title_about)
setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
setNavigationOnClickListener { activity?.onBackPressed() }
}
}
override fun onStop() {
super.onStop()
(activity as MainActivity).toolbar.setNavigationIcon(R.drawable.ic_menu_white_24dp)
companion object {
fun newInstance() = AboutFragment()
}
}
package chat.rocket.android.analytics
import chat.rocket.android.analytics.event.AuthenticationEvent
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.analytics.event.SubscriptionTypeEvent
interface Analytics {
/**
* Logs the login event.
*
* @param event The [AuthenticationEvent] used to log in.
* @param loginSucceeded True if successful logged in, false otherwise.
*/
fun logLogin(event: AuthenticationEvent, loginSucceeded: Boolean)
/**
* Logs the sign up event.
*
* @param event The [AuthenticationEvent] used to sign up.
* @param signUpSucceeded True if successful signed up, false otherwise.
*/
fun logSignUp(event: AuthenticationEvent, signUpSucceeded: Boolean)
/**
* Logs the screen view event.
*
* @param event The [ScreenViewEvent] to log.
*/
fun logScreenView(event: ScreenViewEvent)
/**
* Logs the message sent event.
*
* @param event The [SubscriptionTypeEvent] to log.
* @param serverUrl The server URL to log.
*/
fun logMessageSent(event: SubscriptionTypeEvent, serverUrl: String)
/**
* Logs the media upload event.
*
* @param event The [SubscriptionTypeEvent] to log.
* @param mimeType The mime type of the media uploaded to log.
*/
fun logMediaUploaded(event: SubscriptionTypeEvent, mimeType: String)
/**
* Logs the reaction event.
*
* @param event The [SubscriptionTypeEvent] to log.
*/
fun logReaction(event: SubscriptionTypeEvent)
/**
* Logs the server switch event.
*
* @param serverUrl The server URL to log.
* @param serverCount The number of server(s) the use own.
*/
fun logServerSwitch(serverUrl: String, serverCount: Int)
}
package chat.rocket.android.analytics
import chat.rocket.android.analytics.event.AuthenticationEvent
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.analytics.event.SubscriptionTypeEvent
import chat.rocket.android.server.domain.AnalyticsTrackingInteractor
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import javax.inject.Inject
class AnalyticsManager @Inject constructor(
private val analyticsTrackingInteractor: AnalyticsTrackingInteractor,
getCurrentServerInteractor: GetCurrentServerInteractor,
getAccountsInteractor: GetAccountsInteractor,
private val analytics: List<Analytics>
) {
val serverUrl = getCurrentServerInteractor.get()
val accounts = getAccountsInteractor.get()
fun logLogin(
event: AuthenticationEvent,
loginSucceeded: Boolean
) {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logLogin(event, loginSucceeded) }
}
}
fun logSignUp(
event: AuthenticationEvent,
signUpSucceeded: Boolean
) {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logSignUp(event, signUpSucceeded) }
}
}
fun logScreenView(event: ScreenViewEvent) {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logScreenView(event) }
}
}
fun logMessageSent(event: SubscriptionTypeEvent) {
if (analyticsTrackingInteractor.get() && serverUrl != null) {
analytics.forEach { it.logMessageSent(event, serverUrl) }
}
}
fun logMediaUploaded(event: SubscriptionTypeEvent, mimeType: String) {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logMediaUploaded(event, mimeType) }
}
}
fun logReaction(event: SubscriptionTypeEvent) {
if (analyticsTrackingInteractor.get()) {
analytics.forEach { it.logReaction(event) }
}
}
fun logServerSwitch() {
if (analyticsTrackingInteractor.get() && serverUrl != null) {
analytics.forEach { it.logServerSwitch(serverUrl, accounts.size) }
}
}
}
package chat.rocket.android.analytics.event
sealed class AuthenticationEvent(val methodName: String) {
object AuthenticationWithUserAndPassword : AuthenticationEvent("User and password")
object AuthenticationWithCas : AuthenticationEvent("CAS")
object AuthenticationWithSaml : AuthenticationEvent("SAML")
object AuthenticationWithOauth : AuthenticationEvent("Oauth")
object AuthenticationWithDeeplink : AuthenticationEvent("Deep link")
}
package chat.rocket.android.analytics.event
sealed class ScreenViewEvent(val screenName: String) {
object About : ScreenViewEvent("AboutFragment")
object ChatRoom : ScreenViewEvent("ChatRoomFragment")
object ChatRooms : ScreenViewEvent("ChatRoomsFragment")
object CreateChannel : ScreenViewEvent("CreateChannelFragment")
object FavoriteMessages : ScreenViewEvent("FavoriteMessagesFragment")
object Files : ScreenViewEvent("FilesFragment")
object Login : ScreenViewEvent("LoginFragment")
object MemberBottomSheet : ScreenViewEvent("MemberBottomSheetFragment")
object Members : ScreenViewEvent("MembersFragment")
object Mentions : ScreenViewEvent("MentionsFragment")
object MessageInfo : ScreenViewEvent("MessageInfoFragment")
object Password : ScreenViewEvent("PasswordFragment")
object PinnedMessages : ScreenViewEvent("PinnedMessagesFragment")
object Preferences : ScreenViewEvent("PreferencesFragment")
object Profile : ScreenViewEvent("ProfileFragment")
object RegisterUsername : ScreenViewEvent("RegisterUsernameFragment")
object ResetPassword : ScreenViewEvent("ResetPasswordFragment")
object Server : ScreenViewEvent("ServerFragment")
object Settings : ScreenViewEvent("SettingsFragment")
object SignUp : ScreenViewEvent("SignupFragment")
object TwoFa : ScreenViewEvent("TwoFAFragment")
}
package chat.rocket.android.analytics.event
sealed class SubscriptionTypeEvent(val subscriptionTypeName: String) {
object DirectMessage : SubscriptionTypeEvent("Direct Message")
object Channel : SubscriptionTypeEvent("Channel")
object Group : SubscriptionTypeEvent("Group")
}
......@@ -22,6 +22,9 @@ class AppLifecycleObserver @Inject constructor(
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onEnterForeground() {
changeTemporaryStatus(UserStatus.Online())
serverInteractor.get()?.let { currentServer ->
factory.create(currentServer).resetReconnectionTimer()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
......
......@@ -80,7 +80,7 @@ private fun formatLocalDateTime(localDateTime: LocalDateTime): String {
}
private fun formatLocalDate(localDate: LocalDate): String {
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
val formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
return localDate.format(formatter).toString()
}
......
......@@ -8,36 +8,42 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Worker
import chat.rocket.android.BuildConfig
import chat.rocket.android.dagger.DaggerAppComponent
import chat.rocket.android.dagger.injector.HasWorkerInjector
import chat.rocket.android.dagger.qualifier.ForMessages
import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.Fitzpatrick
import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.installCrashlyticsWrapper
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.SITE_URL
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.emoji.EmojiRepository
import com.crashlytics.android.Crashlytics
import com.crashlytics.android.core.CrashlyticsCore
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.android.util.setupFabric
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.getCustomEmojis
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import com.jakewharton.threetenabp.AndroidThreeTen
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasActivityInjector
import dagger.android.HasBroadcastReceiverInjector
import dagger.android.HasServiceInjector
import io.fabric.sdk.android.Fabric
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject
class RocketChatApplication : Application(), HasActivityInjector, HasServiceInjector,
HasBroadcastReceiverInjector {
HasBroadcastReceiverInjector, HasWorkerInjector {
@Inject
lateinit var appLifecycleObserver: AppLifecycleObserver
......@@ -51,6 +57,9 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
@Inject
lateinit var broadcastReceiverInjector: DispatchingAndroidInjector<BroadcastReceiver>
@Inject
lateinit var workerInjector: DispatchingAndroidInjector<Worker>
@Inject
lateinit var imagePipelineConfig: ImagePipelineConfig
@Inject
......@@ -67,6 +76,8 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
lateinit var localRepository: LocalRepository
@Inject
lateinit var accountRepository: AccountsRepository
@Inject
lateinit var factory: RocketChatClientFactory
@Inject
@field:ForMessages
......@@ -87,9 +98,8 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
context = WeakReference(applicationContext)
AndroidThreeTen.init(this)
EmojiRepository.load(this)
setupCrashlytics()
setupFabric(this)
setupFresco()
setupTimber()
......@@ -103,6 +113,9 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
// TODO - remove REALM files.
// TODO - remove this
checkCurrentServer()
// TODO - FIXME - we need to proper inject the EmojiRepository and initialize it properly
loadEmojis()
}
private fun checkCurrentServer() {
......@@ -125,15 +138,6 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
}
}
private fun setupCrashlytics() {
val core = CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()
Fabric.with(this, Crashlytics.Builder().core(core).build())
installCrashlyticsWrapper(this@RocketChatApplication,
getCurrentServerInteractor, settingsInteractor,
accountRepository, localRepository)
}
private fun setupFresco() {
Fresco.initialize(this, imagePipelineConfig, draweeConfig)
}
......@@ -146,17 +150,13 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
}
}
override fun activityInjector(): AndroidInjector<Activity> {
return activityDispatchingAndroidInjector
}
override fun activityInjector() = activityDispatchingAndroidInjector
override fun serviceInjector(): AndroidInjector<Service> {
return serviceDispatchingAndroidInjector
}
override fun serviceInjector() = serviceDispatchingAndroidInjector
override fun broadcastReceiverInjector(): AndroidInjector<BroadcastReceiver> {
return broadcastReceiverInjector
}
override fun broadcastReceiverInjector() = broadcastReceiverInjector
override fun workerInjector() = workerInjector
companion object {
var context: WeakReference<Context>? = null
......@@ -164,9 +164,47 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
return context?.get()
}
}
// TODO - FIXME - This is a big Workaround
/**
* Load all emojis for the current server. Simple emojis are always the same for every server,
* but custom emojis vary according to the its url.
*/
fun loadEmojis() {
EmojiRepository.init(this)
val currentServer = getCurrentServerInteractor.get()
currentServer?.let { server ->
launch {
val client = factory.create(server)
EmojiRepository.setCurrentServerUrl(server)
val customEmojiList = mutableListOf<Emoji>()
try {
for (customEmoji in retryIO("getCustomEmojis()") { client.getCustomEmojis() }) {
customEmojiList.add(Emoji(
shortname = ":${customEmoji.name}:",
category = EmojiCategory.CUSTOM.name,
url = "$currentServer/emoji-custom/${customEmoji.name}.${customEmoji.extension}",
count = 0,
fitzpatrick = Fitzpatrick.Default.type,
keywords = customEmoji.aliases,
shortnameAlternates = customEmoji.aliases,
siblings = mutableListOf(),
unicode = "",
isDefault = true
))
}
EmojiRepository.load(this@RocketChatApplication, customEmojis = customEmojiList)
} catch (ex: RocketChatException) {
Timber.e(ex)
EmojiRepository.load(this@RocketChatApplication as Context)
}
}
}
}
}
private fun LocalRepository.needOldMessagesCleanUp() = getBoolean(CLEANUP_OLD_MESSAGES_NEEDED, true)
private fun LocalRepository.setOldMessagesCleanedUp() = save(CLEANUP_OLD_MESSAGES_NEEDED, false)
private const val CLEANUP_OLD_MESSAGES_NEEDED = "CLEANUP_OLD_MESSAGES_NEEDED"
\ No newline at end of file
private const val CLEANUP_OLD_MESSAGES_NEEDED = "CLEANUP_OLD_MESSAGES_NEEDED"
package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.AuthenticationEvent
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.OauthHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.GetConnectingServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
......@@ -23,12 +24,12 @@ import chat.rocket.android.server.domain.isGoogleAuthenticationEnabled
import chat.rocket.android.server.domain.isLdapAuthenticationEnabled
import chat.rocket.android.server.domain.isLinkedinAuthenticationEnabled
import chat.rocket.android.server.domain.isLoginFormEnabled
import chat.rocket.android.server.domain.isWordpressAuthenticationEnabled
import chat.rocket.android.server.domain.isPasswordResetEnabled
import chat.rocket.android.server.domain.isRegistrationEnabledForNewUsers
import chat.rocket.android.server.domain.wordpressUrl
import chat.rocket.android.server.domain.isWordpressAuthenticationEnabled
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.domain.wideTile
import chat.rocket.android.server.domain.wordpressUrl
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.avatarUrl
......@@ -37,7 +38,6 @@ import chat.rocket.android.util.extensions.encodeToBase64
import chat.rocket.android.util.extensions.generateRandomString
import chat.rocket.android.util.extensions.isEmail
import chat.rocket.android.util.extensions.parseColor
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.samlUrl
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO
......@@ -80,8 +80,8 @@ class LoginPresenter @Inject constructor(
private val navigator: AuthenticationNavigator,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val getAccountsInteractor: GetAccountsInteractor,
private val settingsInteractor: GetSettingsInteractor,
private val analyticsManager: AnalyticsManager,
serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServer: SaveCurrentServerInteractor,
private val saveAccountInteractor: SaveAccountInteractor,
......@@ -97,6 +97,7 @@ class LoginPresenter @Inject constructor(
private lateinit var credentialSecret: String
private lateinit var deepLinkUserId: String
private lateinit var deepLinkToken: String
private lateinit var loginMethod: AuthenticationEvent
fun setupView() {
setupConnectionInfo(currentServer)
......@@ -118,6 +119,7 @@ class LoginPresenter @Inject constructor(
else -> {
this.usernameOrEmail = usernameOrEmail
this.password = password
loginMethod = AuthenticationEvent.AuthenticationWithUserAndPassword
doAuthentication(TYPE_LOGIN_USER_EMAIL)
}
}
......@@ -125,17 +127,20 @@ class LoginPresenter @Inject constructor(
fun authenticateWithCas(casToken: String) {
credentialToken = casToken
loginMethod = AuthenticationEvent.AuthenticationWithCas
doAuthentication(TYPE_LOGIN_CAS)
}
fun authenticateWithSaml(samlToken: String) {
credentialToken = samlToken
loginMethod = AuthenticationEvent.AuthenticationWithSaml
doAuthentication(TYPE_LOGIN_SAML)
}
fun authenticateWithOauth(oauthToken: String, oauthSecret: String) {
credentialToken = oauthToken
credentialSecret = oauthSecret
loginMethod = AuthenticationEvent.AuthenticationWithOauth
doAuthentication(TYPE_LOGIN_OAUTH)
}
......@@ -146,6 +151,7 @@ class LoginPresenter @Inject constructor(
deepLinkUserId = deepLinkInfo.userId
deepLinkToken = deepLinkInfo.token
tokenRepository.save(serverUrl, Token(deepLinkUserId, deepLinkToken))
loginMethod = AuthenticationEvent.AuthenticationWithDeeplink
doAuthentication(TYPE_LOGIN_DEEP_LINK)
} else {
// If we don't have the login credentials, just go through normal setup and user input.
......@@ -446,7 +452,7 @@ class LoginPresenter @Inject constructor(
if (myself.id == deepLinkUserId) {
Token(deepLinkUserId, deepLinkToken)
} else {
throw RocketChatAuthException("Invalid Authentication Deep Link Credentials...")
throw RocketChatAuthException("Invalid AuthenticationEvent Deep Link Credentials...")
}
}
else -> {
......@@ -465,11 +471,12 @@ class LoginPresenter @Inject constructor(
username = myself.username,
utcOffset = myself.utcOffset
)
localRepository.saveCurrentUser(url = currentServer, user = user)
localRepository.saveCurrentUser(currentServer, user)
saveCurrentServer.save(currentServer)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, myself.username)
saveAccount(myself.username!!)
saveToken(token)
registerPushToken()
analyticsManager.logLogin(loginMethod, true)
if (loginType == TYPE_LOGIN_USER_EMAIL) {
view.saveSmartLockCredentials(usernameOrEmail, password)
}
......@@ -483,6 +490,7 @@ class LoginPresenter @Inject constructor(
navigator.toTwoFA(usernameOrEmail, password)
}
else -> {
analyticsManager.logLogin(loginMethod, false)
exception.message?.let {
view.showMessage(it)
}.ifNull {
......@@ -621,12 +629,4 @@ class LoginPresenter @Inject constructor(
private fun saveToken(token: Token) {
tokenRepository.save(currentServer, token)
}
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// 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
......@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import com.google.android.gms.auth.api.credentials.Credential
interface LoginView : LoadingView, MessageView {
......
......@@ -6,7 +6,6 @@ import android.content.Intent
import android.graphics.PorterDuff
import android.os.Build
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.text.style.ClickableSpan
import android.view.LayoutInflater
import android.view.View
......@@ -18,23 +17,39 @@ import android.widget.LinearLayout
import android.widget.ScrollView
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.fragment.app.Fragment
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.login.presentation.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView
import chat.rocket.android.helper.*
import chat.rocket.android.util.extensions.*
import chat.rocket.android.webview.sso.ui.INTENT_SSO_TOKEN
import chat.rocket.android.webview.sso.ui.ssoWebViewIntent
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.helper.getCredentials
import chat.rocket.android.helper.hasCredentialsSupport
import chat.rocket.android.helper.requestStoredCredentials
import chat.rocket.android.helper.saveCredentials
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.shake
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.extensions.vibrateSmartPhone
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_SECRET
import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN
import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent
import chat.rocket.android.webview.sso.ui.INTENT_SSO_TOKEN
import chat.rocket.android.webview.sso.ui.ssoWebViewIntent
import chat.rocket.common.util.ifNull
import com.google.android.gms.auth.api.credentials.*
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject
internal const val TAG_LOGIN_FRAGMENT = "LoginFragment"
internal const val REQUEST_CODE_FOR_SIGN_IN_REQUIRED = 1
internal const val REQUEST_CODE_FOR_MULTIPLE_ACCOUNTS_RESOLUTION = 2
internal const val REQUEST_CODE_FOR_SAVE_RESOLUTION = 3
internal const val REQUEST_CODE_FOR_CAS = 4
internal const val REQUEST_CODE_FOR_SAML = 5
internal const val REQUEST_CODE_FOR_OAUTH = 6
......@@ -42,13 +57,14 @@ internal const val REQUEST_CODE_FOR_OAUTH = 6
class LoginFragment : Fragment(), LoginView {
@Inject
lateinit var presenter: LoginPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
private var isOauthViewEnable = false
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
areLoginOptionsNeeded()
}
private var isGlobalLayoutListenerSetUp = false
private var deepLinkInfo: LoginDeepLinkInfo? = null
private val credentialsClient by lazy { Credentials.getClient(requireActivity()) }
companion object {
private const val DEEP_LINK_INFO = "DeepLinkInfo"
......@@ -85,6 +101,12 @@ class LoginFragment : Fragment(), LoginView {
}.ifNull {
presenter.setupView()
}
if (!hasCredentialsSupport()) {
image_key.isVisible = false
}
analyticsManager.logScreenView(ScreenViewEvent.Login)
}
override fun onDestroyView() {
......@@ -100,13 +122,15 @@ class LoginFragment : Fragment(), LoginView {
if (data != null) {
when (requestCode) {
REQUEST_CODE_FOR_MULTIPLE_ACCOUNTS_RESOLUTION -> {
onCredentialRetrieved(data.getParcelableExtra(Credential.EXTRA_KEY))
getCredentials(data)?.let {
onCredentialRetrieved(it.first, it.second)
}
}
REQUEST_CODE_FOR_SIGN_IN_REQUIRED -> {
//use the hints to autofill sign in forms to reduce the info to be filled.
val credential: Credential = data.getParcelableExtra(Credential.EXTRA_KEY)
text_username_or_email.setText(credential.id)
text_password.setText(credential.password)
getCredentials(data)?.let { credential ->
text_username_or_email.setText(credential.first)
text_password.setText(credential.second)
}
}
REQUEST_CODE_FOR_SAVE_RESOLUTION -> {
showMessage(getString(R.string.message_credentials_saved_successfully))
......@@ -154,19 +178,19 @@ class LoginFragment : Fragment(), LoginView {
private fun requestStoredCredentials() {
activity?.let {
SmartLockHelper.requestStoredCredentials(credentialsClient, it)?.let {
onCredentialRetrieved(it)
it.requestStoredCredentials()?.let { credentials ->
onCredentialRetrieved(credentials.first, credentials.second)
}
}
}
private fun onCredentialRetrieved(credential: Credential) {
presenter.authenticateWithUserAndPassword(credential.id, credential.password.toString())
private fun onCredentialRetrieved(id: String, password: String) {
presenter.authenticateWithUserAndPassword(id, password)
}
override fun saveSmartLockCredentials(id: String, password: String) {
activity?.let {
SmartLockHelper.save(credentialsClient, it, id, password)
it.saveCredentials(id, password)
}
}
......
......@@ -4,9 +4,14 @@ import android.content.Intent
import chat.rocket.android.R
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.login.ui.LoginFragment
import chat.rocket.android.authentication.login.ui.TAG_LOGIN_FRAGMENT
import chat.rocket.android.authentication.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.authentication.registerusername.ui.TAG_REGISTER_USERNAME_FRAGMENT
import chat.rocket.android.authentication.resetpassword.ui.ResetPasswordFragment
import chat.rocket.android.authentication.resetpassword.ui.TAG_RESET_PASSWORD_FRAGMENT
import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.authentication.signup.ui.TAG_SIGNUP_FRAGMENT
import chat.rocket.android.authentication.twofactor.ui.TAG_TWO_FA_FRAGMENT
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.authentication.ui.newServerIntent
......@@ -19,13 +24,13 @@ import chat.rocket.android.webview.ui.webViewIntent
class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
fun toLogin() {
activity.addFragmentBackStack("LoginFragment", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_LOGIN_FRAGMENT, R.id.fragment_container) {
LoginFragment.newInstance()
}
}
fun toLogin(deepLinkInfo: LoginDeepLinkInfo) {
activity.addFragmentBackStack("LoginFragment", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_LOGIN_FRAGMENT, R.id.fragment_container) {
LoginFragment.newInstance(deepLinkInfo)
}
}
......@@ -35,19 +40,19 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
}
fun toTwoFA(username: String, password: String) {
activity.addFragmentBackStack("TwoFAFragment", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_TWO_FA_FRAGMENT, R.id.fragment_container) {
TwoFAFragment.newInstance(username, password)
}
}
fun toSignUp() {
activity.addFragmentBackStack("SignupFragment", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_SIGNUP_FRAGMENT, R.id.fragment_container) {
SignupFragment.newInstance()
}
}
fun toForgotPassword() {
activity.addFragmentBackStack("ResetPasswordFragment", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_RESET_PASSWORD_FRAGMENT, R.id.fragment_container) {
ResetPasswordFragment.newInstance()
}
}
......@@ -58,7 +63,7 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
}
fun toRegisterUsername(userId: String, authToken: String) {
activity.addFragmentBackStack("RegisterUsernameFragment", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_REGISTER_USERNAME_FRAGMENT, R.id.fragment_container) {
RegisterUsernameFragment.newInstance(userId, authToken)
}
}
......
package chat.rocket.android.authentication.registerusername.presentation
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.AuthenticationEvent
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.GetConnectingServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SaveAccountInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.favicon
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.domain.wideTile
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
......@@ -23,10 +30,9 @@ class RegisterUsernamePresenter @Inject constructor(
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val factory: RocketChatClientFactory,
factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val analyticsManager: AnalyticsManager,
serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServer: SaveCurrentServerInteractor,
settingsInteractor: GetSettingsInteractor
......@@ -50,10 +56,14 @@ class RegisterUsernamePresenter @Inject constructor(
saveAccount(registeredUsername)
saveCurrentServer.save(currentServer)
tokenRepository.save(currentServer, Token(userId, authToken))
registerPushToken()
analyticsManager.logSignUp(
AuthenticationEvent.AuthenticationWithOauth,
true
)
navigator.toChatList()
}
} catch (exception: RocketChatException) {
analyticsManager.logSignUp(AuthenticationEvent.AuthenticationWithOauth, false)
exception.message?.let {
view.showMessage(it)
}.ifNull {
......@@ -66,14 +76,6 @@ class RegisterUsernamePresenter @Inject constructor(
}
}
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// 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(username: String) {
val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it)
......
......@@ -3,21 +3,34 @@ package chat.rocket.android.authentication.registerusername.ui
import DrawableHelper
import android.os.Build
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernamePresenter
import chat.rocket.android.authentication.registerusername.presentation.RegisterUsernameView
import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.shake
import chat.rocket.android.util.extensions.showKeyboard
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.extensions.vibrateSmartPhone
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_register_username.*
import javax.inject.Inject
internal const val TAG_REGISTER_USERNAME_FRAGMENT = "RegisterUsernameFragment"
class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
@Inject
lateinit var presenter: RegisterUsernamePresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
private lateinit var userId: String
private lateinit var authToken: String
......@@ -61,6 +74,8 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
}
setupOnClickListener()
analyticsManager.logScreenView(ScreenViewEvent.RegisterUsername)
}
override fun alertBlankUsername() {
......
......@@ -3,26 +3,35 @@ package chat.rocket.android.authentication.resetpassword.ui
import DrawableHelper
import android.os.Build
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordPresenter
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordView
import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.shake
import chat.rocket.android.util.extensions.showKeyboard
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.extensions.vibrateSmartPhone
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_reset_password.*
import javax.inject.Inject
internal const val TAG_RESET_PASSWORD_FRAGMENT = "ResetPasswordFragment"
class ResetPasswordFragment : Fragment(), ResetPasswordView {
@Inject
lateinit var presenter: ResetPasswordPresenter
companion object {
fun newInstance() = ResetPasswordFragment()
}
@Inject
lateinit var analyticsManager: AnalyticsManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -48,6 +57,8 @@ class ResetPasswordFragment : Fragment(), ResetPasswordView {
}
setupOnClickListener()
analyticsManager.logScreenView(ScreenViewEvent.ResetPassword)
}
override fun alertBlankEmail() {
......@@ -131,4 +142,8 @@ class ResetPasswordFragment : Fragment(), ResetPasswordView {
presenter.resetPassword(text_email.textContent)
}
}
companion object {
fun newInstance() = ResetPasswordFragment()
}
}
\ No newline at end of file
......@@ -3,44 +3,46 @@ package chat.rocket.android.authentication.server.ui
import android.app.AlertDialog
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.server.presentation.ServerPresenter
import chat.rocket.android.authentication.server.presentation.ServerView
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.hintContent
import chat.rocket.android.util.extensions.ifEmpty
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.sanitize
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui
import chat.rocket.common.util.ifNull
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_server.*
import okhttp3.HttpUrl
import javax.inject.Inject
internal const val TAG_SERVER_FRAGMENT = "ServerFragment"
class ServerFragment : Fragment(), ServerView {
@Inject
lateinit var presenter: ServerPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
private var deepLinkInfo: LoginDeepLinkInfo? = null
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
text_server_url.isCursorVisible = KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)
}
companion object {
private const val DEEP_LINK_INFO = "DeepLinkInfo"
fun newInstance(deepLinkInfo: LoginDeepLinkInfo?) = ServerFragment().apply {
arguments = Bundle().apply {
putParcelable(DEEP_LINK_INFO, deepLinkInfo)
}
}
}
private var protocol = "https://"
private var ignoreChange = false
......@@ -100,6 +102,8 @@ class ServerFragment : Fragment(), ServerView {
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
analyticsManager.logScreenView(ScreenViewEvent.Server)
}
override fun onDestroyView() {
......@@ -217,4 +221,14 @@ class ServerFragment : Fragment(), ServerView {
}
}
}
companion object {
private const val DEEP_LINK_INFO = "DeepLinkInfo"
fun newInstance(deepLinkInfo: LoginDeepLinkInfo?) = ServerFragment().apply {
arguments = Bundle().apply {
putParcelable(DEEP_LINK_INFO, deepLinkInfo)
}
}
}
}
\ No newline at end of file
package chat.rocket.android.authentication.signup.presentation
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.AuthenticationEvent
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.GetConnectingServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SaveAccountInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.server.domain.favicon
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.domain.wideTile
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.privacyPolicyUrl
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.extensions.termsOfServiceUrl
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.signup
......@@ -25,13 +35,12 @@ class SignupPresenter @Inject constructor(
private val localRepository: LocalRepository,
private val serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val analyticsManager: AnalyticsManager,
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor
) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = settingsInteractor.get(serverInteractor.get()!!)
fun signup(name: String, username: String, password: String, email: String) {
......@@ -65,10 +74,17 @@ class SignupPresenter @Inject constructor(
saveCurrentServerInteractor.save(currentServer)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
saveAccount(me)
registerPushToken()
analyticsManager.logSignUp(
AuthenticationEvent.AuthenticationWithUserAndPassword,
true
)
view.saveSmartLockCredentials(username, password)
navigator.toChatList()
} catch (exception: RocketChatException) {
analyticsManager.logSignUp(
AuthenticationEvent.AuthenticationWithUserAndPassword,
false
)
exception.message?.let {
view.showMessage(it)
}.ifNull {
......@@ -95,14 +111,6 @@ class SignupPresenter @Inject constructor(
}
}
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// 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 {
currentServer.serverLogoUrl(it)
......
......@@ -2,7 +2,6 @@ package chat.rocket.android.authentication.signup.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import com.google.android.gms.auth.api.credentials.Credential
interface SignupView : LoadingView, MessageView {
......
......@@ -5,30 +5,39 @@ import android.app.Activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.text.style.ClickableSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import androidx.fragment.app.Fragment
import chat.rocket.android.R
import chat.rocket.android.R.string.message_credentials_saved_successfully
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.authentication.signup.presentation.SignupPresenter
import chat.rocket.android.authentication.signup.presentation.SignupView
import chat.rocket.android.helper.KeyboardHelper
import chat.rocket.android.helper.SmartLockHelper
import chat.rocket.android.helper.TextHelper
import chat.rocket.android.util.extensions.*
import com.google.android.gms.auth.api.credentials.Credentials
import chat.rocket.android.helper.saveCredentials
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.shake
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.extensions.vibrateSmartPhone
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_sign_up.*
import javax.inject.Inject
internal const val TAG_SIGNUP_FRAGMENT = "SignupFragment"
internal const val SAVE_CREDENTIALS = 1
class SignupFragment : Fragment(), SignupView {
@Inject
lateinit var presenter: SignupPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)) {
bottom_container.setVisible(false)
......@@ -41,10 +50,6 @@ class SignupFragment : Fragment(), SignupView {
}
}
companion object {
fun newInstance() = SignupFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
......@@ -75,6 +80,8 @@ class SignupFragment : Fragment(), SignupView {
text_email.textContent
)
}
analyticsManager.logScreenView(ScreenViewEvent.SignUp)
}
override fun onDestroyView() {
......@@ -155,9 +162,7 @@ class SignupFragment : Fragment(), SignupView {
}
override fun saveSmartLockCredentials(id: String, password: String) {
activity?.let {
SmartLockHelper.save(Credentials.getClient(it), it, id, password)
}
activity?.saveCredentials(id, password)
}
private fun tintEditTextDrawableStart() {
......@@ -216,4 +221,8 @@ class SignupFragment : Fragment(), SignupView {
text_password.isEnabled = value
text_email.isEnabled = value
}
companion object {
fun newInstance() = SignupFragment()
}
}
package chat.rocket.android.authentication.twofactor.presentation
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.AuthenticationEvent
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.domain.GetConnectingServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SaveAccountInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.favicon
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.domain.wideTile
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatAuthException
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.login
import chat.rocket.core.internal.rest.me
import chat.rocket.core.model.Myself
import javax.inject.Inject
class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
settingsInteractor: GetSettingsInteractor) {
class TwoFAPresenter @Inject constructor(
private val view: TwoFAView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
private val tokenRepository: TokenRepository,
private val localRepository: LocalRepository,
private val serverInteractor: GetConnectingServerInteractor,
private val saveCurrentServerInteractor: SaveCurrentServerInteractor,
private val analyticsManager: AnalyticsManager,
private val factory: RocketChatClientFactory,
private val saveAccountInteractor: SaveAccountInteractor,
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)
fun authenticate(usernameOrEmail: String, password: String, twoFactorAuthenticationCode: String) {
fun authenticate(
usernameOrEmail: String,
password: String,
twoFactorAuthenticationCode: String
) {
val server = serverInteractor.get()
when {
server == null -> {
......@@ -58,12 +70,20 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
saveAccount(me)
saveCurrentServerInteractor.save(currentServer)
tokenRepository.save(server, token)
registerPushToken()
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, me.username)
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
true
)
navigator.toChatList()
} catch (exception: RocketChatException) {
if (exception is RocketChatAuthException) {
view.alertInvalidTwoFactorAuthenticationCode()
} else {
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
false
)
exception.message?.let {
view.showMessage(it)
}.ifNull {
......@@ -80,14 +100,6 @@ class TwoFAPresenter @Inject constructor(private val view: TwoFAView,
fun signup() = navigator.toSignUp()
private suspend fun registerPushToken() {
localRepository.get(LocalRepository.KEY_PUSH_TOKEN)?.let {
client.registerPushToken(it, getAccountsInteractor.get(), factory)
}
// 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 {
currentServer.serverLogoUrl(it)
......
......@@ -4,37 +4,37 @@ import DrawableHelper
import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.fragment.app.Fragment
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.authentication.twofactor.presentation.TwoFAPresenter
import chat.rocket.android.authentication.twofactor.presentation.TwoFAView
import chat.rocket.android.util.extensions.*
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.shake
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import chat.rocket.android.util.extensions.ui
import chat.rocket.android.util.extensions.vibrateSmartPhone
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_two_fa.*
import javax.inject.Inject
internal const val TAG_TWO_FA_FRAGMENT = "TwoFAFragment"
class TwoFAFragment : Fragment(), TwoFAView {
@Inject lateinit var presenter: TwoFAPresenter
@Inject
lateinit var presenter: TwoFAPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
lateinit var username: String
lateinit var password: String
// TODO - we could create an in memory repository to save username and password.
companion object {
private const val USERNAME = "username"
private const val PASSWORD = "password"
fun newInstance(username: String, password: String) = TwoFAFragment().apply {
arguments = Bundle(2).apply {
putString(USERNAME, username)
putString(PASSWORD, password)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
......@@ -44,7 +44,11 @@ class TwoFAFragment : Fragment(), TwoFAView {
password = arguments?.getString(PASSWORD) ?: ""
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_two_fa)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_authentication_two_fa)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -60,6 +64,8 @@ class TwoFAFragment : Fragment(), TwoFAView {
}
setupOnClickListener()
analyticsManager.logScreenView(ScreenViewEvent.TwoFa)
}
override fun alertBlankTwoFactorAuthenticationCode() {
......@@ -103,7 +109,8 @@ class TwoFAFragment : Fragment(), TwoFAView {
private fun tintEditTextDrawableStart() {
ui {
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, it)
val lockDrawable =
DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, it)
DrawableHelper.wrapDrawable(lockDrawable)
DrawableHelper.tintDrawable(lockDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_two_factor_auth, lockDrawable)
......@@ -120,4 +127,17 @@ class TwoFAFragment : Fragment(), TwoFAView {
presenter.authenticate(username, password, text_two_factor_auth.textContent)
}
}
// TODO - we could create an in memory repository to save username and password.
companion object {
private const val USERNAME = "username"
private const val PASSWORD = "password"
fun newInstance(username: String, password: String) = TwoFAFragment().apply {
arguments = Bundle(2).apply {
putString(USERNAME, username)
putString(PASSWORD, password)
}
}
}
}
......@@ -10,6 +10,7 @@ import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.domain.model.getLoginDeepLinkInfo
import chat.rocket.android.authentication.presentation.AuthenticationPresenter
import chat.rocket.android.authentication.server.ui.ServerFragment
import chat.rocket.android.authentication.server.ui.TAG_SERVER_FRAGMENT
import chat.rocket.android.util.extensions.addFragment
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
......@@ -64,7 +65,7 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
}
fun showServerInput(deepLinkInfo: LoginDeepLinkInfo?) {
addFragment("ServerFragment", R.id.fragment_container, allowStateLoss = true) {
addFragment(TAG_SERVER_FRAGMENT, R.id.fragment_container, allowStateLoss = true) {
ServerFragment.newInstance(deepLinkInfo)
}
}
......
......@@ -6,7 +6,6 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import chat.rocket.android.R
import chat.rocket.android.chatinformation.ui.MessageInfoFragment.Companion.TAG_MESSAGE_INFO_FRAGMENT
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import dagger.android.AndroidInjection
......
......@@ -9,14 +9,14 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.chatinformation.adapter.ReadReceiptAdapter
import chat.rocket.android.chatinformation.presentation.MessageInfoPresenter
import chat.rocket.android.chatinformation.presentation.MessageInfoView
import chat.rocket.android.chatinformation.viewmodel.ReadReceiptViewModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.core.model.ReadReceipt
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_message_info.*
import javax.inject.Inject
......@@ -29,13 +29,14 @@ fun newInstance(messageId: String): Fragment {
}
}
internal const val TAG_MESSAGE_INFO_FRAGMENT = "MessageInfoFragment"
private const val BUNDLE_MESSAGE_ID = "message_id"
class MessageInfoFragment : Fragment(), MessageInfoView {
@Inject
lateinit var presenter: MessageInfoPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
private lateinit var adapter: ReadReceiptAdapter
private lateinit var messageId: String
......@@ -64,6 +65,8 @@ class MessageInfoFragment : Fragment(), MessageInfoView {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
presenter.loadReadReceipts(messageId = messageId)
analyticsManager.logScreenView(ScreenViewEvent.MessageInfo)
}
private fun setupRecyclerView() {
......@@ -93,8 +96,4 @@ class MessageInfoFragment : Fragment(), MessageInfoView {
override fun showReadReceipts(messageReceipts: List<ReadReceiptViewModel>) {
adapter.addAll(messageReceipts)
}
companion object {
const val TAG_MESSAGE_INFO_FRAGMENT = "MessageInfoFragment"
}
}
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.uimodel.ActionsAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import kotlinx.android.synthetic.main.item_actions_attachment.view.*
import androidx.recyclerview.widget.LinearLayoutManager
import timber.log.Timber
class ActionsAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
var actionAttachmentOnClickListener: ActionAttachmentOnClickListener
) : BaseViewHolder<ActionsAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(actions_attachment_container)
}
}
override fun bindViews(data: ActionsAttachmentUiModel) {
val actions = data.actions
val alignment = data.buttonAlignment
Timber.d("no of actions : ${actions.size} : $actions")
with(itemView) {
title.text = data.title ?: ""
actions_list.layoutManager = LinearLayoutManager(itemView.context,
when (alignment) {
"horizontal" -> LinearLayoutManager.HORIZONTAL
else -> LinearLayoutManager.VERTICAL //Default
}, false)
actions_list.adapter = ActionsListAdapter(actions, actionAttachmentOnClickListener)
}
}
}
interface ActionAttachmentOnClickListener {
fun onActionClicked(view: View, action: Action)
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.util.extensions.inflate
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import com.facebook.drawee.backends.pipeline.Fresco
import kotlinx.android.synthetic.main.item_action_button.view.*
import timber.log.Timber
class ActionsListAdapter(actions: List<Action>, var actionAttachmentOnClickListener: ActionAttachmentOnClickListener) : RecyclerView.Adapter<ActionsListAdapter.ViewHolder>() {
var actions: List<Action> = actions
inner class ViewHolder(var layout: View) : RecyclerView.ViewHolder(layout) {
lateinit var action: ButtonAction
private val onClickListener = View.OnClickListener {
actionAttachmentOnClickListener.onActionClicked(it, action)
}
init {
with(itemView) {
action_button.setOnClickListener(onClickListener)
action_image_button.setOnClickListener(onClickListener)
}
}
fun bindAction(action: Action) {
with(itemView) {
Timber.d("action : $action")
this@ViewHolder.action = action as ButtonAction
if (action.imageUrl != null) {
action_button.isVisible = false
action_image_button.isVisible = true
//Image button
val controller = Fresco.newDraweeControllerBuilder().apply {
setUri(action.imageUrl)
autoPlayAnimations = true
oldController = action_image_button.controller
}.build()
action_image_button.controller = controller
} else if (action.text != null) {
action_button.isVisible = true
action_image_button.isVisible = false
this.action_button.setText(action.text)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = parent.inflate(R.layout.item_action_button)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return actions.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val action = actions[position]
holder.bindAction(action)
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.app.AlertDialog
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.uimodel.*
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage
import timber.log.Timber
import java.security.InvalidParameterException
class ChatRoomAdapter(
private val roomId: String? = null,
private val roomType: String? = null,
private val roomName: String? = null,
private val actionSelectListener: OnActionSelected? = null,
......@@ -72,6 +74,10 @@ class ChatRoomAdapter(
actionSelectListener?.openDirectMessage(roomName, permalink)
}
}
BaseUiModel.ViewType.ACTIONS_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_actions_attachment)
ActionsAttachmentViewHolder(view, actionsListener, reactionListener, actionAttachmentOnClickListener)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
......@@ -125,6 +131,8 @@ class ChatRoomAdapter(
holder.bind(dataSet[position] as GenericFileAttachmentUiModel)
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyUiModel)
is ActionsAttachmentViewHolder ->
holder.bind(dataSet[position] as ActionsAttachmentUiModel)
}
}
......@@ -150,23 +158,36 @@ class ChatRoomAdapter(
}
fun prependData(dataSet: List<BaseUiModel<*>>) {
val item = dataSet.indexOfFirst { newItem ->
this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1
}
if (item == -1) {
this.dataSet.addAll(0, dataSet)
notifyItemRangeInserted(0, dataSet.size)
} else {
dataSet.forEach { item ->
val index = this.dataSet.indexOfFirst {
item.messageId == it.messageId && item.viewType == it.viewType
}
if (index > -1) {
this.dataSet[index] = item
notifyItemChanged(index)
}
//---At first we will update all already saved elements with received updated ones
val filteredDataSet = dataSet.filter { newItem ->
val matchedIndex = this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType }
if (matchedIndex > -1) {
this.dataSet[matchedIndex] = newItem
notifyItemChanged(matchedIndex)
}
return@filter (matchedIndex < 0)
}
val minAdditionDate = filteredDataSet.minBy { it.message.timestamp } ?: return
//---In the most cases we will just add new elements to the top of messages heap
if (this.dataSet.isEmpty() || minAdditionDate.message.timestamp > this.dataSet[0].message.timestamp) {
this.dataSet.addAll(0, filteredDataSet)
notifyItemRangeInserted(0, filteredDataSet.size)
return
}
//---Else branch: merging messages---
//---We are inserting new received elements into set. Sort them by time+type and show
if (filteredDataSet.isEmpty()) return
this.dataSet.addAll(0, filteredDataSet)
val tmp = this.dataSet.sortedWith(Comparator { t, t2 ->
val timeComparison = t.message.timestamp.compareTo(t2.message.timestamp)
if (timeComparison == 0) {
return@Comparator t.viewType.compareTo(t2.viewType)
}
timeComparison
}).reversed()
this.dataSet.clear()
this.dataSet.addAll(tmp)
notifyDataSetChanged()
}
fun updateItem(message: BaseUiModel<*>) {
......@@ -203,6 +224,33 @@ class ChatRoomAdapter(
}
}
private val actionAttachmentOnClickListener = object : ActionAttachmentOnClickListener {
override fun onActionClicked(view: View, action: Action) {
val temp = action as ButtonAction
if (temp.url != null && temp.isWebView != null) {
if (temp.isWebView == true) {
//TODO: Open in a configurable sizable webview
Timber.d("Open in a configurable sizable webview")
} else {
//Open in chrome custom tab
temp.url?.let { view.openTabbedUrl(it) }
}
} else if (temp.message != null && temp.isMessageInChatWindow != null) {
if (temp.isMessageInChatWindow == true) {
//Send to chat window
temp.message?.let {
if (roomId != null) {
actionSelectListener?.sendMessage(roomId, it)
}
}
} else {
//TODO: Send to bot but not in chat window
Timber.d("Send to bot but not in chat window")
}
}
}
}
private val actionsListener = object : BaseViewHolder.ActionsListener {
override fun isActionsEnabled(): Boolean = enableActions
......@@ -259,5 +307,6 @@ class ChatRoomAdapter(
fun deleteMessage(roomId: String, id: String)
fun showReactions(id: String)
fun openDirectMessage(roomName: String, message: String)
fun sendMessage(chatRoomId: String, text: String)
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.method.LinkMovementMethod
import android.text.style.ImageSpan
import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.MessageUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.core.model.isSystemMessage
import com.bumptech.glide.load.resource.gif.GifDrawable
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
......@@ -15,7 +19,7 @@ class MessageViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener) {
) : BaseViewHolder<MessageUiModel>(itemView, listener, reactionListener), Drawable.Callback {
init {
with(itemView) {
......@@ -26,22 +30,26 @@ class MessageViewHolder(
override fun bindViews(data: MessageUiModel) {
with(itemView) {
day_marker_layout.visibility = if (data.showDayMarker) {
day.text = data.currentDayMarkerText
View.VISIBLE
} else {
View.GONE
}
day.text = data.currentDayMarkerText
day_marker_layout.isVisible = data.showDayMarker
if (data.isFirstUnread) {
new_messages_notif.visibility = View.VISIBLE
} else {
new_messages_notif.visibility = View.GONE
}
new_messages_notif.isVisible = data.isFirstUnread
text_message_time.text = data.time
text_sender.text = data.senderName
text_content.text = data.content
if (data.content is Spannable) {
val spans = data.content.getSpans(0, data.content.length, ImageSpan::class.java)
spans.forEach {
if (it.drawable is GifDrawable) {
it.drawable.callback = this@MessageViewHolder
(it.drawable as GifDrawable).start()
}
}
}
text_content.text_content.text = data.content
image_avatar.setImageURI(data.avatar)
text_content.setTextColor(if (data.isTemporary) Color.GRAY else Color.BLACK)
......@@ -64,4 +72,22 @@ class MessageViewHolder(
}
}
}
override fun unscheduleDrawable(who: Drawable?, what: Runnable?) {
with(itemView) {
text_content.removeCallbacks(what)
}
}
override fun invalidateDrawable(p0: Drawable?) {
with(itemView) {
text_content.invalidate()
}
}
override fun scheduleDrawable(who: Drawable?, what: Runnable?, w: Long) {
with(itemView) {
text_content.postDelayed(what, w)
}
}
}
......@@ -4,37 +4,42 @@ import chat.rocket.android.R
import chat.rocket.android.chatinformation.ui.messageInformationIntent
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.favoritemessages.ui.TAG_FAVORITE_MESSAGES_FRAGMENT
import chat.rocket.android.files.ui.TAG_FILES_FRAGMENT
import chat.rocket.android.members.ui.TAG_MEMBERS_FRAGMENT
import chat.rocket.android.mentions.ui.TAG_MENTIONS_FRAGMENT
import chat.rocket.android.pinnedmessages.ui.TAG_PINNED_MESSAGES_FRAGMENT
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack
class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
fun toMembersList(chatRoomId: String) {
activity.addFragmentBackStack("MembersFragment", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_MEMBERS_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.members.ui.newInstance(chatRoomId)
}
}
fun toMentions(chatRoomId: String) {
activity.addFragmentBackStack("MentionsFragment", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_MENTIONS_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.mentions.ui.newInstance(chatRoomId)
}
}
fun toPinnedMessageList(chatRoomId: String) {
activity.addFragmentBackStack("PinnedMessages", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_PINNED_MESSAGES_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.pinnedmessages.ui.newInstance(chatRoomId)
}
}
fun toFavoriteMessageList(chatRoomId: String) {
activity.addFragmentBackStack("FavoriteMessages", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_FAVORITE_MESSAGES_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.favoritemessages.ui.newInstance(chatRoomId)
}
}
fun toFileList(chatRoomId: String) {
activity.addFragmentBackStack("Files", R.id.fragment_container) {
activity.addFragmentBackStack(TAG_FILES_FRAGMENT, R.id.fragment_container) {
chat.rocket.android.files.ui.newInstance(chatRoomId)
}
}
......
......@@ -148,9 +148,4 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
/**
* Open a DM with the user in the given [chatRoom] and pass the [permalink] for the message
* to reply.
*/
fun openDirectMessage(chatRoom: ChatRoom, permalink: String)
}
......@@ -5,7 +5,6 @@ import android.app.job.JobService
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.sendMessage
import chat.rocket.core.model.Message
import dagger.android.AndroidInjection
......
......@@ -6,8 +6,6 @@ import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.appcompat.app.AppCompatActivity
import android.text.SpannableStringBuilder
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.server.domain.GetCurrentServerInteractor
......@@ -169,8 +167,4 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
super.onBackPressed()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
}
companion object {
const val TAG_CHAT_ROOM_FRAGMENT = "ChatRoomFragment"
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.core.view.isVisible
import chat.rocket.android.emoji.internal.GlideApp
import chat.rocket.android.util.extensions.getFileName
import chat.rocket.android.util.extensions.getMimeType
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
activity?.let { fragmentActivity ->
uri.getMimeType(fragmentActivity).let { mimeType ->
imagePreview.isVisible = false
audioVideoAttachment.isVisible = false
textFile.isVisible = false
var bitmap: Bitmap? = null
activity?.let { context ->
uri.getMimeType(context).let { mimeType ->
description.text.clear()
when {
mimeType.startsWith("image") -> {
imagePreview.isVisible = true
imagePreview.setImageURI(uri)
}
mimeType.startsWith("video") -> {
audioVideoAttachment.isVisible = true
GlideApp
.with(context)
.asBitmap()
.load(uri)
.override(imagePreview.width, imagePreview.height)
.fitCenter()
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
bitmap = resource
imagePreview.setImageBitmap(resource)
imagePreview.isVisible = true
}
})
}
mimeType.startsWith("video") -> audioVideoAttachment.isVisible = true
else -> {
textFile.isVisible = true
textFile.text = uri.getFileName(fragmentActivity)
textFile.text = uri.getFileName(context)
}
}
}
}
sendButton.setOnClickListener {
presenter.uploadFile(chatRoomId, uri, (citation ?: "") + description.text.toString())
presenter.uploadFile(
chatRoomId,
uri,
(citation ?: "") + description.text.toString(),
bitmap
)
alertDialog.dismiss()
}
cancelButton.setOnClickListener { alertDialog.dismiss() }
......
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ActionsAttachment
data class ActionsAttachmentUiModel(
override val attachmentUrl: String,
val title: String?,
val actions: List<Action>,
val buttonAlignment: String,
override val message: Message,
override val rawData: ActionsAttachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseAttachmentUiModel<ActionsAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.ACTIONS_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_actions_attachment
}
\ No newline at end of file
......@@ -29,7 +29,8 @@ interface BaseUiModel<out T> {
AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8),
GENERIC_FILE_ATTACHMENT(9),
MESSAGE_REPLY(10)
MESSAGE_REPLY(10),
ACTIONS_ATTACHMENT(11)
}
}
......
......@@ -46,6 +46,7 @@ import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.MessageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.attachment.actions.ActionsAttachment
import chat.rocket.core.model.isSystemMessage
import chat.rocket.core.model.url.Url
import kotlinx.coroutines.experimental.CommonPool
......@@ -305,10 +306,26 @@ class UiModelMapper @Inject constructor(
is MessageAttachment -> mapMessageAttachment(message, attachment)
is AuthorAttachment -> mapAuthorAttachment(message, attachment)
is ColorAttachment -> mapColorAttachment(message, attachment)
is ActionsAttachment -> mapActionsAttachment(message, attachment)
else -> null
}
}
private fun mapActionsAttachment(message: Message, attachment: ActionsAttachment): BaseUiModel<*>? {
return with(attachment) {
val content = stripMessageQuotes(message)
val localDateTime = DateTimeHelper.getLocalDateTime(message.timestamp)
val dayMarkerText = DateTimeHelper.getFormattedDateForMessages(localDateTime, context)
ActionsAttachmentUiModel(attachmentUrl = url, title = title,
actions = actions, buttonAlignment = buttonAlignment, message = message, rawData = attachment,
messageId = message.id, reactions = getReactions(message),
preview = message.copy(message = content.message), unread = message.unread,
showDayMarker = false, currentDayMarkerText = dayMarkerText)
}
}
private fun mapColorAttachment(message: Message, attachment: ColorAttachment): BaseUiModel<*>? {
return with(attachment) {
val content = stripMessageQuotes(message)
......@@ -493,7 +510,7 @@ class UiModelMapper @Inject constructor(
list.add(
ReactionUiModel(messageId = message.id,
shortname = shortname,
unicode = EmojiParser.parse(shortname),
unicode = EmojiParser.parse(context, shortname),
count = count,
usernames = usernames)
)
......
......@@ -21,6 +21,8 @@ import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.chatrooms.adapter.RoomsAdapter
import chat.rocket.android.chatrooms.presentation.ChatRoomsPresenter
import chat.rocket.android.chatrooms.presentation.ChatRoomsView
......@@ -45,6 +47,8 @@ import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import timber.log.Timber
import javax.inject.Inject
internal const val TAG_CHAT_ROOMS_FRAGMENT = "ChatRoomsFragment"
private const val BUNDLE_CHAT_ROOM_ID = "BUNDLE_CHAT_ROOM_ID"
class ChatRoomsFragment : Fragment(), ChatRoomsView {
......@@ -54,14 +58,12 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
lateinit var factory: ChatRoomsViewModelFactory
@Inject
lateinit var dbManager: DatabaseManager // TODO - remove when moving ChatRoom screen to DB
@Inject
lateinit var analyticsManager: AnalyticsManager
lateinit var viewModel: ChatRoomsViewModel
private var searchView: SearchView? = null
private var sortView: MenuItem? = null
private val handler = Handler()
private var chatRoomId: String? = null
private var progressDialog: ProgressDialog? = null
......@@ -108,6 +110,8 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
subscribeUi()
setupToolbar()
analyticsManager.logScreenView(ScreenViewEvent.ChatRooms)
}
private fun subscribeUi() {
......@@ -117,9 +121,13 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
recycler_view.layoutManager = LinearLayoutManager(it)
recycler_view.addItemDecoration(DividerItemDecoration(it,
recycler_view.addItemDecoration(
DividerItemDecoration(
it,
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_start),
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)))
resources.getDimensionPixelSize(R.dimen.divider_item_decorator_bound_end)
)
)
recycler_view.itemAnimator = DefaultItemAnimator()
recycler_view.adapter = adapter
......@@ -134,7 +142,7 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
})
viewModel.loadingState.observe(viewLifecycleOwner, Observer { state ->
when(state) {
when (state) {
is LoadingState.Loading -> if (state.count == 0L) showLoading()
is LoadingState.Loaded -> {
hideLoading()
......@@ -188,35 +196,47 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
// TODO - simplify this
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 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)
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.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
})
SharedPreferenceHelper.putInt(
Constants.CHATROOM_SORT_TYPE_KEY, when (checkedId) {
R.id.radio_sort_alphabetical -> 0
R.id.radio_sort_activity -> 1
else -> 1
}
)
}
}
groupByTypeCheckBox.isChecked = groupByType
groupByTypeCheckBox.setOnCheckedChangeListener { _, isChecked ->
SharedPreferenceHelper.putBoolean(Constants.CHATROOM_GROUP_BY_TYPE_KEY, isChecked)
SharedPreferenceHelper.putBoolean(
Constants.CHATROOM_GROUP_BY_TYPE_KEY,
isChecked
)
}
AlertDialog.Builder(context)
.setTitle(R.string.dialog_sort_title)
.setView(dialogLayout)
.setPositiveButton("Done") { dialog, _ ->
.setPositiveButton(R.string.dialog_button_done) { dialog, _ ->
invalidateQueryOnSearch()
updateSort()
dialog.dismiss()
......@@ -227,10 +247,13 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
}
private fun updateSort() {
val sortType = SharedPreferenceHelper.getInt(Constants.CHATROOM_SORT_TYPE_KEY, ChatRoomsSortOrder.ACTIVITY)
val sortType = SharedPreferenceHelper.getInt(
Constants.CHATROOM_SORT_TYPE_KEY,
ChatRoomsSortOrder.ACTIVITY
)
val grouped = SharedPreferenceHelper.getBoolean(Constants.CHATROOM_GROUP_BY_TYPE_KEY, false)
val query = when(sortType) {
val query = when (sortType) {
ChatRoomsSortOrder.ALPHABETICAL -> {
Query.ByName(grouped)
}
......@@ -297,11 +320,16 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
text_connection_status.text = getString(R.string.status_connected)
handler.postDelayed(dismissStatus, 2000)
}
is State.Disconnected -> text_connection_status.text = getString(R.string.status_disconnected)
is State.Connecting -> text_connection_status.text = getString(R.string.status_connecting)
is State.Authenticating -> text_connection_status.text = getString(R.string.status_authenticating)
is State.Disconnecting -> text_connection_status.text = getString(R.string.status_disconnecting)
is State.Waiting -> text_connection_status.text = getString(R.string.status_waiting, state.seconds)
is State.Disconnected -> text_connection_status.text =
getString(R.string.status_disconnected)
is State.Connecting -> text_connection_status.text =
getString(R.string.status_connecting)
is State.Authenticating -> text_connection_status.text =
getString(R.string.status_authenticating)
is State.Disconnecting -> text_connection_status.text =
getString(R.string.status_disconnecting)
is State.Waiting -> text_connection_status.text =
getString(R.string.status_waiting, state.seconds)
}
}
}
......
......@@ -17,15 +17,15 @@ import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.rest.spotlight
import chat.rocket.core.model.SpotlightResult
import com.shopify.livedataktx.distinct
import com.shopify.livedataktx.map
import com.shopify.livedataktx.nonNull
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.isActive
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.newSingleThreadContext
import kotlinx.coroutines.experimental.withContext
import me.henrytao.livedataktx.distinct
import me.henrytao.livedataktx.map
import me.henrytao.livedataktx.nonNull
import timber.log.Timber
import java.security.InvalidParameterException
import kotlin.coroutines.experimental.coroutineContext
......
......@@ -15,6 +15,8 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.createchannel.presentation.CreateChannelPresenter
import chat.rocket.android.createchannel.presentation.CreateChannelView
import chat.rocket.android.main.ui.MainActivity
......@@ -34,9 +36,13 @@ import kotlinx.android.synthetic.main.fragment_create_channel.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
internal const val TAG_CREATE_CHANNEL_FRAGMENT = "CreateChannelFragment"
class CreateChannelFragment : Fragment(), CreateChannelView, ActionMode.Callback {
@Inject
lateinit var createChannelPresenter: CreateChannelPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
private var actionMode: ActionMode? = null
private val adapter: MembersAdapter = MembersAdapter {
if (it.username != null) {
......@@ -69,6 +75,8 @@ class CreateChannelFragment : Fragment(), CreateChannelView, ActionMode.Callback
setupViewListeners()
setupRecyclerView()
subscribeEditTexts()
analyticsManager.logScreenView(ScreenViewEvent.CreateChannel)
}
override fun onDestroyView() {
......@@ -161,7 +169,7 @@ class CreateChannelFragment : Fragment(), CreateChannelView, ActionMode.Callback
override fun prepareToShowChatList() {
with(activity as MainActivity) {
setCheckedNavDrawerItem(R.id.action_chat_rooms)
setCheckedNavDrawerItem(R.id.menu_action_chats)
openDrawer()
getDrawerLayout().postDelayed(1000) {
closeDrawer()
......
package chat.rocket.android.dagger
import android.app.Application
import chat.rocket.android.app.AppLifecycleObserver
import chat.rocket.android.app.RocketChatApplication
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.module.ActivityBuilder
import chat.rocket.android.dagger.module.AndroidWorkerInjectionModule
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.push.FirebaseTokenService
import dagger.BindsInstance
import dagger.Component
import dagger.android.support.AndroidSupportInjectionModule
......@@ -16,7 +15,8 @@ import javax.inject.Singleton
@Singleton
@Component(modules = [AndroidSupportInjectionModule::class,
AppModule::class, ActivityBuilder::class, ServiceBuilder::class, ReceiverBuilder::class])
AppModule::class, ActivityBuilder::class, ServiceBuilder::class, ReceiverBuilder::class,
AndroidWorkerInjectionModule::class])
interface AppComponent {
@Component.Builder
......@@ -29,10 +29,5 @@ interface AppComponent {
fun inject(app: RocketChatApplication)
fun inject(service: FirebaseTokenService)
fun inject(service: MessageService)
/*@Component.Builder
abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/
}
package chat.rocket.android.dagger.injector
import androidx.work.Worker
object AndroidWorkerInjection {
fun inject(worker: Worker) {
val application = worker.applicationContext
if (application !is HasWorkerInjector) {
throw RuntimeException("${application.javaClass.canonicalName} does not implement ${HasWorkerInjector::class.java.canonicalName}")
}
val workerInjector = (application as HasWorkerInjector).workerInjector()
checkNotNull(workerInjector) { "${application.javaClass}.workerInjector() return null" }
workerInjector.inject(worker)
}
}
\ No newline at end of file
package chat.rocket.android.dagger.injector
import androidx.work.Worker
import dagger.android.AndroidInjector
interface HasWorkerInjector {
fun workerInjector(): AndroidInjector<Worker>
}
\ No newline at end of file
package chat.rocket.android.dagger.module
import chat.rocket.android.about.di.AboutFragmentProvider
import chat.rocket.android.authentication.di.AuthenticationModule
import chat.rocket.android.authentication.login.di.LoginFragmentProvider
import chat.rocket.android.authentication.registerusername.di.RegisterUsernameFragmentProvider
......@@ -25,6 +26,7 @@ import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.members.di.MembersFragmentProvider
import chat.rocket.android.mentions.di.MentionsFragmentProvider
import chat.rocket.android.pinnedmessages.di.PinnedMessagesFragmentProvider
import chat.rocket.android.preferences.di.PreferencesFragmentProvider
import chat.rocket.android.profile.di.ProfileFragmentProvider
import chat.rocket.android.server.di.ChangeServerModule
import chat.rocket.android.server.ui.ChangeServerActivity
......@@ -56,7 +58,9 @@ abstract class ActivityBuilder {
ChatRoomsFragmentProvider::class,
CreateChannelProvider::class,
ProfileFragmentProvider::class,
SettingsFragmentProvider::class
SettingsFragmentProvider::class,
AboutFragmentProvider::class,
PreferencesFragmentProvider::class
]
)
abstract fun bindMainActivity(): MainActivity
......
package chat.rocket.android.dagger.module
import androidx.work.Worker
import dagger.Module
import dagger.android.AndroidInjector
import dagger.multibindings.Multibinds
@Module
abstract class AndroidWorkerInjectionModule {
@Multibinds
abstract fun workerInjectorFactories(): Map<Class<out Worker>, AndroidInjector.Factory<out Worker>>
}
\ No newline at end of file
......@@ -9,6 +9,10 @@ import android.content.Context
import android.content.SharedPreferences
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.analytics.Analytics
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.AnswersAnalytics
import chat.rocket.android.analytics.GoogleAnalyticsForFirebase
import chat.rocket.android.authentication.infraestructure.SharedPreferencesMultiServerTokenRepository
import chat.rocket.android.authentication.infraestructure.SharedPreferencesTokenRepository
import chat.rocket.android.chatroom.service.MessageService
......@@ -23,9 +27,12 @@ import chat.rocket.android.push.GroupedPush
import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.ActiveUsersRepository
import chat.rocket.android.server.domain.AnalyticsTrackingInteractor
import chat.rocket.android.server.domain.AnalyticsTrackingRepository
import chat.rocket.android.server.domain.ChatRoomsRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.JobSchedulerInteractor
......@@ -45,6 +52,7 @@ import chat.rocket.android.server.infraestructure.SharedPreferencesAccountsRepos
import chat.rocket.android.server.infraestructure.SharedPreferencesMessagesRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesPermissionsRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesSettingsRepository
import chat.rocket.android.server.infraestructure.SharedPrefsAnalyticsTrackingRepository
import chat.rocket.android.server.infraestructure.SharedPrefsConnectingServerRepository
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.util.AppJsonAdapterFactory
......@@ -113,7 +121,10 @@ class AppModule {
@Provides
@Singleton
fun provideImagePipelineConfig(context: Context, okHttpClient: OkHttpClient): ImagePipelineConfig {
fun provideImagePipelineConfig(
context: Context,
okHttpClient: OkHttpClient
): ImagePipelineConfig {
val listeners = setOf(RequestLoggingListener())
return OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient)
......@@ -163,6 +174,12 @@ class AppModule {
return SharedPrefsCurrentServerRepository(prefs)
}
@Provides
@Singleton
fun provideAnalyticsTrackingRepository(prefs: SharedPreferences): AnalyticsTrackingRepository {
return SharedPrefsAnalyticsTrackingRepository(prefs)
}
@Provides
@ForAuthentication
fun provideConnectingServerRepository(prefs: SharedPreferences): CurrentServerRepository {
......@@ -177,7 +194,10 @@ class AppModule {
@Provides
@Singleton
fun providePermissionsRepository(localRepository: LocalRepository, moshi: Moshi): PermissionsRepository {
fun providePermissionsRepository(
localRepository: LocalRepository,
moshi: Moshi
): PermissionsRepository {
return SharedPreferencesPermissionsRepository(localRepository, moshi)
}
......@@ -226,15 +246,20 @@ class AppModule {
@Provides
@Singleton
fun provideMultiServerTokenRepository(repository: LocalRepository, moshi: Moshi): MultiServerTokenRepository {
fun provideMultiServerTokenRepository(
repository: LocalRepository,
moshi: Moshi
): MultiServerTokenRepository {
return SharedPreferencesMultiServerTokenRepository(repository, moshi)
}
@Provides
@Singleton
fun provideMessageRepository(@ForMessages preferences: SharedPreferences,
moshi: Moshi,
currentServerInteractor: GetCurrentServerInteractor): MessagesRepository {
fun provideMessageRepository(
@ForMessages preferences: SharedPreferences,
moshi: Moshi,
currentServerInteractor: GetCurrentServerInteractor
): MessagesRepository {
return SharedPreferencesMessagesRepository(preferences, moshi, currentServerInteractor)
}
......@@ -249,10 +274,12 @@ class AppModule {
fun provideConfiguration(context: Application): SpannableConfiguration {
val res = context.resources
return SpannableConfiguration.builder(context)
.theme(SpannableTheme.builder()
.blockMargin(0)
.linkColor(res.getColor(R.color.colorAccent))
.build())
.theme(
SpannableTheme.builder()
.blockMargin(0)
.linkColor(res.getColor(R.color.colorAccent))
.build()
)
.build()
}
......@@ -269,7 +296,10 @@ class AppModule {
@Provides
@Singleton
fun provideAccountsRepository(preferences: SharedPreferences, moshi: Moshi): AccountsRepository =
fun provideAccountsRepository(
preferences: SharedPreferences,
moshi: Moshi
): AccountsRepository =
SharedPreferencesAccountsRepository(preferences, moshi)
@Provides
......@@ -288,8 +318,16 @@ class AppModule {
manager: NotificationManager,
moshi: Moshi,
getAccountInteractor: GetAccountInteractor,
getSettingsInteractor: GetSettingsInteractor): PushManager {
return PushManager(groupedPushes, manager, moshi, getAccountInteractor, getSettingsInteractor, context)
getSettingsInteractor: GetSettingsInteractor
): PushManager {
return PushManager(
groupedPushes,
manager,
moshi,
getAccountInteractor,
getSettingsInteractor,
context
)
}
@Provides
......@@ -299,14 +337,19 @@ class AppModule {
@Provides
fun provideSendMessageJob(context: Application): JobInfo {
return JobInfo.Builder(MessageService.RETRY_SEND_MESSAGE_ID,
ComponentName(context, MessageService::class.java))
return JobInfo.Builder(
MessageService.RETRY_SEND_MESSAGE_ID,
ComponentName(context, MessageService::class.java)
)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build()
}
@Provides
fun provideJobSchedulerInteractor(jobScheduler: JobScheduler, jobInfo: JobInfo): JobSchedulerInteractor {
fun provideJobSchedulerInteractor(
jobScheduler: JobScheduler,
jobInfo: JobInfo
): JobSchedulerInteractor {
return JobSchedulerInteractorImpl(jobScheduler, jobInfo)
}
......@@ -323,4 +366,33 @@ class AppModule {
): DatabaseManager {
return factory.create(currentServer)
}
@Provides
@Singleton
fun provideAnswersAnalytics(): AnswersAnalytics {
return AnswersAnalytics()
}
@Provides
@Singleton
fun provideGoogleAnalyticsForFirebase(context: Application): GoogleAnalyticsForFirebase {
return GoogleAnalyticsForFirebase(context)
}
@Provides
@Singleton
fun provideAnalyticsManager(
analyticsTrackingInteractor: AnalyticsTrackingInteractor,
getCurrentServerInteractor: GetCurrentServerInteractor,
getAccountsInteractor: GetAccountsInteractor,
answersAnalytics: AnswersAnalytics,
googleAnalyticsForFirebase: GoogleAnalyticsForFirebase
): AnalyticsManager {
return AnalyticsManager(
analyticsTrackingInteractor,
getCurrentServerInteractor,
getAccountsInteractor,
listOf(answersAnalytics, googleAnalyticsForFirebase)
)
}
}
package chat.rocket.android.dagger.qualifier
import androidx.work.Worker
import dagger.MapKey
import kotlin.reflect.KClass
@MapKey
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class WorkerKey(val value: KClass<out Worker>)
......@@ -330,7 +330,7 @@ class DatabaseManager(val context: Application,
id = room.id,
subscriptionId = subscription.id,
type = room.type.toString(),
name = room.name ?: subscription.name ?: throw NullPointerException(),// this should be filtered on the SDK
name = room.name ?: subscription.name ?: throw NullPointerException(), // this should be filtered on the SDK
fullname = subscription.fullName ?: room.fullName,
userId = userId,
ownerId = room.user?.id,
......
......@@ -10,6 +10,8 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.uimodel.BaseUiModel
......@@ -31,13 +33,16 @@ fun newInstance(chatRoomId: String): Fragment {
}
}
internal const val TAG_FAVORITE_MESSAGES_FRAGMENT = "FavoriteMessagesFragment"
private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
private lateinit var chatRoomId: String
private val adapter = ChatRoomAdapter(enableActions = false)
@Inject
lateinit var presenter: FavoriteMessagesPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
private lateinit var chatRoomId: String
private val adapter = ChatRoomAdapter(enableActions = false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -61,6 +66,8 @@ class FavoriteMessagesFragment : Fragment(), FavoriteMessagesView {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
presenter.loadFavoriteMessages(chatRoomId)
analyticsManager.logScreenView(ScreenViewEvent.FavoriteMessages)
}
override fun showFavoriteMessages(favoriteMessages: List<BaseUiModel<*>>) {
......
......@@ -13,6 +13,8 @@ import androidx.recyclerview.widget.DividerItemDecoration.HORIZONTAL
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.files.adapter.FilesAdapter
import chat.rocket.android.files.presentation.FilesPresenter
......@@ -36,11 +38,14 @@ fun newInstance(chatRoomId: String): Fragment {
}
}
internal const val TAG_FILES_FRAGMENT = "FilesFragment"
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
class FilesFragment : Fragment(), FilesView {
@Inject
lateinit var presenter: FilesPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
private val adapter: FilesAdapter =
FilesAdapter { fileUiModel -> presenter.openFile(fileUiModel) }
private val linearLayoutManager = LinearLayoutManager(context)
......@@ -68,28 +73,31 @@ class FilesFragment : Fragment(), FilesView {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
presenter.loadFiles(chatRoomId)
analyticsManager.logScreenView(ScreenViewEvent.Files)
}
override fun showFiles(dataSet: List<FileUiModel>, total: Long) {
setupToolbar(total)
if (adapter.itemCount == 0) {
adapter.prependData(dataSet)
if (dataSet.size >= 30) {
recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView
) {
presenter.loadFiles(chatRoomId)
}
})
ui {
setupToolbar(total)
if (adapter.itemCount == 0) {
adapter.prependData(dataSet)
if (dataSet.size >= 30) {
recycler_view.addOnScrollListener(object :
EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(
page: Int,
totalItemsCount: Int,
recyclerView: RecyclerView
) {
presenter.loadFiles(chatRoomId)
}
})
}
group_no_file.isVisible = dataSet.isEmpty()
} else {
adapter.appendData(dataSet)
}
group_no_file.isVisible = dataSet.isEmpty()
} else {
adapter.appendData(dataSet)
}
}
......
......@@ -34,8 +34,8 @@ class FileUiModel(
}
private fun getUserDisplayName(): String {
val username = "@${genericAttachment.user?.username}"
val realName = genericAttachment.user?.name
val username = "@${genericAttachment.user.username}"
val realName = genericAttachment.user.name
val uploaderName = if (settings.useRealName()) realName else username
return uploaderName ?: username
}
......
......@@ -7,6 +7,7 @@ import android.graphics.Paint
import android.graphics.RectF
import android.text.Spanned
import android.text.style.ClickableSpan
import android.text.style.ImageSpan
import android.text.style.ReplacementSpan
import android.util.Patterns
import android.view.View
......@@ -25,7 +26,6 @@ import org.commonmark.node.Document
import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.Text
import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder
......@@ -60,11 +60,11 @@ class MessageParser @Inject constructor(
}
}
val builder = SpannableBuilder()
val content = EmojiRepository.shortnameToUnicode(text, true)
val content = EmojiRepository.shortnameToUnicode(text)
val parentNode = parser.parse(toLenientMarkdown(content))
parentNode.accept(MarkdownVisitor(configuration, builder))
parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(configuration, builder))
parentNode.accept(EmojiVisitor(context, configuration, builder))
message.mentions?.let {
parentNode.accept(MentionVisitor(context, builder, mentions, selfUsername))
}
......@@ -126,16 +126,29 @@ class MessageParser @Inject constructor(
}
class EmojiVisitor(
private val context: Context,
configuration: SpannableConfiguration,
private val builder: SpannableBuilder
) : SpannableMarkdownVisitor(configuration, builder) {
private val emojiSize = context.resources.getDimensionPixelSize(R.dimen.custom_emoji_small)
override fun visit(document: Document) {
val spannable = EmojiParser.parse(builder.text())
val spannable = EmojiParser.parse(context, builder.text())
if (spannable is Spanned) {
val spans = spannable.getSpans(0, spannable.length, EmojiTypefaceSpan::class.java)
spans.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it), 0)
val emojiOneTypefaceSpans = spannable.getSpans(0, spannable.length,
EmojiTypefaceSpan::class.java)
val emojiImageSpans = spannable.getSpans(0, spannable.length, ImageSpan::class.java)
emojiOneTypefaceSpans.forEach {
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
emojiImageSpans.forEach {
it.drawable?.setBounds(0, 0, emojiSize, emojiSize)
builder.setSpan(it, spannable.getSpanStart(it), spannable.getSpanEnd(it),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
......@@ -230,4 +243,4 @@ class MessageParser @Inject constructor(
canvas.drawText(text, start, end, x + padding, y.toFloat(), paint)
}
}
}
\ No newline at end of file
}
......@@ -2,7 +2,6 @@ package chat.rocket.android.helper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.useRealName
import chat.rocket.common.model.SimpleUser
......@@ -12,43 +11,13 @@ import javax.inject.Inject
class UserHelper @Inject constructor(
private val localRepository: LocalRepository,
private val getCurrentServerInteractor: GetCurrentServerInteractor,
settingsRepository: SettingsRepository
private val settingsRepository: SettingsRepository
) {
private val settings: PublicSettings = settingsRepository.get(getCurrentServerInteractor.get()!!)
/**
* Return the display name for the given [user].
* If setting 'Use_Real_Name' is true then the real name will be given, or else
* the username without the '@' is yielded. The fallback for any case is the username, which
* could be null.
*/
fun displayName(user: User): String? {
return if (settings.useRealName()) user.name ?: user.username else user.username
}
fun displayName(user: SimpleUser): String {
return if (settings.useRealName()) user.name ?: user.username ?: "" else user.username ?: ""
}
/**
* Return current logged user's display name.
*
* @see displayName
*/
fun displayName(): String? {
user()?.let {
return displayName(it)
}
return null
}
/**
* Return current logged [User].
*/
fun user(): User? {
return localRepository.getCurrentUser(serverUrl())
}
fun user(): User? = getCurrentServerInteractor.get()?.let { localRepository.getCurrentUser(it) }
/**
* Return the username for the current logged [User].
......@@ -56,13 +25,20 @@ class UserHelper @Inject constructor(
fun username(): String? = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY, null)
/**
* Whether current [User] is admin on the current server.
* Return the display name for the given [user].
* If setting 'Use_Real_Name' is true then the real name will be given, otherwise the username
* without the '@' is yielded.
*/
fun isAdmin(): Boolean {
return user()?.roles?.find { it.equals("admin", ignoreCase = true) } != null
}
fun displayName(user: SimpleUser) = getCurrentServerInteractor.get()?.let {
if (settingsRepository.get(it).useRealName()) {
user.name
} else {
user.username
}
}.orEmpty()
private fun serverUrl(): String {
return getCurrentServerInteractor.get()!!
}
}
\ No newline at end of file
/**
* Whether current [User] is admin on the current server.
*/
fun isAdmin(): Boolean = user()?.roles?.find { it.equals("admin", true) } != null
}
......@@ -4,39 +4,50 @@ import chat.rocket.android.R
import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.chatrooms.ui.ChatRoomsFragment
import chat.rocket.android.chatrooms.ui.TAG_CHAT_ROOMS_FRAGMENT
import chat.rocket.android.createchannel.ui.CreateChannelFragment
import chat.rocket.android.createchannel.ui.TAG_CREATE_CHANNEL_FRAGMENT
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.profile.ui.ProfileFragment
import chat.rocket.android.profile.ui.TAG_PROFILE_FRAGMENT
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.settings.ui.SettingsFragment
import chat.rocket.android.settings.ui.TAG_SETTINGS_FRAGMENT
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.webview.adminpanel.ui.AdminPanelWebViewFragment
class MainNavigator(internal val activity: MainActivity) {
fun toChatList(chatRoomId: String? = null) {
activity.addFragment("ChatRoomsFragment", R.id.fragment_container) {
activity.addFragment(TAG_CHAT_ROOMS_FRAGMENT, R.id.fragment_container) {
ChatRoomsFragment.newInstance(chatRoomId)
}
}
fun toCreateChannel() {
activity.addFragment("CreateChannelFragment", R.id.fragment_container) {
activity.addFragment(TAG_CREATE_CHANNEL_FRAGMENT, R.id.fragment_container) {
CreateChannelFragment.newInstance()
}
}
fun toUserProfile() {
activity.addFragment("ProfileFragment", R.id.fragment_container) {
activity.addFragment(TAG_PROFILE_FRAGMENT, R.id.fragment_container) {
ProfileFragment.newInstance()
}
}
fun toSettings() {
activity.addFragment("SettingsFragment", R.id.fragment_container) {
activity.addFragment(TAG_SETTINGS_FRAGMENT, R.id.fragment_container) {
SettingsFragment.newInstance()
}
}
fun toAdminPanel(webPageUrl: String, userToken: String) {
activity.addFragment("AdminPanelWebViewFragment", R.id.fragment_container) {
AdminPanelWebViewFragment.newInstance(webPageUrl, userToken)
}
}
fun toChatRoom(
chatRoomId: String,
chatRoomName: String,
......@@ -62,7 +73,17 @@ class MainNavigator(internal val activity: MainActivity) {
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
fun toNewServer(serverUrl: String? = null) {
/**
* Switches to a server, given a [serverUrl] or adds a new server (navigating to the
* AuthenticationActivity) if the user server list only contains one server and the
* user logs out from this server.
* NOTE: If the user has more than one server and logs out from the current server, then it will
* switch to the first server in the server list.
*
* @param serverUrl The server URL to switch from, or null in case user logs out from the
* current server.
*/
fun switchOrAddNewServer(serverUrl: String? = null) {
activity.startActivity(activity.changeServerIntent(serverUrl = serverUrl))
activity.finish()
}
......
package chat.rocket.android.main.presentation
import android.content.Context
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.emoji.Emoji
import chat.rocket.android.emoji.EmojiRepository
import chat.rocket.android.emoji.Fitzpatrick
import chat.rocket.android.emoji.internal.EmojiCategory
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.main.uimodel.NavHeaderUiModel
import chat.rocket.android.main.uimodel.NavHeaderUiModelMapper
......@@ -11,6 +16,7 @@ import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.RefreshPermissionsInteractor
import chat.rocket.android.server.domain.RemoveAccountInteractor
import chat.rocket.android.server.domain.SaveAccountInteractor
import chat.rocket.android.server.domain.TokenRepository
......@@ -20,6 +26,7 @@ import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.adminPanelUrl
import chat.rocket.android.util.extensions.registerPushToken
import chat.rocket.android.util.extensions.serverLogoUrl
import chat.rocket.android.util.retryIO
......@@ -28,14 +35,13 @@ import chat.rocket.common.RocketChatException
import chat.rocket.common.model.UserStatus
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.setDefaultStatus
import chat.rocket.core.internal.rest.getCustomEmojis
import chat.rocket.core.internal.rest.logout
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.unregisterPushToken
import chat.rocket.core.model.Myself
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.channels.Channel
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import timber.log.Timber
import javax.inject.Inject
......@@ -47,12 +53,13 @@ class MainPresenter @Inject constructor(
private val tokenRepository: TokenRepository,
private val serverInteractor: GetCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val refreshPermissionsInteractor: RefreshPermissionsInteractor,
private val localRepository: LocalRepository,
private val navHeaderMapper: NavHeaderUiModelMapper,
private val saveAccountInteractor: SaveAccountInteractor,
private val getAccountsInteractor: GetAccountsInteractor,
private val removeAccountInteractor: RemoveAccountInteractor,
private val factory: RocketChatClientFactory,
factory: RocketChatClientFactory,
private val groupedPush: GroupedPush,
dbManagerFactory: DatabaseManagerFactory,
getSettingsInteractor: GetSettingsInteractor,
......@@ -63,7 +70,6 @@ class MainPresenter @Inject constructor(
private val dbManager = dbManagerFactory.create(currentServer)
private val client: RocketChatClient = factory.create(currentServer)
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
private val userDataChannel = Channel<Myself>()
fun toChatList(chatRoomId: String? = null) = navigator.toChatList(chatRoomId)
......@@ -72,6 +78,10 @@ class MainPresenter @Inject constructor(
fun toSettings() = navigator.toSettings()
fun toAdminPanel() = tokenRepository.get(currentServer)?.let {
navigator.toAdminPanel(currentServer.adminPanelUrl(), it.authToken)
}
fun toCreateChannel() = navigator.toCreateChannel()
fun loadServerAccounts() {
......@@ -121,6 +131,38 @@ class MainPresenter @Inject constructor(
}
}
/**
* Load all emojis for the current server. Simple emojis are always the same for every server,
* but custom emojis vary according to the its url.
*/
fun loadEmojis() {
launchUI(strategy) {
EmojiRepository.setCurrentServerUrl(currentServer)
val customEmojiList = mutableListOf<Emoji>()
try {
for (customEmoji in retryIO("getCustomEmojis()") { client.getCustomEmojis() }) {
customEmojiList.add(Emoji(
shortname = ":${customEmoji.name}:",
category = EmojiCategory.CUSTOM.name,
url = "$currentServer/emoji-custom/${customEmoji.name}.${customEmoji.extension}",
count = 0,
fitzpatrick = Fitzpatrick.Default.type,
keywords = customEmoji.aliases,
shortnameAlternates = customEmoji.aliases,
siblings = mutableListOf(),
unicode = "",
isDefault = true
))
}
EmojiRepository.load(view as Context, customEmojis = customEmojiList)
} catch (ex: RocketChatException) {
Timber.e(ex)
EmojiRepository.load(view as Context)
}
}
}
/**
* Logout from current server.
*/
......@@ -145,7 +187,7 @@ class MainPresenter @Inject constructor(
tokenRepository.remove(currentServer)
withContext(CommonPool) { dbManager.logout() }
navigator.toNewServer()
navigator.switchOrAddNewServer()
} catch (ex: Exception) {
Timber.d(ex, "Error cleaning up the session...")
}
......@@ -155,6 +197,7 @@ class MainPresenter @Inject constructor(
fun connect() {
refreshSettingsInteractor.refreshAsync(currentServer)
refreshPermissionsInteractor.refreshAsync(currentServer)
manager.connect()
}
......@@ -165,7 +208,7 @@ class MainPresenter @Inject constructor(
fun changeServer(serverUrl: String) {
if (currentServer != serverUrl) {
navigator.toNewServer(serverUrl)
navigator.switchOrAddNewServer(serverUrl)
} else {
view.closeServerSelection()
}
......@@ -190,13 +233,6 @@ class MainPresenter @Inject constructor(
}
}
suspend fun refreshToken(token: String?) {
token?.let {
localRepository.save(LocalRepository.KEY_PUSH_TOKEN, token)
client.registerPushToken(token, getAccountsInteractor.get(), factory)
}
}
private suspend fun saveAccount(uiModel: NavHeaderUiModel) {
val icon = settings.favicon()?.let {
currentServer.serverLogoUrl(it)
......
......@@ -5,10 +5,9 @@ import android.app.Activity
import android.app.AlertDialog
import android.app.ProgressDialog
import android.os.Bundle
import android.view.Gravity
import android.view.MenuItem
import androidx.annotation.IdRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
......@@ -19,15 +18,16 @@ import chat.rocket.android.main.adapter.Selector
import chat.rocket.android.main.presentation.MainPresenter
import chat.rocket.android.main.presentation.MainView
import chat.rocket.android.main.uimodel.NavHeaderUiModel
import chat.rocket.android.push.refreshPushToken
import chat.rocket.android.server.domain.PermissionsInteractor
import chat.rocket.android.server.domain.model.Account
import chat.rocket.android.server.ui.INTENT_CHAT_ROOM_ID
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.invalidateFirebaseToken
import chat.rocket.common.model.UserStatus
import com.google.firebase.iid.FirebaseInstanceId
import com.google.firebase.messaging.FirebaseMessaging
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......@@ -36,9 +36,6 @@ import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.activity_main.*
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
private const val CURRENT_STATE = "current_state"
......@@ -51,6 +48,8 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var presenter: MainPresenter
@Inject
lateinit var permissions: PermissionsInteractor
private var isFragmentAdded: Boolean = false
private var expanded = false
private val headerLayout by lazy { view_navigation.getHeaderView(0) }
......@@ -62,15 +61,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
launch(CommonPool) {
try {
val token = FirebaseInstanceId.getInstance().token
Timber.d("FCM token: $token")
presenter.refreshToken(token)
} catch (ex: Exception) {
Timber.d(ex, "Missing play services...")
}
}
refreshPushToken()
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
......@@ -80,6 +71,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
presenter.connect()
presenter.loadServerAccounts()
presenter.loadCurrentInfo()
presenter.loadEmojis()
setupToolbar()
setupNavigationView()
}
......@@ -135,7 +127,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
text_user_name.text = userDisplayName
}
if (userAvatar != null) {
image_avatar.setImageURI(userAvatar)
setAvatar(userAvatar)
}
if (serverLogo != null) {
server_logo.setImageURI(serverLogo)
......@@ -172,9 +164,9 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
}
headerLayout.image_avatar.setOnClickListener {
view_navigation.menu.findItem(R.id.action_profile).isChecked = true
view_navigation.menu.findItem(R.id.menu_action_profile).isChecked = true
presenter.toUserProfile()
drawer_layout.closeDrawer(Gravity.START)
drawer_layout.closeDrawer(GravityCompat.START)
}
}
......@@ -211,7 +203,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
}
override fun invalidateToken(token: String) =
FirebaseInstanceId.getInstance().deleteToken(token, FirebaseMessaging.INSTANCE_ID_SCOPE)
invalidateFirebaseToken(token)
override fun showMessage(resId: Int) = showToast(resId)
......@@ -224,44 +216,31 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
}
fun setupNavigationView() {
view_navigation.setNavigationItemSelectedListener { menuItem ->
menuItem.isChecked = true
with (view_navigation.menu) {
clear()
setupMenu(this)
}
view_navigation.setNavigationItemSelectedListener {
it.isChecked = true
closeDrawer()
onNavDrawerItemSelected(menuItem)
onNavDrawerItemSelected(it)
true
}
toolbar.setNavigationIcon(R.drawable.ic_menu_white_24dp)
toolbar.setNavigationOnClickListener {
openDrawer()
}
toolbar.setNavigationOnClickListener { openDrawer() }
}
private fun onNavDrawerItemSelected(menuItem: MenuItem) {
when (menuItem.itemId) {
R.id.action_chat_rooms -> {
presenter.toChatList()
}
R.id.action_profile -> {
presenter.toUserProfile()
}
R.id.action_channel -> {
presenter.toCreateChannel()
}
R.id.action_settings -> {
presenter.toSettings()
}
R.id.action_logout -> {
presenter.logout()
}
}
fun setAvatar(avatarUrl: String) {
headerLayout.image_avatar.setImageURI(avatarUrl)
}
fun getDrawerLayout(): DrawerLayout = drawer_layout
fun openDrawer() = drawer_layout.openDrawer(Gravity.START)
fun openDrawer() = drawer_layout.openDrawer(GravityCompat.START)
fun closeDrawer() = drawer_layout.closeDrawer(Gravity.START)
fun closeDrawer() = drawer_layout.closeDrawer(GravityCompat.START)
fun setCheckedNavDrawerItem(@IdRes item: Int) = view_navigation.setCheckedItem(item)
......
package chat.rocket.android.main.ui
import android.view.Menu
import android.view.MenuItem
import chat.rocket.android.R
internal fun MainActivity.setupMenu(menu: Menu) {
with(menu) {
add(
R.id.menu_section_one,
R.id.menu_action_chats,
Menu.NONE,
R.string.title_chats
).setIcon(R.drawable.ic_chat_bubble_black_24dp)
.isChecked = true
add(
R.id.menu_section_one,
R.id.menu_action_create_channel,
Menu.NONE,
R.string.action_create_channel
).setIcon(R.drawable.ic_create_black_24dp)
add(
R.id.menu_section_two,
R.id.menu_action_profile,
Menu.NONE,
R.string.title_profile
).setIcon(R.drawable.ic_person_black_24dp)
add(
R.id.menu_section_two,
R.id.menu_action_settings,
Menu.NONE,
R.string.title_settings
).setIcon(R.drawable.ic_settings_black_24dp)
if (permissions.canSeeTheAdminPanel()) {
add(
R.id.menu_section_two,
R.id.menu_action_admin_panel,
Menu.NONE,
R.string.title_admin_panel
).setIcon(R.drawable.ic_settings_black_24dp)
}
add(
R.id.menu_section_three,
R.id.menu_action_logout,
Menu.NONE,
R.string.action_logout
).setIcon(R.drawable.ic_logout_black_24dp)
setGroupCheckable(R.id.menu_section_one, true, true)
setGroupCheckable(R.id.menu_section_two, true, true)
setGroupCheckable(R.id.menu_section_three, true, true)
}
}
internal fun MainActivity.onNavDrawerItemSelected(menuItem: MenuItem) {
when (menuItem.itemId) {
R.id.menu_action_chats-> presenter.toChatList()
R.id.menu_action_create_channel -> presenter.toCreateChannel()
R.id.menu_action_profile -> presenter.toUserProfile()
R.id.menu_action_settings -> presenter.toSettings()
R.id.menu_action_admin_panel -> presenter.toAdminPanel()
R.id.menu_action_logout -> presenter.logout()
}
}
......@@ -2,6 +2,7 @@ package chat.rocket.android.members.di
import chat.rocket.android.members.ui.MembersFragment
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.members.ui.MemberBottomSheetFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
......@@ -11,4 +12,9 @@ abstract class MembersFragmentProvider {
@ContributesAndroidInjector(modules = [MembersFragmentModule::class])
@PerFragment
abstract fun provideMembersFragment(): MembersFragment
@ContributesAndroidInjector()
@PerFragment
abstract fun provideMemberBottomSheetFragment(): MemberBottomSheetFragment
}
\ No newline at end of file
package chat.rocket.android.members.presentation
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.members.ui.TAG_MEMBER_BOTTOM_SHEET_FRAGMENT
import chat.rocket.android.members.ui.newInstance
class MembersNavigator(internal val activity: ChatRoomActivity) {
......@@ -8,7 +9,7 @@ class MembersNavigator(internal val activity: ChatRoomActivity) {
fun toMemberDetails(avatarUri: String, realName: String, username: String, email: String, utcOffset: String) {
activity.apply {
newInstance(avatarUri, realName, username, email, utcOffset)
.show(supportFragmentManager, "MemberBottomSheetFragment")
.show(supportFragmentManager, TAG_MEMBER_BOTTOM_SHEET_FRAGMENT)
}
}
}
package chat.rocket.android.members.ui
import android.os.Bundle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.textContent
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_member_bottom_sheet.*
import javax.inject.Inject
fun newInstance(avatarUri: String,
realName: String,
username: String,
email: String,
utcOffset: String): BottomSheetDialogFragment {
fun newInstance(
avatarUri: String,
realName: String,
username: String,
email: String,
utcOffset: String
): BottomSheetDialogFragment {
return MemberBottomSheetFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_AVATAR_URI, avatarUri)
......@@ -27,13 +33,17 @@ fun newInstance(avatarUri: String,
}
}
internal const val TAG_MEMBER_BOTTOM_SHEET_FRAGMENT = "MemberBottomSheetFragment"
private const val BUNDLE_AVATAR_URI = "avatar_uri"
private const val BUNDLE_REAL_NAME = "real_name"
private const val BUNDLE_USERNAME = "username"
private const val BUNDLE_EMAIL = "email"
private const val BUNDLE_UTC_OFFSET = "utc_offset"
class MemberBottomSheetFragment: BottomSheetDialogFragment() {
class MemberBottomSheetFragment : BottomSheetDialogFragment() {
@Inject
lateinit var analyticsManager: AnalyticsManager
private lateinit var avatarUri: String
private lateinit var realName: String
private lateinit var username: String
......@@ -42,6 +52,7 @@ class MemberBottomSheetFragment: BottomSheetDialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
val bundle = arguments
if (bundle != null) {
......@@ -54,12 +65,19 @@ class MemberBottomSheetFragment: BottomSheetDialogFragment() {
requireNotNull(bundle) { "no arguments supplied when the fragment was instantiated" }
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? =
inflater.inflate(R.layout.fragment_member_bottom_sheet, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
showMemberDetails()
analyticsManager.logScreenView(ScreenViewEvent.MemberBottomSheet)
}
private fun showMemberDetails() {
......@@ -74,7 +92,7 @@ class MemberBottomSheetFragment: BottomSheetDialogFragment() {
text_member_email_address.isVisible = false
}
if (utcOffset.isNotEmpty()){
if (utcOffset.isNotEmpty()) {
text_member_utc.content = utcOffset
} else {
text_utc.isVisible = false
......
package chat.rocket.android.members.ui
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.members.adapter.MembersAdapter
......@@ -31,11 +33,14 @@ fun newInstance(chatRoomId: String): Fragment {
}
}
internal const val TAG_MEMBERS_FRAGMENT = "MembersFragment"
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
class MembersFragment : Fragment(), MembersView {
@Inject
lateinit var presenter: MembersPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
private val adapter: MembersAdapter =
MembersAdapter { memberUiModel -> presenter.toMemberDetails(memberUiModel) }
private val linearLayoutManager = LinearLayoutManager(context)
......@@ -63,6 +68,8 @@ class MembersFragment : Fragment(), MembersView {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
presenter.loadChatRoomsMembers(chatRoomId)
analyticsManager.logScreenView(ScreenViewEvent.Members)
}
override fun showMembers(dataSet: List<MemberUiModel>, total: Long) {
......
......@@ -10,6 +10,8 @@ import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.uimodel.BaseUiModel
......@@ -31,6 +33,7 @@ fun newInstance(chatRoomId: String): Fragment {
}
}
internal const val TAG_MENTIONS_FRAGMENT = "MentionsFragment"
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
class MentionsFragment : Fragment(), MentionsView {
......@@ -39,6 +42,8 @@ class MentionsFragment : Fragment(), MentionsView {
private val adapter = ChatRoomAdapter(enableActions = false)
@Inject
lateinit var presenter: MentionsPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -63,6 +68,8 @@ class MentionsFragment : Fragment(), MentionsView {
setupToolbar()
presenter.loadMentions(chatRoomId)
analyticsManager.logScreenView(ScreenViewEvent.Mentions)
}
override fun showMentions(mentions: List<BaseUiModel<*>>) {
......
package chat.rocket.android.pinnedmessages.ui
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.chatroom.adapter.ChatRoomAdapter
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.uimodel.BaseUiModel
import chat.rocket.android.helper.EndlessRecyclerViewScrollListener
import chat.rocket.android.pinnedmessages.presentation.PinnedMessagesPresenter
import chat.rocket.android.pinnedmessages.presentation.PinnedMessagesView
import chat.rocket.android.server.domain.AnalyticsTrackingInteractor
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.ui
......@@ -31,14 +34,16 @@ fun newInstance(chatRoomId: String): Fragment {
}
}
internal const val TAG_PINNED_MESSAGES_FRAGMENT = "PinnedMessagesFragment"
private const val BUNDLE_CHAT_ROOM_ID = "chat_room_id"
class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
private lateinit var chatRoomId: String
private val adapter = ChatRoomAdapter(enableActions = false)
@Inject
lateinit var presenter: PinnedMessagesPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
private lateinit var chatRoomId: String
private val adapter = ChatRoomAdapter(enableActions = false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......@@ -63,6 +68,8 @@ class PinnedMessagesFragment : Fragment(), PinnedMessagesView {
setupToolbar()
presenter.loadPinnedMessages(chatRoomId)
analyticsManager.logScreenView(ScreenViewEvent.PinnedMessages)
}
override fun showPinnedMessages(pinnedMessages: List<BaseUiModel<*>>) {
......
package chat.rocket.android.preferences.di
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.preferences.presentation.PreferencesView
import chat.rocket.android.preferences.ui.PreferencesFragment
import dagger.Module
import dagger.Provides
@Module
class PreferencesFragmentModule {
@Provides
@PerFragment
fun preferencesView(frag: PreferencesFragment): PreferencesView {
return frag
}
}
\ No newline at end of file
package chat.rocket.android.preferences.di
import chat.rocket.android.dagger.scope.PerFragment
import chat.rocket.android.preferences.ui.PreferencesFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class PreferencesFragmentProvider {
@ContributesAndroidInjector(modules = [PreferencesFragmentModule::class])
@PerFragment
abstract fun providePreferencesFragment(): PreferencesFragment
}
\ No newline at end of file
package chat.rocket.android.preferences.presentation
import chat.rocket.android.server.domain.AnalyticsTrackingInteractor
import javax.inject.Inject
class PreferencesPresenter @Inject constructor(
private val view: PreferencesView,
private val analyticsTrackingInteractor: AnalyticsTrackingInteractor
) {
fun loadAnalyticsTrackingInformation() {
view.setupAnalyticsTrackingView(analyticsTrackingInteractor.get())
}
fun enableAnalyticsTracking() {
analyticsTrackingInteractor.save(true)
}
fun disableAnalyticsTracking() {
analyticsTrackingInteractor.save(false)
}
}
\ No newline at end of file
package chat.rocket.android.preferences.presentation
interface PreferencesView {
/**
* Setups the analytics tracking view.
*
* @param isAnalyticsTrackingEnabled Whether the analytics tracking is enabled
*/
fun setupAnalyticsTrackingView(isAnalyticsTrackingEnabled: Boolean)
}
\ No newline at end of file
package chat.rocket.android.preferences.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.analytics.AnalyticsManager
import chat.rocket.android.analytics.event.ScreenViewEvent
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.preferences.presentation.PreferencesPresenter
import chat.rocket.android.preferences.presentation.PreferencesView
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.app_bar.*
import kotlinx.android.synthetic.main.fragment_preferences.*
import javax.inject.Inject
internal const val TAG_PREFERENCES_FRAGMENT = "PreferencesFragment"
class PreferencesFragment : Fragment(), PreferencesView {
@Inject
lateinit var presenter: PreferencesPresenter
@Inject
lateinit var analyticsManager: AnalyticsManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_preferences, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar()
setupListeners()
presenter.loadAnalyticsTrackingInformation()
analyticsManager.logScreenView(ScreenViewEvent.Preferences)
}
override fun setupAnalyticsTrackingView(isAnalyticsTrackingEnabled: Boolean) {
if (BuildConfig.FLAVOR == "foss") {
switch_analytics_tracking.isChecked = false
switch_analytics_tracking.isEnabled = false
text_analytics_tracking_description.text =
getString(R.string.msg_not_applicable_since_it_is_a_foss_version)
return
}
if (isAnalyticsTrackingEnabled) {
text_analytics_tracking_description.text =
getString(R.string.msg_send_analytics_tracking)
} else {
text_analytics_tracking_description.text =
getString(R.string.msg_do_not_send_analytics_tracking)
}
switch_analytics_tracking.isChecked = isAnalyticsTrackingEnabled
}
private fun setupToolbar() {
with((activity as MainActivity).toolbar) {
title = getString(R.string.title_preferences)
setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
setNavigationOnClickListener { activity?.onBackPressed() }
}
}
private fun setupListeners() {
switch_analytics_tracking.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
text_analytics_tracking_description.text =
getString(R.string.msg_send_analytics_tracking)
presenter.enableAnalyticsTracking()
} else {
text_analytics_tracking_description.text =
getString(R.string.msg_do_not_send_analytics_tracking)
presenter.disableAnalyticsTracking()
}
}
}
companion object {
fun newInstance() = PreferencesFragment()
}
}
package chat.rocket.android.profile.presentation
import android.graphics.Bitmap
import android.net.Uri
import chat.rocket.android.chatroom.domain.UriInteractor
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.UserHelper
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extension.compressImageAndGetByteArray
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.resetAvatar
import chat.rocket.core.internal.rest.setAvatar
import chat.rocket.core.internal.rest.updateProfile
import java.util.*
import javax.inject.Inject
class ProfilePresenter @Inject constructor(
private val view: ProfileView,
private val strategy: CancelStrategy,
private val uriInteractor: UriInteractor,
val userHelper: UserHelper,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory
) {
private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl)
private lateinit var myselfId: String
private val myselfId = userHelper.user()?.id ?: ""
private var myselfName = userHelper.user()?.name ?: ""
private var myselfUsername = userHelper.username() ?: ""
private var myselfEmailAddress = userHelper.user()?.emails?.getOrNull(0)?.address ?: ""
fun loadUserProfile() {
launchUI(strategy) {
view.showLoading()
try {
val myself = retryIO("me") { client.me() }
val id = myself.id
val username = myself.username
if (id == null || username == null) {
view.showProfile(
serverUrl.avatarUrl(myselfUsername),
myselfName,
myselfUsername,
myselfEmailAddress
)
} catch (exception: RocketChatException) {
view.showMessage(exception)
} finally {
view.hideLoading()
}
}
}
fun updateUserProfile(email: String, name: String, username: String) {
launchUI(strategy) {
view.showLoading()
try {
retryIO { client.updateProfile(myselfId, email, name, username) }
myselfEmailAddress = email
myselfName = name
myselfUsername = username
view.showProfileUpdateSuccessfullyMessage()
loadUserProfile()
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
} else {
myselfId = id
val avatarUrl = serverUrl.avatarUrl(username)
val email = myself.emails?.getOrNull(0)?.address
view.showProfile(
avatarUrl,
myself.name ?: "",
myself.username ?: "",
email
)
}
} finally {
view.hideLoading()
}
}
}
fun updateAvatar(uri: Uri) {
launchUI(strategy) {
view.showLoading()
try {
retryIO {
client.setAvatar(
uriInteractor.getFileName(uri) ?: uri.toString(),
uriInteractor.getMimeType(uri)
) {
uriInteractor.getInputStream(uri)
}
}
view.reloadUserAvatar(serverUrl.avatarUrl(myselfUsername))
} catch (exception: RocketChatException) {
view.showMessage(exception)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
}
}
fun updateUserProfile(email: String, name: String, username: String, avatarUrl: String = "") {
fun preparePhotoAndUpdateAvatar(bitmap: Bitmap) {
launchUI(strategy) {
view.showLoading()
try {
if (avatarUrl != "") {
retryIO { client.setAvatar(avatarUrl) }
val byteArray = bitmap.compressImageAndGetByteArray("image/png")
retryIO {
client.setAvatar(
UUID.randomUUID().toString() + ".png",
"image/png"
) {
byteArray?.inputStream()
}
}
val user = retryIO {
client.updateProfile(
userId = myselfId, email = email, name = name, username = username
)
view.reloadUserAvatar(serverUrl.avatarUrl(myselfUsername))
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showProfileUpdateSuccessfullyMessage()
loadUserProfile()
} finally {
view.hideLoading()
}
}
}
fun resetAvatar() {
launchUI(strategy) {
view.showLoading()
try {
retryIO { client.resetAvatar(myselfId) }
view.reloadUserAvatar(serverUrl.avatarUrl(myselfUsername))
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
......
......@@ -15,6 +15,13 @@ interface ProfileView : LoadingView, MessageView {
*/
fun showProfile(avatarUrl: String, name: String, username: String, email: String?)
/**
* Reloads the user avatar (after successfully updating it).
*
* @param avatarUrl The user avatar URL.
*/
fun reloadUserAvatar(avatarUrl: String)
/**
* Shows a profile update successfully message
*/
......
......@@ -4,7 +4,6 @@ import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.RemoteInput
import android.widget.Toast
import chat.rocket.android.R
......
package chat.rocket.android.push
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.core.internal.rest.registerPushToken
import com.google.firebase.iid.FirebaseInstanceId
import com.google.firebase.iid.FirebaseInstanceIdService
import dagger.android.AndroidInjection
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
class FirebaseTokenService : FirebaseInstanceIdService() {
@Inject
lateinit var factory: RocketChatClientFactory
@Inject
lateinit var getCurrentServerInteractor: GetCurrentServerInteractor
@Inject
lateinit var localRepository: LocalRepository
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onTokenRefresh() {
try {
val fcmToken = FirebaseInstanceId.getInstance().token
val currentServer = getCurrentServerInteractor.get()
val client = currentServer?.let { factory.create(currentServer) }
fcmToken?.let {
localRepository.save(LocalRepository.KEY_PUSH_TOKEN, fcmToken)
client?.let {
launch {
try {
Timber.d("Registering push token: $fcmToken for ${client.url}")
retryIO("register push token") { client.registerPushToken(fcmToken) }
} catch (ex: RocketChatException) {
Timber.e(ex, "Error registering push token")
}
}
}
}
} catch (ex: Exception) {
Timber.e(ex, "Error refreshing Firebase TOKEN")
}
}
}
\ No newline at end of file
package chat.rocket.android.push.di
import chat.rocket.android.dagger.module.AppModule
import chat.rocket.android.push.FirebaseTokenService
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class FirebaseTokenServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideFirebaseTokenService(): FirebaseTokenService
}
\ No newline at end of file
......@@ -3,7 +3,7 @@ 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)
fun save(account: Account)
fun load(): List<Account>
fun remove(serverUrl: String)
}
\ No newline at end of file
package chat.rocket.android.server.domain
import javax.inject.Inject
class AnalyticsTrackingInteractor @Inject constructor(val repository: AnalyticsTrackingRepository) {
fun save(isAnalyticsTrackingEnable: Boolean) = repository.save(isAnalyticsTrackingEnable)
fun get(): Boolean = repository.get()
}
\ No newline at end of file
package chat.rocket.android.server.domain
interface AnalyticsTrackingRepository {
fun save(isAnalyticsTrackingEnable: Boolean)
fun get(): Boolean
}
\ No newline at end of file
......@@ -3,5 +3,5 @@ package chat.rocket.android.server.domain
import javax.inject.Inject
class GetAccountsInteractor @Inject constructor(val repository: AccountsRepository) {
suspend fun get() = repository.load()
fun get() = repository.load()
}
\ No newline at end of file
......@@ -72,4 +72,22 @@ interface MessagesRepository {
suspend fun getAllUnsent(): List<Message>
suspend fun getUnsentByRoomId(roomId: String): List<Message>
/**
* Save time of the latest room messages sync.
* Call this fun only when the latest messages list being received via /history or /messages network calls
*
* @param rid The id of the room the messages are.
* @param timeMillis time of room messages sync or the latest room message timestamp(which came with /history request)
*/
suspend fun saveLastSyncDate(rid: String, timeMillis: Long)
/**
* Get time when the room chat history has been loaded last time.
*
* @param rid The id of the room the messages are.
*
* @return Last Sync time or Null.
*/
suspend fun getLastSyncDate(rid: String): Long?
}
\ No newline at end of file
......@@ -5,20 +5,20 @@ import chat.rocket.core.model.Permission
interface PermissionsRepository {
/**
* Store [permission] locally.
* Stores a list of [Permission] locally.
*
* @param url The server url from where we're interest to store the permission.
* @param permission The permission to store.
* @param url The server url to store the permission.
* @param permissionList The permission list to store.
*/
fun save(url: String, permission: Permission)
fun save(url: String, permissionList: List<Permission>)
/**
* Get permission given by the [permissionId] and for the server [url].
* Gets permission given by the [permissionId] and for the server [url].
*
* @param url The server url from where we're interested on getting the permissions.
* @param permissionId the id of the permission to get.
* @param url The server url to get the permissions from.
* @param permissionId the ID of the permission to get.
*
* @return The interested [Permission] or null if not found.
* @return The [Permission] or null if not found.
*/
fun get(url: String, permissionId: String): Permission?
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.retryIO
import chat.rocket.core.internal.rest.permissions
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
/**
* This class reloads the current logged server permission whenever its used.
*/
class RefreshPermissionsInteractor @Inject constructor(
private val factory: RocketChatClientFactory,
private val repository: PermissionsRepository
) {
fun refreshAsync(server: String) {
launch(CommonPool) {
try {
factory.create(server).let { client ->
val permissions = retryIO(
description = "permissions",
times = 5,
maxDelay = 5000,
initialDelay = 300
) {
client.permissions()
}
repository.save(server, permissions)
}
} catch (ex: Exception) {
Timber.e(ex, "Error refreshing permissions for: $server")
}
}
}
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment