Commit 59c55366 authored by aniket's avatar aniket

Merge branch 'develop' into aniket/feat/create-new-channel

parents 30e54bd8 b9f51766
......@@ -57,7 +57,10 @@ jobs:
command: ./gradlew lint
- run:
name: Run Unit test
command: echo ./gradlew test # TODO: Fix unit test errors soon...
command: ./gradlew test
- run:
name: Compile Instrumentation test
command: ./gradlew assembleAndroidTest
- store_artifacts:
path: app/build/reports/
destination: reports
......
Your Rocket.Chat.Android version: (make sure you are running the latest)
<!-- Version can be found by opening the side menu and then clicking on the chevron alongside username -->
## Description
<!-- Please, describe what's the issue here. -->
## Devices and Versions
<!-- Version can be found by opening the side menu and then clicking on "Settings" and then "About" -->
Your Rocket.Chat.Android version: (e.g. 2.1.0)
Your Rocket.Chat Server version: (e.g. 0.63.1-develop)
<!-- Found a bug? List all devices that reproduced it and all that doesn't -->
Mobile device model and OS version: (e.g. "Nexus 7 - Android 6.0.1")
<!-- Don't forget to list the steps to reproduce. Stack traces may help too :) -->
## Steps to reproduce
<!-- In case it is a bug, can you describe the steps to reproduce it please? -->
## Logs
<!-- Do you have any logs? It can help the developers indentifying the cause in case it's a bug. -->
<!-- To get the logs, you can use [Logcat](https://developer.android.com/studio/debug/am-logcat.html) in Android Studio or you can use [Pidcat](https://github.com/JakeWharton/pidcat) -->
#.travis.yml
language: android
jdk: oraclejdk8
sudo: required
android:
components: # Cookbooks version: https://github.com/travis-ci/travis-cookbooks/tree/9c6cd11
- tools # Update preinstalled tools from revision 24.0.2 to 24.4.1
- build-tools-25.0.3 # Match build-tools version used in build.gradle
- platform-tools # Update platform-tools to revision 25.0.3+
- tools # Update tools from revision 24.4.1 to 25.2.5
env:
global:
- API=26 # Android API level 26 by default
- TAG=google_apis # Google APIs by default, alternatively use default
- ABI=armeabi-v7a # ARM ABI v7a by default
- QEMU_AUDIO_DRV=none # Disable emulator audio to avoid warning
- ANDROID_HOME=/usr/local/android-sdk # Depends on the cookbooks version used in the VM
- TOOLS=${ANDROID_HOME}/tools # PATH order matters, exists more than one emulator script
- PATH=${ANDROID_HOME}:${ANDROID_HOME}/emulator:${TOOLS}:${TOOLS}/bin:${ANDROID_HOME}/platform-tools:${PATH}
- ADB_INSTALL_TIMEOUT=20 # minutes (2 minutes by default)
install:
# List and delete unnecessary components to free space
- sdkmanager --list || true
- sdkmanager --uninstall "system-images;android-15;default;armeabi-v7a"
# Update sdk tools to latest version and install/update components
- echo yes | sdkmanager "tools"
- echo yes | sdkmanager "platforms;android-26" # Latest platform required by SDK tools
- echo yes | sdkmanager "platforms;android-${API}" # Android platform required by emulator
- echo yes | sdkmanager "extras;android;m2repository"
- echo yes | sdkmanager "extras;google;m2repository"
- echo yes | sdkmanager "extras;m2repository;com;android;support;constraint;constraint-layout;1.0.2"
- echo yes | sdkmanager "extras;m2repository;com;android;support;constraint;constraint-layout-solver;1.0.2"
# - echo yes | sdkmanager "$EMULATOR" # Install emulator system image
# Create and start emulator
# - echo no | avdmanager create avd -n acib -k "$EMULATOR" -f --abi "$ABI" --tag "$TAG"
# - emulator -avd acib -engine classic -no-window -verbose -qemu -m 512 &
before_script:
# - echo y | android update sdk --no-ui --all --filter tools,platform-tools
# - echo y | android update sdk --no-ui --all --filter android-25
# - echo y | android update sdk --no-ui --all --filter extra-android-m2repository,extra-android-support
# - echo y | android update sdk --no-ui --all --filter extra-google-m2repository,extra-google-google_play_services
# - echo y | android update sdk --no-ui --all --filter build-tools-25.0.3
# - echo yes | sdkmanager "extras;m2repository;com;android;support;constraint;constraint-layout;1.0.2"
# - echo yes | sdkmanager "extras;m2repository;com;android;support;constraint;constraint-layout-solver;1.0.2"
- ./gradlew dependencies
script:
- ./gradlew checkstyle findbugs pmd
......@@ -5,34 +5,36 @@
[![CircleCI](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop.svg?style=shield)](https://circleci.com/gh/RocketChat/Rocket.Chat.Android/tree/develop) [![Build Status](https://travis-ci.org/RocketChat/Rocket.Chat.Android.svg?branch=develop)](https://travis-ci.org/RocketChat/Rocket.Chat.Android) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a81156a8682e4649994270d3670c3c83)](https://www.codacy.com/app/matheusjardimb/Rocket.Chat.Android)
## Description
Currently, the app is maintained in two branches, namely `v1+` and `v2+`. The `v1+` is maintained in the `develop` branch and the `v2+` is maintained in the `develop-2.x` branch. The older version is written partially in `java` and `kotlin`, but we intend to write the latest version completely in `kotlin`.
Clone the repository by running `git clone https://github.com/RocketChat/Rocket.Chat.Android.git` in your terminal. To build the v1.0+ of the app, run `git checkout develop` and to build the v2.0+, run `git checkout develop-2.x`.
Since both the versions use `kotlin` for some or all of their classes, following are the common prerequisites for both versions:
This repository contains all the code related to the Android native application of [Rocket.Chat](https://github.com/RocketChat/Rocket.Chat/#about-rocketchat). To send new pull-requests, always use the branch `develop` as base and open an issue with the description of what you want/need to accomplish, if the issue wasn't created yet.
## How to build
- Android Studio 3.0+ comes with built in kotlin support, so install the latest version (3.0+) of Android Studio (recommended). For older versions, you need to manually install kotlin plugin. Go to `File > Settings > Plugins` and search for `kotlin` and install it. You'll need to restart the IDE in order to see the changes.
- Make sure that you have the latest **gradle** and the **android plugin** versions installed. Go to `File > Project Structure > Project` and make sure that you have the latest versions installed. Refer [this](https://developer.android.com/studio/releases/gradle-plugin.html#updating-gradle) to see the compatible versions.
- Kotlin is already configured in the project. To check, go to `Tools > Kotlin > Configure Kotlin in project`. A message saying kotlin is already configured in the project pops up. You can update kotlin to the latest version by going to `Tools > Kotlin > Configure Kotlin updates` and download the latest version of kotlin.
### Instructions specific to version
#### v1.0+
- After checking out to `develop` branch as mentioned above, simply import the project in Android Studio.
### SDK Instructions
#### v2+
- This version requires the [Kotlin SDK](https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK) for Rocket.Chat. Clone the Kotlin SDK in by running `git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git`.
- First, a build is required for the SDK, so that required jar files are generated. Make sure that the android repository and the kotlin sdk have the same immediate parent directory. Change the current directory to `Rocket.Chat.Android/app` and run the `build-sdk.sh` which will result in creating of the required jar file `core*.jar` and `common*.jar` in `Rocket.Chat.Android/app/libs`,by the following steps in your terminal window:
```
cd Rocket.Chat.Android/app
./build-sdk.sh
```
## How to run
### Command Line
- Connect your physical device to your pc via USB or start an emulator. Run `adb devices` in terminal. You should see your device in the list of devices.
- In order to build the debug apk, run `./gradlew assembleDebug`. This would generate a debug apk which can be found under `Rocket.Chat.Android/app/build/outputs/apk/debug` folder with the name `app-debug.apk`.
- In order to build and install the apk directly to the connected device, run `./gradlew installDebug`.
### Android Studio
- After importing the project in android studio, go to `Run > Run app` and then select your device, or create a new virtual device by following the wizard.
## Bug report & Feature request
......
......@@ -13,8 +13,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion 21
targetSdkVersion versions.targetSdk
versionCode 2008
versionName "2.0.0-beta6"
versionCode 2021
versionName "2.2.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......@@ -77,6 +77,8 @@ dependencies {
implementation libraries.room
kapt libraries.roomProcessor
implementation libraries.roomRxjava
implementation libraries.lifecycleExtensions
kapt libraries.lifecycleCompiler
implementation libraries.rxKotlin
implementation libraries.rxAndroid
......@@ -101,7 +103,6 @@ dependencies {
implementation libraries.frescoImageViewer
implementation libraries.markwon
implementation libraries.markwonImageLoader
implementation libraries.sheetMenu
......@@ -112,9 +113,9 @@ dependencies {
}
testImplementation libraries.junit
androidTestImplementation(libraries.expressoCore, {
exclude group: 'com.android.support', module: 'support-annotations'
})
testImplementation libraries.truth
androidTestImplementation libraries.espressoCore
androidTestImplementation libraries.espressoIntents
}
kotlin {
......@@ -123,6 +124,10 @@ kotlin {
}
}
androidExtensions {
experimental = true
}
// 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
task compileSdk(type:Exec) {
......
package chat.rocket.android.chatroom.ui
import android.content.Intent
import android.support.test.espresso.intent.rule.IntentsTestRule
import android.support.test.filters.LargeTest
import org.junit.Rule
import org.junit.Test
import android.app.Activity
import android.app.Instrumentation.ActivityResult
import android.support.test.InstrumentationRegistry
import android.support.test.espresso.intent.Intents.intended
import android.support.test.espresso.intent.Intents.intending
import android.support.test.espresso.intent.matcher.IntentMatchers.*
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.not
import org.junit.Before
@LargeTest
class ChatRoomFragmentTest {
@JvmField
@Rule
val activityRule = IntentsTestRule<ChatRoomActivity>(ChatRoomActivity::class.java, false, false)
@Before
fun stubAllExternalIntents() {
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 filters = arrayOf("image/*")
fragment.showFileSelection(filters)
intended(allOf(
hasAction(Intent.ACTION_GET_CONTENT),
hasType("*/*"),
hasCategories(setOf(Intent.CATEGORY_OPENABLE)),
hasExtra(Intent.EXTRA_MIME_TYPES, filters)))
}
@Test
fun showFileSelection_nullFiltersAreNotApplied() {
val fragment = activityRule.activity.supportFragmentManager.findFragmentByTag(ChatRoomActivity.TAG_CHAT_ROOM_FRAGMENT) as ChatRoomFragment
fragment.showFileSelection(null)
intended(allOf(
hasAction(Intent.ACTION_GET_CONTENT),
hasType("*/*"),
hasCategories(setOf(Intent.CATEGORY_OPENABLE)),
not(hasExtraWithKey(Intent.EXTRA_MIME_TYPES))))
}
}
\ No newline at end of file
......@@ -5,8 +5,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
......@@ -17,10 +17,13 @@
<application
android:name=".app.RocketChatApplication"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true">
<activity
android:name=".authentication.ui.AuthenticationActivity"
android:configChanges="orientation"
......@@ -32,27 +35,46 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="auth"
android:scheme="rocketchat" />
<data
android:host="go.rocket.chat"
android:path="/auth"
android:scheme="https" />
</intent-filter>
</activity>
<activity
android:name=".server.ui.ChangeServerActivity"
android:theme="@style/AuthenticationTheme" />
<activity
android:name=".main.ui.MainActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".webview.ui.WebViewActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".webview.cas.ui.CasWebViewActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".webview.oauth.ui.OauthWebViewActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<activity
android:name=".chatroom.ui.ChatRoomActivity"
android:theme="@style/AppTheme"
......@@ -65,14 +87,17 @@
<activity android:name=".createChannel.addMembers.ui.AddMembersActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name=".chatroom.ui.PinnedMessagesActivity"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<!-- TODO: Change to fragment-->
<activity
android:name=".settings.password.ui.PasswordActivity"
android:theme="@style/AppTheme" />
<!-- TODO: Change to fragment-->
<activity
android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme" />
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
......@@ -108,13 +133,14 @@
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<service
android:name=".chatroom.service.MessageService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<meta-data
android:name="io.fabric.ApiKey"
android:value="12ac6e94f850aaffcdff52001af77ca415d06a43" />
<activity android:name=".settings.about.ui.AboutActivity"
android:theme="@style/AppTheme"/>
</application>
</manifest>
package chat.rocket.android.app
import android.arch.lifecycle.Lifecycle
import android.arch.lifecycle.LifecycleObserver
import android.arch.lifecycle.OnLifecycleEvent
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.common.RocketChatException
import chat.rocket.common.model.UserStatus
import chat.rocket.core.internal.realtime.setTemporaryStatus
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
class AppLifecycleObserver @Inject constructor(
private val serverInteractor: GetCurrentServerInteractor,
private val factory: RocketChatClientFactory,
private val getAccountInteractor: GetAccountInteractor
) : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onEnterForeground() {
changeTemporaryStatus(UserStatus.Online())
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onEnterBackground() {
changeTemporaryStatus(UserStatus.Away())
}
private fun changeTemporaryStatus(userStatus: UserStatus) {
launch {
val currentServer = serverInteractor.get()
val account = currentServer?.let { getAccountInteractor.get(currentServer) }
val client = account?.let { factory.create(currentServer) }
try {
client?.setTemporaryStatus(userStatus)
} catch (exception: RocketChatException) {
Timber.e(exception)
}
}
}
}
\ No newline at end of file
......@@ -2,7 +2,6 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
import android.support.v4.graphics.drawable.DrawableCompat
import android.widget.EditText
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.common.model.UserStatus
......@@ -16,7 +15,8 @@ object DrawableHelper {
* @param context The context.
* @return A drawable.
*/
fun getDrawableFromId(id: Int, context: Context): Drawable = context.resources.getDrawable(id, null)
fun getDrawableFromId(id: Int, context: Context): Drawable =
context.resources.getDrawable(id, null)
/**
* Wraps an array of Drawable to be used for example for tinting.
......@@ -68,7 +68,8 @@ object DrawableHelper {
* @see tintDrawables
* @see wrapDrawable
*/
fun tintDrawable(drawable: Drawable, context: Context, resId: Int) = DrawableCompat.setTint(drawable, ContextCompat.getColor(context, resId))
fun tintDrawable(drawable: Drawable, context: Context, resId: Int) =
DrawableCompat.setTint(drawable, ContextCompat.getColor(context, resId))
/**
* Compounds an array of Drawable (to appear to the left of the text) into an array of TextView.
......@@ -96,55 +97,23 @@ object DrawableHelper {
* @param drawable The Drawable.
* @see compoundDrawables
*/
fun compoundDrawable(textView: TextView, drawable: Drawable) = textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
fun compoundDrawable(textView: TextView, drawable: Drawable) =
textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
/**
* Returns the user status drawable.
*
* @param userStatus The user status.
* @param context The context.
* @see [UserStatus]
* @return The user status drawable.
*/
fun getUserStatusDrawable(userStatus: UserStatus, context: Context): Drawable {
fun getUserStatusDrawable(userStatus: UserStatus?, context: Context): Drawable {
return when (userStatus) {
is UserStatus.Online -> {
getDrawableFromId(R.drawable.ic_status_online_24dp, context)
}
is UserStatus.Away -> {
getDrawableFromId(R.drawable.ic_status_away_24dp, context)
}
is UserStatus.Busy -> {
getDrawableFromId(R.drawable.ic_status_busy_24dp, context)
}
else -> getDrawableFromId(R.drawable.ic_status_invisible_24dp, context)
}
}
// TODO Why we need two UserStatus?
/**
* Returns the user status drawable.
*
* @param userStatus The user status.
* @param context The context.
* @sse [chat.rocket.core.internal.realtime.UserStatus]
* @return The user status drawable.
*/
fun getUserStatusDrawable(
userStatus: chat.rocket.core.internal.realtime.UserStatus,
context: Context
): Drawable {
return when (userStatus) {
is chat.rocket.core.internal.realtime.UserStatus.Online -> {
getDrawableFromId(R.drawable.ic_status_online_24dp, context)
}
is chat.rocket.core.internal.realtime.UserStatus.Away -> {
getDrawableFromId(R.drawable.ic_status_away_24dp, context)
}
is chat.rocket.core.internal.realtime.UserStatus.Busy -> {
getDrawableFromId(R.drawable.ic_status_busy_24dp, context)
}
else -> getDrawableFromId(R.drawable.ic_status_invisible_24dp, context)
is UserStatus.Online -> getDrawableFromId(R.drawable.ic_status_online_12dp, context)
is UserStatus.Away -> getDrawableFromId(R.drawable.ic_status_away_12dp, context)
is UserStatus.Busy -> getDrawableFromId(R.drawable.ic_status_busy_12dp, context)
else -> getDrawableFromId(R.drawable.ic_status_invisible_12dp, context)
}
}
}
\ No newline at end of file
......@@ -3,6 +3,7 @@ package chat.rocket.android.app
import android.app.Activity
import android.app.Application
import android.app.Service
import android.arch.lifecycle.ProcessLifecycleOwner
import android.content.BroadcastReceiver
import android.content.Context
import android.content.SharedPreferences
......@@ -17,6 +18,7 @@ import chat.rocket.android.app.migration.model.RealmSession
import chat.rocket.android.app.migration.model.RealmUser
import chat.rocket.android.authentication.domain.model.toToken
import chat.rocket.android.dagger.DaggerAppComponent
import chat.rocket.android.dagger.qualifier.ForMessages
import chat.rocket.android.helper.CrashlyticsTree
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
......@@ -43,10 +45,12 @@ import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject
class RocketChatApplication : Application(), HasActivityInjector, HasServiceInjector,
HasBroadcastReceiverInjector {
@Inject
lateinit var appLifecycleObserver: AppLifecycleObserver
@Inject
lateinit var activityDispatchingAndroidInjector: DispatchingAndroidInjector<Activity>
......@@ -81,10 +85,21 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
@Inject
lateinit var localRepository: LocalRepository
@Inject
@field:ForMessages
lateinit var messagesPrefs: SharedPreferences
override fun onCreate() {
super.onCreate()
DaggerAppComponent.builder().application(this).build().inject(this)
DaggerAppComponent.builder()
.application(this)
.build()
.inject(this)
ProcessLifecycleOwner.get()
.lifecycle
.addObserver(appLifecycleObserver)
// TODO - remove this on the future, temporary migration stuff for pre-release versions.
migrateInternalTokens()
......@@ -97,6 +112,13 @@ class RocketChatApplication : Application(), HasActivityInjector, HasServiceInje
setupFresco()
setupTimber()
if (localRepository.needOldMessagesCleanUp()) {
messagesPrefs.edit {
clear()
}
localRepository.setOldMessagesCleanedUp()
}
// TODO - remove this and all realm stuff when we got to 80% in 2.0
try {
if (!localRepository.hasMigrated()) {
......@@ -276,5 +298,9 @@ private fun LocalRepository.setMigrated(migrated: Boolean) {
}
private fun LocalRepository.hasMigrated() = getBoolean(LocalRepository.MIGRATION_FINISHED_KEY)
private fun LocalRepository.needOldMessagesCleanUp() = getBoolean(CLEANUP_OLD_MESSAGES_NEEDED, true)
private fun LocalRepository.setOldMessagesCleanedUp() = save(CLEANUP_OLD_MESSAGES_NEEDED, false)
private const val INTERNAL_TOKEN_MIGRATION_NEEDED = "INTERNAL_TOKEN_MIGRATION_NEEDED"
private const val CLEANUP_OLD_MESSAGES_NEEDED = "CLEANUP_OLD_MESSAGES_NEEDED"
\ No newline at end of file
package chat.rocket.android.authentication.domain.model
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
@SuppressLint("ParcelCreator")
@Parcelize
data class LoginDeepLinkInfo(
val url: String,
val userId: String,
val token: String
) : Parcelable
fun Intent.getLoginDeepLinkInfo(): LoginDeepLinkInfo? {
val uri = data
return if (action == Intent.ACTION_VIEW && uri != null && uri.isAuthenticationDeepLink()) {
val host = uri.getQueryParameter("host")
val url = if (host.startsWith("http")) host else "https://$host"
val userId = uri.getQueryParameter("userId")
val token = uri.getQueryParameter("token")
try {
LoginDeepLinkInfo(url, userId, token)
} catch (ex: Exception) {
Timber.d(ex, "Error parsing login deeplink")
null
}
} else null
}
private inline fun Uri.isAuthenticationDeepLink(): Boolean {
if (host == "auth")
return true
else if (host == "go.rocket.chat" && path == "/auth")
return true
return false
}
\ No newline at end of file
package chat.rocket.android.authentication.login.presentation
import chat.rocket.android.authentication.server.presentation.VersionCheckView
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface LoginView : LoadingView, MessageView, VersionCheckView {
interface LoginView : LoadingView, MessageView {
/**
* Shows the form view (i.e the username/email and password fields) if it is enabled by the server settings.
......@@ -66,6 +65,18 @@ interface LoginView : LoadingView, MessageView, VersionCheckView {
*/
fun setupSignUpView()
/**
* Shows the forgot password view if enabled by the server settings.
*
* REMARK: We must set up the forgot password view listener [setupForgotPasswordView].
*/
fun showForgotPasswordView()
/**
* Setups the forgot password view when tapped.
*/
fun setupForgotPasswordView()
/**
* Hides the sign up view.
*/
......@@ -75,7 +86,7 @@ interface LoginView : LoadingView, MessageView, VersionCheckView {
* Enables and shows the oauth view if there is login via social accounts enabled by the server settings.
*
* REMARK: We must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle],
* [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter] or [enableLoginByGitlab]) for the oauth view.
* [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter], [enableLoginByGitlab] or [addCustomOauthServiceButton]) for the oauth view.
* If the possibility of login via social accounts exceeds 3 different ways we should set up the FAB ([setupFabListener]) to show the remaining view(s).
*/
fun enableOauthView()
......@@ -145,6 +156,14 @@ interface LoginView : LoadingView, MessageView, VersionCheckView {
*/
fun setupLinkedinButtonListener(linkedinUrl: String, state: String)
/**
* Setups the Facebook button when tapped.
*
* @param facebookOauthUrl The Facebook OAuth URL to authenticate with.
* @param state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
*/
fun setupFacebookButtonListener(facebookOauthUrl: String, state: String)
/**
* Shows the "login by Meteor" view if it is enable by the server settings.
*/
......@@ -170,6 +189,24 @@ interface LoginView : LoadingView, MessageView, VersionCheckView {
*/
fun setupGitlabButtonListener(gitlabUrl: String, state: String)
/**
* Adds a custom OAuth button in the oauth view.
*
* @customOauthUrl The custom OAuth url to sets up the button (the listener).
* @state A random string generated by the app, which you'll verify later (to protect against forgery attacks).
* @serviceName The custom OAuth service name.
* @serviceNameColor The custom OAuth service name color (just stylizing).
* @buttonColor The color of the custom OAuth button (just stylizing).
* @see [enableOauthView]
*/
fun addCustomOauthServiceButton(
customOauthUrl: String,
state: String,
serviceName: String,
serviceNameColor: Int,
buttonColor: Int
)
/**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*/
......
......@@ -2,8 +2,10 @@ package chat.rocket.android.authentication.presentation
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.registerusername.ui.RegisterUsernameFragment
import chat.rocket.android.authentication.resetpassword.ui.ResetPasswordFragment
import chat.rocket.android.authentication.signup.ui.SignupFragment
import chat.rocket.android.authentication.twofactor.ui.TwoFAFragment
import chat.rocket.android.authentication.ui.AuthenticationActivity
......@@ -11,6 +13,7 @@ import chat.rocket.android.authentication.ui.newServerIntent
import chat.rocket.android.main.ui.MainActivity
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack
import chat.rocket.android.util.extensions.toPreviousView
import chat.rocket.android.webview.ui.webViewIntent
class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
......@@ -21,6 +24,16 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
}
}
fun toLogin(deepLinkInfo: LoginDeepLinkInfo) {
activity.addFragmentBackStack("LoginFragment", R.id.fragment_container) {
LoginFragment.newInstance(deepLinkInfo)
}
}
fun toPreviousView() {
activity.toPreviousView()
}
fun toTwoFA(username: String, password: String) {
activity.addFragmentBackStack("TwoFAFragment", R.id.fragment_container) {
TwoFAFragment.newInstance(username, password)
......@@ -33,6 +46,12 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
}
}
fun toForgotPassword() {
activity.addFragmentBackStack("ResetPasswordFragment", R.id.fragment_container) {
ResetPasswordFragment.newInstance()
}
}
fun toWebPage(url: String) {
activity.startActivity(activity.webViewIntent(url))
activity.overridePendingTransition(R.anim.slide_up, R.anim.hold)
......
......@@ -16,7 +16,8 @@ import kotlinx.android.synthetic.main.fragment_authentication_register_username.
import javax.inject.Inject
class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
@Inject lateinit var presenter: RegisterUsernamePresenter
@Inject
lateinit var presenter: RegisterUsernamePresenter
private lateinit var userId: String
private lateinit var authToken: String
......@@ -41,7 +42,11 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
authToken = arguments?.getString(AUTH_TOKEN) ?: ""
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_register_username)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_authentication_register_username)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -59,37 +64,47 @@ class RegisterUsernameFragment : Fragment(), RegisterUsernameView {
}
override fun alertBlankUsername() {
ui {
vibrateSmartPhone()
text_username.shake()
}
}
override fun showLoading() {
ui {
disableUserInput()
view_loading.setVisible(true)
}
}
override fun hideLoading() {
ui {
view_loading.setVisible(false)
enableUserInput()
}
}
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
private fun tintEditTextDrawableStart() {
activity?.apply {
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
ui {
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, it)
DrawableHelper.wrapDrawable(atDrawable)
DrawableHelper.tintDrawable(atDrawable, this, R.color.colorDrawableTintGrey)
DrawableHelper.tintDrawable(atDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_username, atDrawable)
}
}
......
package chat.rocket.android.authentication.resetpassword.di
import android.arch.lifecycle.LifecycleOwner
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordView
import chat.rocket.android.authentication.resetpassword.ui.ResetPasswordFragment
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.dagger.scope.PerFragment
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.experimental.Job
@Module
@PerFragment
class ResetPasswordFragmentModule {
@Provides
fun resetPasswordView(frag: ResetPasswordFragment): ResetPasswordView {
return frag
}
@Provides
fun provideLifecycleOwner(frag: ResetPasswordFragment): LifecycleOwner {
return frag
}
@Provides
fun provideCancelStrategy(owner: LifecycleOwner, jobs: Job): CancelStrategy {
return CancelStrategy(owner, jobs)
}
}
\ No newline at end of file
package chat.rocket.android.authentication.resetpassword.di
import chat.rocket.android.authentication.resetpassword.ui.ResetPasswordFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module
abstract class ResetPasswordFragmentProvider {
@ContributesAndroidInjector(modules = [ResetPasswordFragmentModule::class])
abstract fun provideResetPasswordFragment(): ResetPasswordFragment
}
\ No newline at end of file
package chat.rocket.android.authentication.resetpassword.presentation
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.isEmail
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatException
import chat.rocket.common.RocketChatInvalidResponseException
import chat.rocket.common.util.ifNull
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.forgotPassword
import javax.inject.Inject
class ResetPasswordPresenter @Inject constructor(
private val view: ResetPasswordView,
private val strategy: CancelStrategy,
private val navigator: AuthenticationNavigator,
factory: RocketChatClientFactory,
serverInteractor: GetCurrentServerInteractor
) {
private val currentServer = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(currentServer)
fun resetPassword(email: String) {
when {
email.isBlank() -> view.alertBlankEmail()
!email.isEmail() -> view.alertInvalidEmail()
else -> launchUI(strategy) {
view.showLoading()
try {
retryIO("forgotPassword(email = $email)") {
client.forgotPassword(email)
}
navigator.toPreviousView()
view.emailSent()
} catch (exception: RocketChatException) {
if (exception is RocketChatInvalidResponseException) {
view.updateYourServerVersion()
} else {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
} finally {
view.hideLoading()
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.authentication.resetpassword.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface ResetPasswordView : LoadingView, MessageView {
/**
* Alerts the user about a blank email.
*/
fun alertBlankEmail()
/**
* Alerts the user about a invalid email.
*/
fun alertInvalidEmail()
/**
* Shows a successful email sent message.
*/
fun emailSent()
/**
* Shows a message to update the server version in order to use an app feature.
*/
fun updateYourServerVersion()
}
\ No newline at end of file
package chat.rocket.android.authentication.resetpassword.ui
import DrawableHelper
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import chat.rocket.android.R
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordPresenter
import chat.rocket.android.authentication.resetpassword.presentation.ResetPasswordView
import chat.rocket.android.util.extensions.*
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_reset_password.*
import javax.inject.Inject
class ResetPasswordFragment : Fragment(), ResetPasswordView {
@Inject
lateinit var presenter: ResetPasswordPresenter
companion object {
fun newInstance() = ResetPasswordFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_authentication_reset_password)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity?.apply {
text_email.requestFocus()
showKeyboard(text_email)
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
tintEditTextDrawableStart()
}
setupOnClickListener()
}
override fun alertBlankEmail() {
ui {
vibrateShakeAndRequestFocusForTextEmail()
}
}
override fun alertInvalidEmail() {
ui {
vibrateShakeAndRequestFocusForTextEmail()
showMessage(R.string.msg_invalid_email)
}
}
override fun emailSent() {
showToast(R.string.msg_check_your_email_to_reset_your_password, Toast.LENGTH_LONG)
}
override fun updateYourServerVersion() {
showMessage(R.string.msg_update_app_version_in_order_to_continue)
}
override fun showLoading() {
ui {
disableUserInput()
view_loading.setVisible(true)
}
}
override fun hideLoading() {
ui {
view_loading.setVisible(false)
enableUserInput()
}
}
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
private fun tintEditTextDrawableStart() {
ui {
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, it)
DrawableHelper.wrapDrawable(emailDrawable)
DrawableHelper.tintDrawable(emailDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_email, emailDrawable)
}
}
private fun enableUserInput() {
button_reset_password.isEnabled = true
text_email.isEnabled = true
}
private fun disableUserInput() {
button_reset_password.isEnabled = false
text_email.isEnabled = true
}
private fun vibrateShakeAndRequestFocusForTextEmail() {
vibrateSmartPhone()
text_email.shake()
text_email.requestFocus()
}
private fun setupOnClickListener() {
button_reset_password.setOnClickListener {
presenter.resetPassword(text_email.textContent)
}
}
}
\ No newline at end of file
package chat.rocket.android.authentication.server.presentation
import chat.rocket.android.authentication.domain.model.LoginDeepLinkInfo
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetAccountsInteractor
import chat.rocket.android.server.domain.RefreshSettingsInteractor
import chat.rocket.android.server.domain.SaveCurrentServerInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.server.presentation.CheckServerPresenter
import chat.rocket.android.util.extensions.isValidUrl
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.util.ifNull
import javax.inject.Inject
class ServerPresenter @Inject constructor(private val view: ServerView,
......@@ -15,8 +18,26 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
private val navigator: AuthenticationNavigator,
private val serverInteractor: SaveCurrentServerInteractor,
private val refreshSettingsInteractor: RefreshSettingsInteractor,
private val getAccountsInteractor: GetAccountsInteractor) {
private val getAccountsInteractor: GetAccountsInteractor,
factory: RocketChatClientFactory
) : CheckServerPresenter(strategy, factory, view) {
fun checkServer(server: String) {
if (!server.isValidUrl()) {
view.showInvalidServerUrlMessage()
} else {
view.showLoading()
checkServerInfo(server)
}
}
fun connect(server: String) {
connectToServer(server) {
navigator.toLogin()
}
}
private fun connectToServer(server: String, block: () -> Unit) {
if (!server.isValidUrl()) {
view.showInvalidServerUrlMessage()
} else {
......@@ -32,17 +53,19 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
try {
refreshSettingsInteractor.refresh(server)
serverInteractor.save(server)
navigator.toLogin()
block()
} catch (ex: Exception) {
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
view.showMessage(ex)
} finally {
view.hideLoading()
}
}
}
}
fun deepLink(deepLinkInfo: LoginDeepLinkInfo) {
connectToServer(deepLinkInfo.url) {
navigator.toLogin(deepLinkInfo)
}
}
}
\ No newline at end of file
......@@ -3,7 +3,7 @@ package chat.rocket.android.authentication.server.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
interface ServerView : LoadingView, MessageView {
interface ServerView : LoadingView, MessageView, VersionCheckView {
/**
* Shows an invalid server URL message.
......
package chat.rocket.android.authentication.server.presentation
import okhttp3.HttpUrl
interface VersionCheckView {
/**
* Alerts the user about the server version not meeting the recommended server version.
......@@ -10,4 +12,25 @@ interface VersionCheckView {
* Block user to proceed and alert him due to server having an unsupported server version.
*/
fun blockAndAlertNotRequiredVersion()
/**
* Alerts the user that an error has occurred while checking the server version
* This is optional.
*/
fun errorCheckingServerVersion() {}
/**
* Do some action if version is ok. This is optional.
*/
fun versionOk() {}
/**
* Alters the user this protocol is invalid. This is optional.
*/
fun errorInvalidProtocol() {}
/**
* Updates the server URL after a URL redirection
*/
fun updateServerUrl(url: HttpUrl) {}
}
\ No newline at end of file
package chat.rocket.android.authentication.server.ui
import android.app.AlertDialog
import android.net.Uri
import android.os.Bundle
import android.support.v4.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 chat.rocket.android.BuildConfig
import chat.rocket.android.R
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.common.util.ifNull
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_server.*
import okhttp3.HttpUrl
import javax.inject.Inject
class ServerFragment : Fragment(), ServerView {
@Inject lateinit var presenter: ServerPresenter
@Inject
lateinit var presenter: ServerPresenter
private var deepLinkInfo: LoginDeepLinkInfo? = null
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
text_server_url.isCursorVisible = KeyboardHelper.isSoftKeyboardShown(relative_layout.rootView)
}
companion object {
fun newInstance() = ServerFragment()
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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
deepLinkInfo = arguments?.getParcelable(DEEP_LINK_INFO)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
......@@ -37,6 +58,48 @@ class ServerFragment : Fragment(), ServerView {
super.onViewCreated(view, savedInstanceState)
relative_layout.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
setupOnClickListener()
deepLinkInfo?.let {
val uri = Uri.parse(it.url)
uri?.let { text_server_url.hintContent = it.host }
presenter.deepLink(it)
}
text_server_protocol.adapter = ArrayAdapter<String>(activity,
android.R.layout.simple_dropdown_item_1line, arrayOf("https://", "http://"))
text_server_protocol.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when(position) {
0 -> {
protocol = "https://"
}
1 -> {
if (ignoreChange) {
protocol = "http://"
} else {
ui {
AlertDialog.Builder(it)
.setTitle(R.string.msg_warning)
.setMessage(R.string.msg_http_insecure)
.setPositiveButton(R.string.msg_proceed) { _, _ ->
protocol = "http://"
}
.setNegativeButton(R.string.msg_cancel) { _, _ ->
text_server_protocol.setSelection(0)
}
.setCancelable(false)
.create()
.show()
}
}
}
}
ignoreChange = false
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
override fun onDestroyView() {
......@@ -47,36 +110,109 @@ class ServerFragment : Fragment(), ServerView {
override fun showInvalidServerUrlMessage() = showMessage(getString(R.string.msg_invalid_server_url))
override fun showLoading() {
ui {
enableUserInput(false)
view_loading.setVisible(true)
}
}
override fun hideLoading() {
ui {
view_loading.setVisible(false)
enableUserInput(true)
}
}
override fun showMessage(resId: Int){
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
override fun alertNotRecommendedVersion() {
ui {
hideLoading()
AlertDialog.Builder(it)
.setMessage(getString(R.string.msg_ver_not_recommended, BuildConfig.RECOMMENDED_SERVER_VERSION))
.setPositiveButton(R.string.msg_ok, { _, _ ->
performConnect()
})
.create()
.show()
}
}
override fun blockAndAlertNotRequiredVersion() {
ui {
hideLoading()
AlertDialog.Builder(it)
.setMessage(getString(R.string.msg_ver_not_minimum, BuildConfig.REQUIRED_SERVER_VERSION))
.setPositiveButton(R.string.msg_ok, null)
.setOnDismissListener {
// reset the deeplink info, so the user can log to another server...
deepLinkInfo = null
}
.create()
.show()
}
}
override fun versionOk() {
performConnect()
}
override fun errorCheckingServerVersion() {
hideLoading()
showMessage(R.string.msg_error_checking_server_version)
}
override fun errorInvalidProtocol() {
hideLoading()
showMessage(R.string.msg_invalid_server_protocol)
}
override fun updateServerUrl(url: HttpUrl) {
if (activity != null && view != null) {
if (url.scheme() == "https") text_server_protocol.setSelection(0) else text_server_protocol.setSelection(1)
protocol = "${url.scheme()}://"
val serverUrl = url.toString().removePrefix("${url.scheme()}://")
text_server_url.textContent = serverUrl
}
}
private fun performConnect() {
ui {
deepLinkInfo?.let {
presenter.deepLink(it)
}.ifNull {
val url = text_server_url.textContent.ifEmpty(text_server_url.hintContent)
presenter.connect("$protocol${url.sanitize()}")
}
}
}
private fun enableUserInput(value: Boolean) {
button_connect.isEnabled = value
text_server_url.isEnabled = value
}
private fun setupOnClickListener() {
ui {
button_connect.setOnClickListener {
val url = text_server_url.textContent.ifEmpty(text_server_url.hintContent)
presenter.connect(text_server_protocol.textContent + url)
presenter.checkServer("${protocol}${url.sanitize()}")
}
}
}
}
\ No newline at end of file
......@@ -25,7 +25,7 @@ class SignupFragment : Fragment(), SignupView {
} else {
bottom_container.apply {
postDelayed({
setVisible(true)
ui { setVisible(true) }
}, 3)
}
}
......@@ -64,61 +64,77 @@ class SignupFragment : Fragment(), SignupView {
}
override fun alertBlankName() {
ui {
vibrateSmartPhone()
text_name.shake()
text_name.requestFocus()
}
}
override fun alertBlankUsername() {
ui {
vibrateSmartPhone()
text_username.shake()
text_username.requestFocus()
}
}
override fun alertEmptyPassword() {
ui {
vibrateSmartPhone()
text_password.shake()
text_password.requestFocus()
}
}
override fun alertBlankEmail() {
ui {
vibrateSmartPhone()
text_email.shake()
text_email.requestFocus()
}
}
override fun showLoading() {
ui {
enableUserInput(false)
view_loading.setVisible(true)
}
}
override fun hideLoading() {
ui {
view_loading.setVisible(false)
enableUserInput(true)
}
}
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() {
showMessage(getString(R.string.msg_generic_error))
}
private fun tintEditTextDrawableStart() {
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, this)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, this)
ui {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_person_black_24dp, it)
val atDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_at_black_24dp, it)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, it)
val emailDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_email_black_24dp, it)
val drawables = arrayOf(personDrawable, atDrawable, lockDrawable, emailDrawable)
DrawableHelper.wrapDrawables(drawables)
DrawableHelper.tintDrawables(drawables, this, R.color.colorDrawableTintGrey)
DrawableHelper.tintDrawables(drawables, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawables(arrayOf(text_name, text_username, text_password, text_email), drawables)
}
}
......
......@@ -28,7 +28,7 @@ class TwoFAFragment : Fragment(), TwoFAView {
private const val PASSWORD = "password"
fun newInstance(username: String, password: String) = TwoFAFragment().apply {
arguments = Bundle(1).apply {
arguments = Bundle(2).apply {
putString(USERNAME, username)
putString(PASSWORD, password)
}
......@@ -63,39 +63,49 @@ class TwoFAFragment : Fragment(), TwoFAView {
}
override fun alertBlankTwoFactorAuthenticationCode() {
ui {
vibrateSmartPhone()
text_two_factor_auth.shake()
}
}
override fun alertInvalidTwoFactorAuthenticationCode() {
showMessage(getString(R.string.msg_invalid_2fa_code))
}
override fun showLoading() {
ui {
enableUserInput(false)
view_loading.setVisible(true)
}
}
override fun hideLoading() {
ui {
view_loading.setVisible(false)
enableUserInput(true)
}
}
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
}
override fun showMessage(message: String) {
ui {
showToast(message)
}
}
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
private fun tintEditTextDrawableStart() {
activity?.apply {
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, this)
ui {
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_vpn_key_black_24dp, it)
DrawableHelper.wrapDrawable(lockDrawable)
DrawableHelper.tintDrawable(lockDrawable, this, R.color.colorDrawableTintGrey)
DrawableHelper.tintDrawable(lockDrawable, it, R.color.colorDrawableTintGrey)
DrawableHelper.compoundDrawable(text_two_factor_auth, lockDrawable)
}
}
......
......@@ -6,10 +6,11 @@ import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
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.util.extensions.addFragment
import chat.rocket.android.util.extensions.launchUI
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
......@@ -30,11 +31,13 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
setTheme(R.style.AuthenticationTheme)
super.onCreate(savedInstanceState)
val deepLinkInfo = intent.getLoginDeepLinkInfo()
launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
presenter.loadCredentials(newServer) { authenticated ->
// if we got authenticateWithDeepLink information, pass true to newServer also
presenter.loadCredentials(newServer || deepLinkInfo != null) { authenticated ->
if (!authenticated) {
showServerInput(savedInstanceState)
showServerInput(savedInstanceState, deepLinkInfo)
}
}
}
......@@ -49,9 +52,9 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector
}
fun showServerInput(savedInstanceState: Bundle?) {
fun showServerInput(savedInstanceState: Bundle?, deepLinkInfo: LoginDeepLinkInfo?) {
addFragment("ServerFragment", R.id.fragment_container) {
ServerFragment.newInstance()
ServerFragment.newInstance(deepLinkInfo)
}
}
}
......
......@@ -14,10 +14,9 @@ class AudioAttachmentViewHolder(itemView: View,
init {
with(itemView) {
setupActionMenu(attachment_container)
image_attachment.setVisible(false)
audio_video_attachment.setVisible(true)
setupActionMenu(attachment_container)
setupActionMenu(audio_video_attachment)
}
}
......
......@@ -19,12 +19,9 @@ class AuthorAttachmentViewHolder(itemView: View,
init {
with(itemView) {
setupActionMenu(author_attachment_container)
setupActionMenu(text_fields)
setupActionMenu(text_author_name)
}
}
override fun bindViews(data: AuthorAttachmentViewModel) {
with(itemView) {
data.icon?.let { icon ->
......
......@@ -3,6 +3,8 @@ package chat.rocket.android.chatroom.adapter
import android.support.v7.widget.RecyclerView
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.bottomsheet.BottomSheetMenu
import chat.rocket.android.chatroom.ui.bottomsheet.adapter.ActionListAdapter
......@@ -16,7 +18,6 @@ import com.google.android.flexbox.FlexboxLayoutManager
import ru.whalemare.sheetmenu.extension.inflate
import ru.whalemare.sheetmenu.extension.toList
abstract class BaseViewHolder<T : BaseViewModel<*>>(
itemView: View,
private val listener: ActionsListener,
......@@ -74,23 +75,36 @@ abstract class BaseViewHolder<T : BaseViewModel<*>>(
fun onActionSelected(item: MenuItem, message: Message)
}
val longClickListener = { view: View ->
private val onClickListener = { view: View ->
if (data?.message?.isSystemMessage() == false) {
data?.message?.let {
val menuItems = view.context.inflate(R.menu.message_actions).toList()
menuItems.find { it.itemId == R.id.action_menu_msg_pin_unpin }?.apply {
val isPinned = data?.message?.pinned ?: false
setTitle(if (isPinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = isPinned
menuItems.find { it.itemId == R.id.action_message_unpin }?.apply {
setTitle(if (it.pinned) R.string.action_msg_unpin else R.string.action_msg_pin)
isChecked = it.pinned
}
menuItems.find { it.itemId == R.id.action_message_star }?.apply {
val isStarred = it.starred?.isNotEmpty() ?: false
setTitle(if (isStarred) R.string.action_msg_unstar else R.string.action_msg_star)
isChecked = isStarred
}
val adapter = ActionListAdapter(menuItems, this@BaseViewHolder)
BottomSheetMenu(adapter).show(view.context)
}
true
}
}
internal fun setupActionMenu(view: View) {
if (listener.isActionsEnabled()) {
view.setOnLongClickListener(longClickListener)
view.setOnClickListener(onClickListener)
if (view is ViewGroup) {
for (child in view.children) {
if (child !is RecyclerView && child.id != R.id.recycler_view_reactions) {
setupActionMenu(child)
}
}
}
}
}
......
......@@ -5,7 +5,21 @@ import android.view.MenuItem
import android.view.ViewGroup
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomPresenter
import chat.rocket.android.chatroom.viewmodel.*
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.chatroom.viewmodel.AudioAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.AuthorAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.BaseFileAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.BaseViewModel
import chat.rocket.android.chatroom.viewmodel.ColorAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.GenericFileAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.MessageReplyViewModel
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel
import chat.rocket.android.chatroom.viewmodel.VideoAttachmentViewModel
import chat.rocket.android.chatroom.viewmodel.toViewType
import chat.rocket.android.main.presentation.MainNavigator
import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.Message
......@@ -57,6 +71,20 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_author_attachment)
AuthorAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.COLOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_color_attachment)
ColorAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.GENERIC_FILE_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_file_attachment)
GenericFileAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseViewModel.ViewType.MESSAGE_REPLY -> {
val view = parent.inflate(R.layout.item_message_reply)
MessageReplyViewHolder(view, actionsListener, reactionListener) { roomName, permalink ->
presenter?.openDirectMessage(roomName, permalink)
}
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
......@@ -90,13 +118,26 @@ class ChatRoomAdapter(
}
when (holder) {
is MessageViewHolder -> holder.bind(dataSet[position] as MessageViewModel)
is ImageAttachmentViewHolder -> holder.bind(dataSet[position] as ImageAttachmentViewModel)
is AudioAttachmentViewHolder -> holder.bind(dataSet[position] as AudioAttachmentViewModel)
is VideoAttachmentViewHolder -> holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder -> holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder -> holder.bind(dataSet[position] as MessageAttachmentViewModel)
is AuthorAttachmentViewHolder -> holder.bind(dataSet[position] as AuthorAttachmentViewModel)
is MessageViewHolder ->
holder.bind(dataSet[position] as MessageViewModel)
is ImageAttachmentViewHolder ->
holder.bind(dataSet[position] as ImageAttachmentViewModel)
is AudioAttachmentViewHolder ->
holder.bind(dataSet[position] as AudioAttachmentViewModel)
is VideoAttachmentViewHolder ->
holder.bind(dataSet[position] as VideoAttachmentViewModel)
is UrlPreviewViewHolder ->
holder.bind(dataSet[position] as UrlPreviewViewModel)
is MessageAttachmentViewHolder ->
holder.bind(dataSet[position] as MessageAttachmentViewModel)
is AuthorAttachmentViewHolder ->
holder.bind(dataSet[position] as AuthorAttachmentViewModel)
is ColorAttachmentViewHolder ->
holder.bind(dataSet[position] as ColorAttachmentViewModel)
is GenericFileAttachmentViewHolder ->
holder.bind(dataSet[position] as GenericFileAttachmentViewModel)
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyViewModel)
}
}
......@@ -117,12 +158,22 @@ class ChatRoomAdapter(
}
fun prependData(dataSet: List<BaseViewModel<*>>) {
val item = dataSet.firstOrNull { newItem ->
val item = dataSet.indexOfFirst { newItem ->
this.dataSet.indexOfFirst { it.messageId == newItem.messageId && it.viewType == newItem.viewType } > -1
}
if (item == null) {
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)
}
}
}
}
......@@ -160,26 +211,40 @@ class ChatRoomAdapter(
}
}
val actionsListener = object : BaseViewHolder.ActionsListener {
private val actionsListener = object : BaseViewHolder.ActionsListener {
override fun isActionsEnabled(): Boolean = enableActions
override fun onActionSelected(item: MenuItem, message: Message) {
message.apply {
when (item.itemId) {
R.id.action_menu_msg_delete -> presenter?.deleteMessage(roomId, id)
R.id.action_menu_msg_quote -> presenter?.citeMessage(roomType, roomName, id, false)
R.id.action_menu_msg_reply -> presenter?.citeMessage(roomType, roomName, id, true)
R.id.action_menu_msg_copy -> presenter?.copyMessage(id)
R.id.action_menu_msg_edit -> presenter?.editMessage(roomId, id, message.message)
R.id.action_menu_msg_pin_unpin -> {
with(item) {
if (!isChecked) {
R.id.action_message_reply -> {
presenter?.citeMessage(roomName, roomType, id, true)
}
R.id.action_message_quote -> {
presenter?.citeMessage(roomName, roomType, id, false)
}
R.id.action_message_copy -> {
presenter?.copyMessage(id)
}
R.id.action_message_edit -> {
presenter?.editMessage(roomId, id, message.message)
}
R.id.action_message_star -> {
if (!item.isChecked) {
presenter?.starMessage(id)
} else {
presenter?.unstarMessage(id)
}
}
R.id.action_message_unpin -> {
if (!item.isChecked) {
presenter?.pinMessage(id)
} else {
presenter?.unpinMessage(id)
}
}
}
R.id.action_message_delete -> presenter?.deleteMessage(roomId, id)
R.id.action_menu_msg_react -> presenter?.showReactions(id)
else -> TODO("Not implemented")
}
......
package chat.rocket.android.chatroom.adapter
import android.graphics.drawable.Drawable
import android.support.v4.content.ContextCompat
import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.ColorAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_color_attachment.view.*
class ColorAttachmentViewHolder(itemView: View,
listener: BaseViewHolder.ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ColorAttachmentViewModel>(itemView, listener, reactionListener) {
val drawable: Drawable? = ContextCompat.getDrawable(itemView.context,
R.drawable.quote_vertical_bar)
init {
with(itemView) {
setupActionMenu(color_attachment_container)
attachment_text.movementMethod = LinkMovementMethod()
}
}
override fun bindViews(data: ColorAttachmentViewModel) {
with(itemView) {
drawable?.let {
quote_bar.background = drawable.mutate().apply { setTint(data.color) }
attachment_text.text = data.text
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.net.Uri
import android.view.View
import chat.rocket.android.chatroom.viewmodel.GenericFileAttachmentViewModel
import chat.rocket.android.util.extensions.content
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.common.util.ifNull
import kotlinx.android.synthetic.main.item_file_attachment.view.*
class GenericFileAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<GenericFileAttachmentViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(file_attachment_container)
}
}
override fun bindViews(data: GenericFileAttachmentViewModel) {
with(itemView) {
text_file_name.content = data.attachmentTitle
text_file_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.attachmentUrl)))
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.Manifest
import android.app.Activity
import android.graphics.Color
import android.graphics.Typeface
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Environment
import android.support.design.widget.AppBarLayout
import android.support.v7.widget.Toolbar
import android.text.TextUtils
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.setPadding
import chat.rocket.android.R
import chat.rocket.android.chatroom.viewmodel.ImageAttachmentViewModel
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.interfaces.DraweeController
import chat.rocket.android.helper.AndroidPermissionsHelper
import chat.rocket.android.widget.emoji.EmojiReactionListener
import com.facebook.binaryresource.FileBinaryResource
import com.facebook.cache.common.CacheKey
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imageformat.ImageFormatChecker
import com.facebook.imagepipeline.cache.DefaultCacheKeyFactory
import com.facebook.imagepipeline.core.ImagePipelineFactory
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import com.stfalcon.frescoimageviewer.ImageViewer
import kotlinx.android.synthetic.main.message_attachment.view.*
import timber.log.Timber
import java.io.File
class ImageAttachmentViewHolder(itemView: View,
class ImageAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
private var cacheKey: CacheKey? = null
init {
with(itemView) {
setupActionMenu(attachment_container)
setupActionMenu(image_attachment)
}
}
......@@ -31,9 +61,74 @@ class ImageAttachmentViewHolder(itemView: View,
file_name.text = data.attachmentTitle
image_attachment.setOnClickListener { view ->
// TODO - implement a proper image viewer with a proper Transition
// TODO - We should definitely write our own ImageViewer
var imageViewer: ImageViewer? = null
val request =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(data.attachmentUrl))
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.DISK_CACHE)
.build()
cacheKey = DefaultCacheKeyFactory.getInstance()
.getEncodedCacheKey(request, null)
val pad = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_padding)
val lparams = AppBarLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val toolbar = Toolbar(context).also {
it.inflateMenu(R.menu.image_actions)
it.overflowIcon?.setTint(Color.WHITE)
it.setOnMenuItemClickListener {
return@setOnMenuItemClickListener when (it.itemId) {
R.id.action_save_image -> saveImage()
else -> super.onMenuItemClick(it)
}
}
val titleSize = context.resources
.getDimensionPixelSize(R.dimen.viewer_toolbar_title)
val titleTextView = TextView(context).also {
it.text = data.attachmentTitle
it.setTextColor(Color.WHITE)
it.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize.toFloat())
it.ellipsize = TextUtils.TruncateAt.END
it.setSingleLine()
it.typeface = Typeface.DEFAULT_BOLD
it.setPadding(pad)
}
val backArrowView = ImageView(context).also {
it.setImageResource(R.drawable.ic_arrow_back_white_24dp)
it.setOnClickListener { imageViewer?.onDismiss() }
it.setPadding(0, pad, pad, pad)
}
val layoutParams = AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.WRAP_CONTENT,
AppBarLayout.LayoutParams.WRAP_CONTENT
)
it.addView(backArrowView, layoutParams)
it.addView(titleTextView, layoutParams)
}
val appBarLayout = AppBarLayout(context).also {
it.layoutParams = lparams
it.setBackgroundColor(Color.BLACK)
it.addView(
toolbar, AppBarLayout.LayoutParams(
AppBarLayout.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
)
}
val builder = ImageViewer.createPipelineDraweeControllerBuilder()
.setImageRequest(request)
.setAutoPlayAnimations(true)
ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
imageViewer = ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
.setOverlayView(appBarLayout)
.setStartPosition(0)
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
......@@ -42,4 +137,60 @@ class ImageAttachmentViewHolder(itemView: View,
}
}
private fun saveImage(): Boolean {
if (!canWriteToExternalStorage()) {
checkWritingPermission()
return false
}
if (ImagePipelineFactory.getInstance().mainFileCache.hasKey(cacheKey)) {
val context = itemView.context
val resource = ImagePipelineFactory.getInstance().mainFileCache.getResource(cacheKey)
val cachedFile = (resource as FileBinaryResource).file
val imageFormat = ImageFormatChecker.getImageFormat(resource.openStream())
val imageDir = "${Environment.DIRECTORY_PICTURES}/Rocket.Chat Images/"
val imagePath = Environment.getExternalStoragePublicDirectory(imageDir)
val imageFile =
File(imagePath, "${cachedFile.nameWithoutExtension}.${imageFormat.fileExtension}")
imagePath.mkdirs()
imageFile.createNewFile()
try {
cachedFile.copyTo(imageFile, true)
MediaScannerConnection.scanFile(
context,
arrayOf(imageFile.absolutePath),
null
) { path, uri ->
Timber.i("Scanned $path:")
Timber.i("-> uri=$uri")
}
} catch (ex: Exception) {
Timber.e(ex)
val message = context.getString(R.string.msg_image_saved_failed)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} finally {
val message = context.getString(R.string.msg_image_saved_successfully)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
return true
}
private fun canWriteToExternalStorage(): Boolean {
return AndroidPermissionsHelper.checkPermission(
itemView.context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
private fun checkWritingPermission() {
val context = itemView.context
if (context is ContextThemeWrapper && context.baseContext is Activity) {
val activity = context.baseContext as Activity
AndroidPermissionsHelper.requestPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
AndroidPermissionsHelper.WRITE_EXTERNAL_STORAGE_CODE
)
}
}
}
\ No newline at end of file
......@@ -4,7 +4,7 @@ import android.text.method.LinkMovementMethod
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageAttachmentViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message.view.*
import kotlinx.android.synthetic.main.item_message_attachment.view.*
class MessageAttachmentViewHolder(
itemView: View,
......@@ -14,8 +14,8 @@ class MessageAttachmentViewHolder(
init {
with(itemView) {
setupActionMenu(attachment_container)
text_content.movementMethod = LinkMovementMethod()
setupActionMenu(text_content)
}
}
......
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.viewmodel.MessageReplyViewModel
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_message_reply.view.*
class MessageReplyViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
private val replyCallback: (roomName: String, permalink: String) -> Unit
) : BaseViewHolder<MessageReplyViewModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(itemView)
}
}
override fun bindViews(data: MessageReplyViewModel) {
with(itemView) {
button_message_reply.setOnClickListener {
with(data.rawData) {
replyCallback.invoke(roomName, permalink)
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.graphics.Color
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.viewmodel.MessageViewModel
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import chat.rocket.core.model.isSystemMessage
import kotlinx.android.synthetic.main.avatar.view.*
import kotlinx.android.synthetic.main.item_message.view.*
......@@ -15,8 +19,8 @@ class MessageViewHolder(
init {
with(itemView) {
setupActionMenu(message_container)
text_content.movementMethod = LinkMovementMethod()
setupActionMenu(text_content)
}
}
......@@ -29,6 +33,13 @@ class MessageViewHolder(
text_sender.text = data.senderName
text_content.text = data.content
image_avatar.setImageURI(data.avatar)
text_content.setTextColor(
if (data.isTemporary) Color.GRAY else Color.BLACK
)
data.message.let {
text_edit_indicator.isVisible = it.isSystemMessage() && it.editedBy != null
image_star_indicator.isVisible = it.starred?.isNotEmpty() ?: false
}
}
}
}
\ No newline at end of file
......@@ -5,6 +5,7 @@ import android.net.Uri
import android.view.View
import chat.rocket.android.chatroom.viewmodel.UrlPreviewViewModel
import chat.rocket.android.util.extensions.content
import chat.rocket.android.util.extensions.openTabbedUrl
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.widget.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_url_preview.view.*
......@@ -32,10 +33,17 @@ class UrlPreviewViewHolder(itemView: View,
text_title.content = data.title
text_description.content = data.description ?: ""
url_preview_layout.setOnClickListener { view ->
view.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.rawData.url)))
}
url_preview_layout.setOnClickListener(onClickListener)
text_host.setOnClickListener(onClickListener)
text_title.setOnClickListener(onClickListener)
image_preview.setOnClickListener(onClickListener)
text_description.setOnClickListener(onClickListener)
}
}
private val onClickListener = { view: View ->
if (data != null) {
view.openTabbedUrl(Uri.parse(data!!.rawData.url))
}
}
}
\ No newline at end of file
......@@ -14,10 +14,9 @@ class VideoAttachmentViewHolder(itemView: View,
init {
with(itemView) {
setupActionMenu(attachment_container)
image_attachment.setVisible(false)
audio_video_attachment.setVisible(true)
setupActionMenu(attachment_container)
setupActionMenu(audio_video_attachment)
}
}
......
......@@ -15,9 +15,6 @@ import kotlinx.coroutines.experimental.Job
@PerFragment
class ChatRoomFragmentModule {
@Provides
fun provideChatRoomNavigator(activity: ChatRoomActivity) = ChatRoomNavigator(activity)
@Provides
fun chatRoomView(frag: ChatRoomFragment): ChatRoomView {
return frag
......
package chat.rocket.android.chatroom.di
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.dagger.scope.PerActivity
import dagger.Module
import dagger.Provides
@Module
@PerActivity
class ChatRoomModule {
@Provides
fun provideChatRoomNavigator(activity: ChatRoomActivity) = ChatRoomNavigator(activity)
}
\ No newline at end of file
package chat.rocket.android.chatroom.di
import chat.rocket.android.chatroom.service.MessageService
import chat.rocket.android.dagger.module.AppModule
import dagger.Module
import dagger.android.ContributesAndroidInjector
@Module abstract class MessageServiceProvider {
@ContributesAndroidInjector(modules = [AppModule::class])
abstract fun provideMessageService(): MessageService
}
\ No newline at end of file
package chat.rocket.android.chatroom.domain
data class MessageReply(
val roomName: String,
val permalink: String
)
\ No newline at end of file
......@@ -2,7 +2,9 @@ package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.chatRoomIntent
import chat.rocket.android.members.ui.newInstance
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack
class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
......@@ -12,4 +14,34 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
newInstance(chatRoomId, chatRoomType)
}
}
fun toPinnedMessageList(chatRoomId: String, chatRoomType: String) {
activity.addFragmentBackStack("PinnedMessages", R.id.fragment_container) {
chat.rocket.android.pinnedmessages.ui.newInstance(chatRoomId, chatRoomType)
}
}
fun toFavoriteMessageList(chatRoomId: String, chatRoomType: String) {
activity.addFragmentBackStack("FavoriteMessages", R.id.fragment_container) {
chat.rocket.android.favoritemessages.ui.newInstance(chatRoomId, chatRoomType)
}
}
fun toNewServer() {
activity.startActivity(activity.changeServerIntent())
activity.finish()
}
fun toDirectMessage(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean,
isChatRoomCreator: Boolean,
chatRoomMessage: String) {
activity.startActivity(activity.chatRoomIntent(chatRoomId, chatRoomName, chatRoomType,
isChatRoomReadOnly, chatRoomLastSeen, isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage))
activity.overridePendingTransition(R.anim.open_enter, R.anim.open_exit)
}
}
\ No newline at end of file
......@@ -7,7 +7,8 @@ import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewMo
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
interface ChatRoomView : LoadingView, MessageView {
......@@ -25,10 +26,22 @@ interface ChatRoomView : LoadingView, MessageView {
*/
fun sendMessage(text: String)
/**
* Shows the username(s) of the user(s) who is/are typing in the chat room.
*
* @param usernameList The list of username to show.
*/
fun showTypingStatus(usernameList: ArrayList<String>)
/**
* Hides the typing status view.
*/
fun hideTypingStatusView()
/**
* Perform file selection with the mime type [filter]
*/
fun showFileSelection(filter: Array<String>)
fun showFileSelection(filter: Array<String>?)
/**
* Uploads a file to a chat room.
......@@ -92,10 +105,8 @@ interface ChatRoomView : LoadingView, MessageView {
/**
* Enables the send message button.
*
* @param sendFailed Whether the sent message has failed.
*/
fun enableSendMessageButton(sendFailed: Boolean)
fun enableSendMessageButton()
/**
* Clears the message composition.
......@@ -105,12 +116,16 @@ interface ChatRoomView : LoadingView, MessageView {
fun showInvalidFileSize(fileSize: Int, maxFileSize: Int)
fun showConnectionState(state: State)
fun populatePeopleSuggestions(members: List<PeopleSuggestionViewModel>)
fun populateRoomSuggestions(chatRooms: List<ChatRoomSuggestionViewModel>)
/**
* This user has joined the chat callback.
*
* @param userCanPost Whether the user can post a message or not.
*/
fun onJoined()
fun onJoined(userCanPost: Boolean)
fun showReactionsPopup(messageId: String)
......@@ -120,4 +135,15 @@ interface ChatRoomView : LoadingView, MessageView {
* @param commands The list of available commands.
*/
fun populateCommandSuggestions(commands: List<CommandSuggestionViewModel>)
/**
* Communicate whether it's a broadcast channel and if current user can post to it.
*/
fun onRoomUpdated(userCanPost: Boolean, channelIsBroadcast: Boolean, userCanMod: Boolean)
/**
* Open a DM with the user in the given [chatRoom] and pass the [permalink] for the message
* to reply.
*/
fun openDirectMessage(chatRoom: ChatRoom, permalink: String)
}
\ No newline at end of file
package chat.rocket.android.chatroom.service
import android.app.job.JobParameters
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
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
class MessageService : JobService() {
@Inject
lateinit var factory: ConnectionManagerFactory
@Inject
lateinit var currentServerRepository: CurrentServerRepository
@Inject
lateinit var messageRepository: MessagesRepository
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onStopJob(params: JobParameters?): Boolean {
return false
}
override fun onStartJob(params: JobParameters?): Boolean {
launch(CommonPool) {
val currentServer = currentServerRepository.get()
if (currentServer != null) {
retrySendingMessages(params, currentServer)
jobFinished(params, false)
}
}
return true
}
private suspend fun retrySendingMessages(params: JobParameters?, currentServer: String) {
val temporaryMessages = messageRepository.getAllUnsent()
.sortedWith(compareBy(Message::timestamp))
if (temporaryMessages.isNotEmpty()) {
val connectionManager = factory.create(currentServer)
val client = connectionManager.client
temporaryMessages.forEach { message ->
try {
client.sendMessage(
message = message.message,
messageId = message.id,
roomId = message.roomId,
avatar = message.avatar,
attachments = message.attachments,
alias = message.senderAlias
)
messageRepository.save(message.copy(isTemporary = false))
Timber.d("Sent scheduled message given by id: ${message.id}")
} catch (ex: Exception) {
Timber.e(ex)
// TODO - remove the generic message when we implement :userId:/message subscription
if (ex is IllegalStateException) {
Timber.d(ex, "Probably a read-only problem...")
// TODO: For now we are only going to reschedule when api is fixed.
messageRepository.removeById(message.id)
jobFinished(params, false)
} else {
// some other error
if (ex.message?.contains("E11000", true) == true) {
// XXX: Temporary solution. We need proper error codes from the api.
messageRepository.save(message.copy(isTemporary = false))
}
jobFinished(params, true)
}
}
}
}
}
companion object {
const val RETRY_SEND_MESSAGE_ID = 1
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.ui
import DrawableHelper
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.chatroom.presentation.ChatRoomNavigator
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.util.extensions.addFragment
......@@ -20,44 +19,56 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject
import timber.log.Timber
fun Context.chatRoomIntent(chatRoomId: String,
fun Context.chatRoomIntent(
chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean = true): Intent {
isChatRoomSubscribed: Boolean = true,
isChatRoomCreator: Boolean = false,
chatRoomMessage: String? = null
): Intent {
return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
putExtra(INTENT_CHAT_ROOM_TYPE, chatRoomType)
putExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, isChatRoomReadOnly)
putExtra(INTENT_CHAT_ROOM_LAST_SEEN, chatRoomLastSeen)
putExtra(INTENT_CHAT_IS_SUBSCRIBED, isChatRoomSubscribed)
putExtra(INTENT_CHAT_ROOM_IS_CREATOR, isChatRoomCreator)
putExtra(INTENT_CHAT_ROOM_MESSAGE, chatRoomMessage)
}
}
private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
private const val INTENT_IS_CHAT_ROOM_READ_ONLY = "is_chat_room_read_only"
private const val INTENT_CHAT_ROOM_IS_READ_ONLY = "chat_room_is_read_only"
private const val INTENT_CHAT_ROOM_IS_CREATOR = "chat_room_is_creator"
private const val INTENT_CHAT_ROOM_LAST_SEEN = "chat_room_last_seen"
private const val INTENT_CHAT_IS_SUBSCRIBED = "is_chat_room_subscribed"
private const val INTENT_CHAT_ROOM_MESSAGE = "chat_room_message"
class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
// TODO - workaround for now... We will move to a single activity
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var managerFactory: ConnectionManagerFactory
@Inject
lateinit var serverInteractor: GetCurrentServerInteractor
@Inject
lateinit var navigator: ChatRoomNavigator
@Inject
lateinit var managerFactory: ConnectionManagerFactory
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
private var isChatRoomReadOnly: Boolean = false
private var isChatRoomSubscribed: Boolean = true
private var isChatRoomCreator: Boolean = false
private var chatRoomLastSeen: Long = -1L
override fun onCreate(savedInstanceState: Bundle?) {
......@@ -66,7 +77,13 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
setContentView(R.layout.activity_chat_room)
// Workaround for when we are coming to the app via the recents app and the app was killed.
managerFactory.create(serverInteractor.get()!!).connect()
val serverUrl = serverInteractor.get()
if (serverUrl != null) {
managerFactory.create(serverUrl).connect()
} else {
navigator.toNewServer()
return
}
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
......@@ -77,8 +94,13 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_IS_CHAT_ROOM_READ_ONLY, true)
requireNotNull(chatRoomType) { "no is_chat_room_read_only provided in Intent extras" }
isChatRoomReadOnly = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_READ_ONLY, true)
requireNotNull(isChatRoomReadOnly) { "no chat_room_is_read_only provided in Intent extras" }
isChatRoomCreator = intent.getBooleanExtra(INTENT_CHAT_ROOM_IS_CREATOR, false)
requireNotNull(isChatRoomCreator) { "no chat_room_is_creator provided in Intent extras" }
val chatRoomMessage = intent.getStringExtra(INTENT_CHAT_ROOM_MESSAGE)
setupToolbar()
......@@ -86,10 +108,10 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
isChatRoomSubscribed = intent.getBooleanExtra(INTENT_CHAT_IS_SUBSCRIBED, true)
if (supportFragmentManager.findFragmentByTag("ChatRoomFragment") == null) {
addFragment("ChatRoomFragment", R.id.fragment_container) {
if (supportFragmentManager.findFragmentByTag(TAG_CHAT_ROOM_FRAGMENT) == null) {
addFragment(TAG_CHAT_ROOM_FRAGMENT, R.id.fragment_container) {
newInstance(chatRoomId, chatRoomName, chatRoomType, isChatRoomReadOnly, chatRoomLastSeen,
isChatRoomSubscribed)
isChatRoomSubscribed, isChatRoomCreator, chatRoomMessage)
}
}
}
......@@ -102,6 +124,17 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
return fragmentDispatchingAndroidInjector
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
text_room_name.textContent = chatRoomName
showRoomTypeIcon(true)
toolbar.setNavigationOnClickListener { finishActivity() }
}
fun showRoomTypeIcon(showRoomTypeIcon: Boolean) {
if (showRoomTypeIcon) {
val roomType = roomTypeOf(chatRoomType)
......@@ -129,17 +162,6 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
}
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = chatRoomName
showRoomTypeIcon(true)
toolbar.setNavigationOnClickListener {
finishActivity()
}
}
fun setupToolbarTitle(toolbarTitle: String) {
text_room_name.textContent = toolbarTitle
......@@ -149,4 +171,8 @@ 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.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.R
import chat.rocket.android.util.extensions.addFragment
import chat.rocket.android.util.extensions.textContent
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import kotlinx.android.synthetic.main.app_bar_chat_room.*
import javax.inject.Inject
private const val INTENT_CHAT_ROOM_ID = "chat_room_id"
private const val INTENT_CHAT_ROOM_NAME = "chat_room_name"
private const val INTENT_CHAT_ROOM_TYPE = "chat_room_type"
class PinnedMessagesActivity : AppCompatActivity(), HasSupportFragmentInjector {
@Inject lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
private lateinit var chatRoomId: String
private lateinit var chatRoomName: String
private lateinit var chatRoomType: String
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pinned_messages)
chatRoomId = intent.getStringExtra(INTENT_CHAT_ROOM_ID)
requireNotNull(chatRoomId) { "no chat_room_id provided in Intent extras" }
chatRoomName = intent.getStringExtra(INTENT_CHAT_ROOM_NAME)
requireNotNull(chatRoomName) { "no chat_room_name provided in Intent extras" }
chatRoomType = intent.getStringExtra(INTENT_CHAT_ROOM_TYPE)
requireNotNull(chatRoomType) { "no chat_room_type provided in Intent extras" }
setupToolbar()
addFragment("PinnedMessagesFragment", R.id.fragment_container) {
newPinnedMessagesFragment(chatRoomId, chatRoomName, chatRoomType)
}
}
override fun onBackPressed() = finishActivity()
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return fragmentDispatchingAndroidInjector
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
text_room_name.textContent = getString(R.string.title_pinned_messages)
toolbar.setNavigationOnClickListener {
finishActivity()
}
}
private fun finishActivity() {
super.onBackPressed()
overridePendingTransition(R.anim.close_enter, R.anim.close_exit)
}
}
\ No newline at end of file
......@@ -7,8 +7,10 @@ import chat.rocket.android.util.extensions.setVisible
/**
* An adapter for bottomsheet menu that lists all the actions that could be taken over a chat message.
*/
class ActionListAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuItem.OnMenuItemClickListener) :
ListBottomSheetAdapter(menuItems = menuItems, callback = callback) {
class ActionListAdapter(
menuItems: List<MenuItem> = emptyList(),
callback: MenuItem.OnMenuItemClickListener
) : ListBottomSheetAdapter(menuItems = menuItems, callback = callback) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = menuItems[position]
......@@ -25,7 +27,11 @@ class ActionListAdapter(menuItems: List<MenuItem> = emptyList(), callback: MenuI
callback?.onMenuItemClick(item)
}
val deleteTextColor = holder.itemView.context.resources.getColor(R.color.red)
val color = if (item.itemId == R.id.action_menu_msg_delete) deleteTextColor else textColors.get(item.itemId)
val color = if (item.itemId == R.id.action_message_delete) {
deleteTextColor
} else {
textColors.get(item.itemId)
}
holder.textTitle.setTextColor(color)
}
}
\ No newline at end of file
......@@ -13,7 +13,8 @@ data class AudioAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<AudioAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUDIO_ATTACHMENT.viewType
......
......@@ -15,7 +15,8 @@ data class AuthorAttachmentViewModel(
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseAttachmentViewModel<AuthorAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.AUTHOR_ATTACHMENT.viewType
......
......@@ -12,6 +12,7 @@ interface BaseViewModel<out T> {
var reactions: List<ReactionViewModel>
var nextDownStreamMessage: BaseViewModel<*>?
var preview: Message?
var isTemporary: Boolean
enum class ViewType(val viewType: Int) {
MESSAGE(0),
......@@ -21,7 +22,10 @@ interface BaseViewModel<out T> {
VIDEO_ATTACHMENT(4),
AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7)
AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8),
GENERIC_FILE_ATTACHMENT(9),
MESSAGE_REPLY(10)
}
}
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.ColorAttachment
data class ColorAttachmentViewModel(
override val attachmentUrl: String,
val id: Long,
val color: Int,
val text: CharSequence,
override val message: Message,
override val rawData: ColorAttachment,
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseAttachmentViewModel<ColorAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.COLOR_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_color_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
data class GenericFileAttachmentViewModel(
override val message: Message,
override val rawData: GenericFileAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<GenericFileAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.GENERIC_FILE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_file_attachment
}
\ No newline at end of file
......@@ -13,7 +13,8 @@ data class ImageAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<ImageAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.IMAGE_ATTACHMENT.viewType
......
......@@ -14,7 +14,8 @@ data class MessageAttachmentViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
var messageLink: String? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_ATTACHMENT.viewType
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.android.R
import chat.rocket.android.chatroom.domain.MessageReply
import chat.rocket.core.model.Message
data class MessageReplyViewModel(
override val rawData: MessageReply,
override val messageId: String,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>?,
override var preview: Message?,
override var isTemporary: Boolean = false,
override val message: Message
) : BaseViewModel<MessageReply> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE_REPLY.viewType
override val layoutId: Int
get() = R.layout.item_message_reply
}
\ No newline at end of file
......@@ -15,7 +15,8 @@ data class MessageViewModel(
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null,
var isFirstUnread: Boolean
var isFirstUnread: Boolean,
override var isTemporary: Boolean = false
) : BaseMessageViewModel<Message> {
override val viewType: Int
get() = BaseViewModel.ViewType.MESSAGE.viewType
......
package chat.rocket.android.chatroom.viewmodel
import chat.rocket.core.model.ChatRoomRole
data class RoomViewModel(
val roles: List<ChatRoomRole>,
val isBroadcast: Boolean = false
)
\ No newline at end of file
......@@ -14,7 +14,8 @@ data class UrlPreviewViewModel(
val thumbUrl: String?,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseViewModel<Url> {
override val viewType: Int
get() = BaseViewModel.ViewType.URL_PREVIEW.viewType
......
......@@ -13,7 +13,8 @@ data class VideoAttachmentViewModel(
override val id: Long,
override var reactions: List<ReactionViewModel>,
override var nextDownStreamMessage: BaseViewModel<*>? = null,
override var preview: Message? = null
override var preview: Message? = null,
override var isTemporary: Boolean = false
) : BaseFileAttachmentViewModel<VideoAttachment> {
override val viewType: Int
get() = BaseViewModel.ViewType.VIDEO_ATTACHMENT.viewType
......
......@@ -2,7 +2,7 @@ package chat.rocket.android.chatrooms.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.internal.realtime.State
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.model.ChatRoom
interface ChatRoomsView : LoadingView, MessageView {
......
package chat.rocket.android.core.behaviours
import android.support.annotation.StringRes
import chat.rocket.common.util.ifNull
interface MessageView {
......@@ -15,3 +16,11 @@ interface MessageView {
fun showGenericErrorMessage()
}
fun MessageView.showMessage(ex: Exception) {
ex.message?.let {
showMessage(it)
}.ifNull {
showGenericErrorMessage()
}
}
\ No newline at end of file
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.AppModule
import chat.rocket.android.dagger.module.ReceiverBuilder
......@@ -29,6 +31,8 @@ interface AppComponent {
fun inject(service: FirebaseTokenService)
fun inject(service: MessageService)
/*@Component.Builder
abstract class Builder : AndroidInjector.Builder<RocketChatApplication>()*/
}
This diff is collapsed.
This diff is collapsed.
package chat.rocket.android.member.ui
package chat.rocket.android.members.ui
import android.os.Bundle
import android.support.design.widget.BottomSheetDialogFragment
......
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