Commit 9c238721 authored by Divyanshu Bhargava's avatar Divyanshu Bhargava

Merge branch 'develop-2.x' into edited-msg-indicator

parents 9c32ec50 fdb5fa10
......@@ -57,7 +57,7 @@ jobs:
command: ./gradlew lint
- run:
name: Run Unit test
command: echo ./gradlew test # TODO: Fix unit test errors soon...
command: ./gradlew test
- 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 2011
versionName "2.0.1"
versionCode 2015
versionName "2.1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
......@@ -78,6 +78,8 @@ dependencies {
implementation libraries.room
kapt libraries.roomProcessor
implementation libraries.roomRxjava
implementation libraries.lifecycleExtensions
kapt libraries.lifecycleCompiler
implementation libraries.rxKotlin
implementation libraries.rxAndroid
......@@ -102,7 +104,6 @@ dependencies {
implementation libraries.frescoImageViewer
implementation libraries.markwon
implementation libraries.markwonImageLoader
implementation libraries.sheetMenu
......
......@@ -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"
......@@ -50,37 +50,52 @@
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"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
<!-- TODO: Change to fragment-->
<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"
......@@ -124,10 +139,6 @@
<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
......@@ -108,18 +108,12 @@ object DrawableHelper {
* @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)
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,9 +45,11 @@ import timber.log.Timber
import java.lang.ref.WeakReference
import javax.inject.Inject
class RocketChatApplication : Application(), HasActivityInjector, HasServiceInjector,
HasBroadcastReceiverInjector {
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 INTERNAL_TOKEN_MIGRATION_NEEDED = "INTERNAL_TOKEN_MIGRATION_NEEDED"
\ No newline at end of file
private const val CLEANUP_OLD_MESSAGES_NEEDED = "CLEANUP_OLD_MESSAGES_NEEDED"
\ 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)).
*/
......
......@@ -5,6 +5,7 @@ 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
......@@ -12,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) {
......@@ -28,6 +30,10 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
}
}
fun toPreviousView() {
activity.toPreviousView()
}
fun toTwoFA(username: String, password: String) {
activity.addFragmentBackStack("TwoFAFragment", R.id.fragment_container) {
TwoFAFragment.newInstance(username, password)
......@@ -40,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)
......
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
......@@ -7,9 +7,10 @@ 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,
......@@ -17,7 +18,18 @@ 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) {
......@@ -25,7 +37,7 @@ class ServerPresenter @Inject constructor(private val view: ServerView,
}
}
fun connectToServer(server: String, block: () -> Unit) {
private fun connectToServer(server: String, block: () -> Unit) {
if (!server.isValidUrl()) {
view.showInvalidServerUrlMessage()
} else {
......
......@@ -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.
......
......@@ -10,4 +10,20 @@ 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() {}
}
\ 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 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)
......@@ -33,6 +40,8 @@ class ServerFragment : Fragment(), ServerView {
}
}
private var protocol = "https://"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidSupportInjection.inject(this)
......@@ -49,8 +58,41 @@ class ServerFragment : Fragment(), ServerView {
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 -> {
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()
}
}
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
override fun onDestroyView() {
......@@ -74,7 +116,7 @@ class ServerFragment : Fragment(), ServerView {
}
}
override fun showMessage(resId: Int){
override fun showMessage(resId: Int) {
ui {
showToast(resId)
}
......@@ -90,15 +132,70 @@ class ServerFragment : Fragment(), ServerView {
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)
}
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() {
button_connect.setOnClickListener {
val url = text_server_url.textContent.ifEmpty(text_server_url.hintContent)
presenter.connect(text_server_protocol.textContent + url)
ui {
button_connect.setOnClickListener {
val url = text_server_url.textContent.ifEmpty(text_server_url.hintContent)
presenter.checkServer("${protocol}${url.sanitize()}")
}
}
}
}
\ No newline at end of file
......@@ -34,7 +34,7 @@ class AuthenticationActivity : AppCompatActivity(), HasSupportFragmentInjector {
val deepLinkInfo = intent.getLoginDeepLinkInfo()
launch(UI + job) {
val newServer = intent.getBooleanExtra(INTENT_ADD_NEW_SERVER, false)
// if we got authenticadeWithDeepLink information, pass true to newServer also
// if we got authenticateWithDeepLink information, pass true to newServer also
presenter.loadCredentials(newServer || deepLinkInfo != null) { authenticated ->
if (!authenticated) {
showServerInput(savedInstanceState, deepLinkInfo)
......
......@@ -14,11 +14,11 @@ import timber.log.Timber
import java.security.InvalidParameterException
class ChatRoomAdapter(
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null
private val roomType: String,
private val roomName: String,
private val presenter: ChatRoomPresenter?,
private val enableActions: Boolean = true,
private val reactionListener: EmojiReactionListener? = null
) : RecyclerView.Adapter<BaseViewHolder<*>>() {
private val dataSet = ArrayList<BaseViewModel<*>>()
......@@ -61,6 +61,10 @@ class ChatRoomAdapter(
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)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
......@@ -102,6 +106,7 @@ class ChatRoomAdapter(
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)
}
}
......@@ -175,15 +180,15 @@ 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_quote -> presenter?.citeMessage(roomType, id, false)
R.id.action_menu_msg_reply -> presenter?.citeMessage(roomType, 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 -> {
......
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)
setupActionMenu(text_file_name)
}
}
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,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ImageAttachmentViewModel>(itemView, listener, reactionListener) {
private var cacheKey: CacheKey? = null
init {
with(itemView) {
setupActionMenu(attachment_container)
......@@ -31,15 +62,121 @@ 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()
.setAutoPlayAnimations(true)
ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
.setStartPosition(0)
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
.show()
.setImageRequest(request)
.setAutoPlayAnimations(true)
imageViewer = ImageViewer.Builder(view.context, listOf(data.attachmentUrl))
.setOverlayView(appBarLayout)
.setStartPosition(0)
.hideStatusBar(false)
.setCustomDraweeControllerBuilder(builder)
.show()
}
}
}
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
......@@ -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
......@@ -3,6 +3,7 @@ package chat.rocket.android.chatroom.presentation
import chat.rocket.android.R
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.members.ui.newInstance
import chat.rocket.android.server.ui.changeServerIntent
import chat.rocket.android.util.extensions.addFragmentBackStack
class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
......@@ -12,4 +13,9 @@ class ChatRoomNavigator(internal val activity: ChatRoomActivity) {
newInstance(chatRoomId, chatRoomType)
}
}
fun toNewServer() {
activity.startActivity(activity.changeServerIntent())
activity.finish()
}
}
\ No newline at end of file
......@@ -10,6 +10,7 @@ import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.chatroom.viewmodel.suggestion.ChatRoomSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.CommandSuggestionViewModel
import chat.rocket.android.chatroom.viewmodel.suggestion.PeopleSuggestionViewModel
import chat.rocket.android.core.behaviours.showMessage
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.username
......@@ -30,7 +31,6 @@ import chat.rocket.core.internal.rest.*
import chat.rocket.core.model.Command
import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself
import chat.rocket.core.model.Value
import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.async
......@@ -41,25 +41,28 @@ import timber.log.Timber
import java.util.*
import javax.inject.Inject
class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
private val navigator: ChatRoomNavigator,
private val strategy: CancelStrategy,
getSettingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val permissions: GetPermissionsInteractor,
private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository,
private val usersRepository: UsersRepository,
private val roomsRepository: RoomRepository,
private val localRepository: LocalRepository,
factory: ConnectionManagerFactory,
private val mapper: ViewModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor) {
class ChatRoomPresenter @Inject constructor(
private val view: ChatRoomView,
private val navigator: ChatRoomNavigator,
private val strategy: CancelStrategy,
getSettingsInteractor: GetSettingsInteractor,
serverInteractor: GetCurrentServerInteractor,
private val getChatRoomsInteractor: GetChatRoomsInteractor,
private val permissions: GetPermissionsInteractor,
private val uriInteractor: UriInteractor,
private val messagesRepository: MessagesRepository,
private val usersRepository: UsersRepository,
private val roomsRepository: RoomRepository,
private val localRepository: LocalRepository,
factory: ConnectionManagerFactory,
private val mapper: ViewModelMapper,
private val jobSchedulerInteractor: JobSchedulerInteractor
) {
private val currentServer = serverInteractor.get()!!
private val manager = factory.create(currentServer)
private val client = manager.client
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private var settings: PublicSettings = getSettingsInteractor.get(serverInteractor.get()!!)
private val messagesChannel = Channel<Message>()
private var chatRoomId: String? = null
......@@ -172,7 +175,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
launchUI(strategy) {
view.showLoading()
try {
val fileName = async { uriInteractor.getFileName(uri) }.await()
val fileName = async { uriInteractor.getFileName(uri) }.await() ?: uri.toString()
val mimeType = async { uriInteractor.getMimeType(uri) }.await()
val fileSize = async { uriInteractor.getFileSize(uri) }.await()
val maxFileSize = settings.uploadMaxFileSize()
......@@ -189,12 +192,11 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
}
}
}
} catch (ex: RocketChatException) {
Timber.d(ex)
ex.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
} catch (ex: Exception) {
Timber.d(ex, "Error uploading file")
when (ex) {
is RocketChatException -> view.showMessage(ex)
else -> view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
......@@ -277,7 +279,6 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
// TODO - we need to better treat connection problems here, but no let gaps
// on the messages list
Timber.d(ex, "Error fetching channel history")
ex.printStackTrace()
}
}
}
......@@ -324,43 +325,37 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
* Quote or reply a message.
*
* @param roomType The current room type.
* @param roomName The name of the current room.
* @param messageId The id of the message to make citation for.
* @param mentionAuthor true means the citation is a reply otherwise it's a quote.
*/
fun citeMessage(roomType: String, roomName: String, messageId: String, mentionAuthor: Boolean) {
fun citeMessage(roomType: String, messageId: String, mentionAuthor: Boolean) {
launchUI(strategy) {
val message = messagesRepository.getById(messageId)
val me: Myself? = try {
retryIO("me()") { client.me() } //TODO: Cache this and use an interactor
} catch (ex: Exception) {
Timber.d(ex, "Error getting myself info.")
ex.printStackTrace()
Timber.e(ex)
null
}
message?.let { m ->
val id = m.id
val username = m.sender?.username
val user = "@" + if (settings.useRealName()) m.sender?.name
?: m.sender?.username else m.sender?.username
val mention = if (mentionAuthor && me?.username != username) user else ""
val type = roomTypeOf(roomType)
val room = when (type) {
is RoomType.Channel -> "channel"
is RoomType.DirectMessage -> "direct"
is RoomType.PrivateGroup -> "group"
is RoomType.Livechat -> "livechat"
is RoomType.Custom -> "custom" //TODO: put appropriate callback string here.
}
message?.let { msg ->
val id = msg.id
val username = msg.sender?.username ?: ""
val mention = if (mentionAuthor && me?.username != username) "@$username" else ""
val room = if (roomTypeOf(roomType) is RoomType.DirectMessage) username else roomType
view.showReplyingAction(
username = user,
replyMarkdown = "[ ]($currentServer/$room/$roomName?msg=$id) $mention ",
username = getDisplayName(msg.sender),
replyMarkdown = "[ ]($currentServer/$roomType/$room?msg=$id) $mention ",
quotedMessage = mapper.map(message).last().preview?.message ?: ""
)
}
}
}
private fun getDisplayName(user: SimpleUser?): String {
val username = user?.username ?: ""
return if (settings.useRealName()) user?.name ?: "@$username" else "@$username"
}
/**
* Copy message to clipboard.
*
......@@ -510,7 +505,7 @@ class ChatRoomPresenter @Inject constructor(private val view: ChatRoomView,
fun loadChatRooms() {
launchUI(strategy) {
try {
val chatRooms = getChatRoomsInteractor.get(currentServer)
val chatRooms = getChatRoomsInteractor.getAll(currentServer)
.filterNot {
it.type is RoomType.DirectMessage || it.type is RoomType.Livechat
}
......
......@@ -4,27 +4,24 @@ import chat.rocket.android.chatroom.viewmodel.ViewModelMapper
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.server.domain.GetChatRoomsInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
import chat.rocket.common.util.ifNull
import chat.rocket.core.internal.rest.getRoomPinnedMessages
import chat.rocket.core.model.Value
import chat.rocket.core.model.isSystemMessage
import timber.log.Timber
import javax.inject.Inject
class PinnedMessagesPresenter @Inject constructor(private val view: PinnedMessagesView,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor,
private val mapper: ViewModelMapper,
factory: RocketChatClientFactory,
getSettingsInteractor: GetSettingsInteractor) {
class PinnedMessagesPresenter @Inject constructor(
private val view: PinnedMessagesView,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
private val roomsInteractor: GetChatRoomsInteractor,
private val mapper: ViewModelMapper,
factory: RocketChatClientFactory
) {
private val client = factory.create(serverInteractor.get()!!)
private var settings: Map<String, Value<Any>> = getSettingsInteractor.get(serverInteractor.get()!!)
private var pinnedMessagesListOffset: Int = 0
/**
......
......@@ -3,11 +3,11 @@ 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,15 +20,15 @@ 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,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean = true): Intent {
fun Context.chatRoomIntent(
chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isChatRoomSubscribed: Boolean = true
): Intent {
return Intent(this, ChatRoomActivity::class.java).apply {
putExtra(INTENT_CHAT_ROOM_ID, chatRoomId)
putExtra(INTENT_CHAT_ROOM_NAME, chatRoomName)
......@@ -51,6 +51,7 @@ class ChatRoomActivity : AppCompatActivity(), HasSupportFragmentInjector {
// TODO - workaround for now... We will move to a single activity
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
@Inject lateinit var navigator: ChatRoomNavigator
@Inject lateinit var managerFactory: ConnectionManagerFactory
private lateinit var chatRoomId: String
......@@ -66,7 +67,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" }
......
package chat.rocket.android.chatroom.ui
import android.Manifest
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.support.annotation.DrawableRes
import android.support.v4.app.ActivityCompat
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.widget.DefaultItemAnimator
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
......@@ -39,15 +35,18 @@ import kotlinx.android.synthetic.main.fragment_chat_room.*
import kotlinx.android.synthetic.main.message_attachment_options.*
import kotlinx.android.synthetic.main.message_composer.*
import kotlinx.android.synthetic.main.message_list.*
import timber.log.Timber
import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
fun newInstance(chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isSubscribed: Boolean = true): Fragment {
fun newInstance(
chatRoomId: String,
chatRoomName: String,
chatRoomType: String,
isChatRoomReadOnly: Boolean,
chatRoomLastSeen: Long,
isSubscribed: Boolean = true
): Fragment {
return ChatRoomFragment().apply {
arguments = Bundle(1).apply {
putString(BUNDLE_CHAT_ROOM_ID, chatRoomId)
......@@ -114,7 +113,13 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_chat_room)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return container?.inflate(R.layout.fragment_chat_room)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -224,6 +229,19 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
verticalScrollOffset.set(0)
}
presenter.loadActiveMembers(chatRoomId, chatRoomType, filterSelfOut = true)
toggleNoChatView(adapter.itemCount)
}
}
private fun toggleNoChatView(size: Int) {
if (size == 0){
image_chat_icon.setVisible(true)
text_chat_title.setVisible(true)
text_chat_description.setVisible(true)
}else{
image_chat_icon.setVisible(false)
text_chat_title.setVisible(false)
text_chat_description.setVisible(false)
}
}
......@@ -308,6 +326,7 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
adapter.prependData(message)
recycler_view.scrollToPosition(0)
verticalScrollOffset.set(0)
toggleNoChatView(adapter.itemCount)
}
}
......@@ -468,29 +487,11 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
override fun showFileSelection(filter: Array<String>) {
ui {
if (ContextCompat.checkSelfPermission(it, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(it,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
1)
} else {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_MIME_TYPES, filter)
startActivityForResult(intent, REQUEST_CODE_FOR_PERFORM_SAF)
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
1 -> {
if (!(grantResults.isNotEmpty() && grantResults.first() == PackageManager.PERMISSION_GRANTED)) {
handler.postDelayed({
ui { hideAttachmentOptions() }
}, 400)
}
}
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_MIME_TYPES, filter)
intent.addCategory(Intent.CATEGORY_OPENABLE)
startActivityForResult(intent, REQUEST_CODE_FOR_PERFORM_SAF)
}
}
......
......@@ -23,7 +23,8 @@ interface BaseViewModel<out T> {
AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8)
COLOR_ATTACHMENT(8),
GENERIC_FILE_ATTACHMENT(9)
}
}
......
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
......@@ -10,6 +10,7 @@ import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import chat.rocket.android.R
import chat.rocket.android.infrastructure.LocalRepository
......@@ -49,6 +50,7 @@ class ChatRoomsAdapter(private val context: Context,
fun bind(chatRoom: ChatRoom) = with(itemView) {
bindAvatar(chatRoom, image_avatar)
bindName(chatRoom, text_chat_name)
bindIcon(chatRoom, image_chat_icon)
bindLastMessageDateTime(chatRoom, text_last_message_date_time)
bindLastMessage(chatRoom, text_last_message)
bindUnreadMessages(chatRoom, text_total_unread_messages)
......@@ -80,34 +82,39 @@ class ChatRoomsAdapter(private val context: Context,
}
}
private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.textContent = chatRoom.name
private fun bindIcon(chatRoom: ChatRoom, imageView: ImageView) {
val drawable = when (chatRoom.type) {
is RoomType.Channel -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_channel, context)
}
is RoomType.PrivateGroup -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_lock, context)
}
is RoomType.DirectMessage -> {
DrawableHelper.getDrawableFromId(R.drawable.ic_room_dm, context)
}
is RoomType.Channel -> DrawableHelper.getDrawableFromId(
R.drawable.ic_hashtag_12dp,
context
)
is RoomType.PrivateGroup -> DrawableHelper.getDrawableFromId(
R.drawable.ic_lock_12_dp,
context
)
is RoomType.DirectMessage -> DrawableHelper.getUserStatusDrawable(
chatRoom.status,
context
)
else -> null
}
drawable?.let {
val wrappedDrawable = DrawableHelper.wrapDrawable(it)
val mutableDrawable = wrappedDrawable.mutate()
val color = when (chatRoom.alert || chatRoom.unread > 0) {
true -> R.color.colorPrimaryText
false -> R.color.colorSecondaryText
val mutateDrawable = DrawableHelper.wrapDrawable(it).mutate()
if (chatRoom.type !is RoomType.DirectMessage) {
val color = when (chatRoom.alert || chatRoom.unread > 0) {
true -> R.color.colorPrimaryText
false -> R.color.colorSecondaryText
}
DrawableHelper.tintDrawable(mutateDrawable, context, color)
}
DrawableHelper.tintDrawable(mutableDrawable, context, color)
DrawableHelper.compoundDrawable(textView, mutableDrawable)
imageView.setImageDrawable(mutateDrawable)
}
}
private fun bindName(chatRoom: ChatRoom, textView: TextView) {
textView.textContent = chatRoom.name
}
private fun bindLastMessageDateTime(chatRoom: ChatRoom, textView: TextView) {
val lastMessage = chatRoom.lastMessage
if (lastMessage != null) {
......
......@@ -32,12 +32,9 @@ import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_chat_rooms.*
import kotlinx.coroutines.experimental.Job
import kotlinx.coroutines.experimental.NonCancellable.isActive
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import timber.log.Timber
import javax.inject.Inject
class ChatRoomsFragment : Fragment(), ChatRoomsView {
@Inject lateinit var presenter: ChatRoomsPresenter
@Inject lateinit var serverInteractor: GetCurrentServerInteractor
......@@ -67,7 +64,11 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
super.onDestroy()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_chat_rooms)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = container?.inflate(R.layout.fragment_chat_rooms)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -100,7 +101,6 @@ class ChatRoomsFragment : Fragment(), ChatRoomsView {
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_sort -> {
......
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
......
......@@ -3,11 +3,13 @@ package chat.rocket.android.dagger.module
import chat.rocket.android.authentication.di.AuthenticationModule
import chat.rocket.android.authentication.login.di.LoginFragmentProvider
import chat.rocket.android.authentication.registerusername.di.RegisterUsernameFragmentProvider
import chat.rocket.android.authentication.resetpassword.di.ResetPasswordFragmentProvider
import chat.rocket.android.authentication.server.di.ServerFragmentProvider
import chat.rocket.android.authentication.signup.di.SignupFragmentProvider
import chat.rocket.android.authentication.twofactor.di.TwoFAFragmentProvider
import chat.rocket.android.authentication.ui.AuthenticationActivity
import chat.rocket.android.chatroom.di.ChatRoomFragmentProvider
import chat.rocket.android.chatroom.di.ChatRoomModule
import chat.rocket.android.chatroom.di.PinnedMessagesFragmentProvider
import chat.rocket.android.chatroom.ui.ChatRoomActivity
import chat.rocket.android.chatroom.ui.PinnedMessagesActivity
......@@ -32,6 +34,7 @@ abstract class ActivityBuilder {
ServerFragmentProvider::class,
LoginFragmentProvider::class,
RegisterUsernameFragmentProvider::class,
ResetPasswordFragmentProvider::class,
SignupFragmentProvider::class,
TwoFAFragmentProvider::class
])
......@@ -45,7 +48,9 @@ abstract class ActivityBuilder {
abstract fun bindMainActivity(): MainActivity
@PerActivity
@ContributesAndroidInjector(modules = [ChatRoomFragmentProvider::class, MembersFragmentProvider::class])
@ContributesAndroidInjector(modules = [ChatRoomModule::class,
ChatRoomFragmentProvider::class,
MembersFragmentProvider::class])
abstract fun bindChatRoomActivity(): ChatRoomActivity
@PerActivity
......
......@@ -22,29 +22,8 @@ import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.infrastructure.SharedPrefsLocalRepository
import chat.rocket.android.push.GroupedPush
import chat.rocket.android.push.PushManager
import chat.rocket.android.server.domain.AccountsRepository
import chat.rocket.android.server.domain.ChatRoomsRepository
import chat.rocket.android.server.domain.CurrentServerRepository
import chat.rocket.android.server.domain.GetAccountInteractor
import chat.rocket.android.server.domain.GetCurrentServerInteractor
import chat.rocket.android.server.domain.GetPermissionsInteractor
import chat.rocket.android.server.domain.GetSettingsInteractor
import chat.rocket.android.server.domain.JobSchedulerInteractor
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.server.domain.MultiServerTokenRepository
import chat.rocket.android.server.domain.RoomRepository
import chat.rocket.android.server.domain.SettingsRepository
import chat.rocket.android.server.domain.TokenRepository
import chat.rocket.android.server.domain.UsersRepository
import chat.rocket.android.server.infraestructure.JobSchedulerInteractorImpl
import chat.rocket.android.server.infraestructure.MemoryChatRoomsRepository
import chat.rocket.android.server.infraestructure.MemoryRoomRepository
import chat.rocket.android.server.infraestructure.MemoryUsersRepository
import chat.rocket.android.server.infraestructure.ServerDao
import chat.rocket.android.server.infraestructure.SharedPreferencesAccountsRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesMessagesRepository
import chat.rocket.android.server.infraestructure.SharedPreferencesSettingsRepository
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.*
import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
......@@ -55,6 +34,7 @@ import chat.rocket.common.util.Logger
import chat.rocket.common.util.PlatformLogger
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.AttachmentAdapterFactory
import chat.rocket.core.internal.ReactionsAdapter
import com.facebook.drawee.backends.pipeline.DraweeConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.core.ImagePipelineConfig
......@@ -68,10 +48,8 @@ import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.il.AsyncDrawableLoader
import ru.noties.markwon.spans.SpannableTheme
import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
......@@ -164,10 +142,10 @@ class AppModule {
listeners.add(RequestLoggingListener())
return OkHttpImagePipelineConfigFactory.newBuilder(context, okHttpClient)
.setRequestListeners(listeners)
.setDownsampleEnabled(true)
//.experiment().setBitmapPrepareToDraw(true).experiment()
.experiment().setPartialImageCachingEnabled(true).build()
.setRequestListeners(listeners)
.setDownsampleEnabled(true)
//.experiment().setBitmapPrepareToDraw(true).experiment()
.experiment().setPartialImageCachingEnabled(true).build()
}
@Provides
......@@ -189,6 +167,7 @@ class AppModule {
}
@Provides
@Singleton
fun provideSharedPreferences(context: Application) =
context.getSharedPreferences("rocket.chat", Context.MODE_PRIVATE)
......@@ -196,7 +175,7 @@ class AppModule {
@Provides
@ForMessages
fun provideMessagesSharedPreferences(context: Application) =
context.getSharedPreferences("messages", Context.MODE_PRIVATE)
context.getSharedPreferences("messages", Context.MODE_PRIVATE)
@Provides
@Singleton
......@@ -230,17 +209,33 @@ class AppModule {
@Provides
@Singleton
fun provideMoshi(logger: PlatformLogger,
currentServerInteractor: GetCurrentServerInteractor):
Moshi {
fun provideActiveUsersRepository(): ActiveUsersRepository {
return MemoryActiveUsersRepository()
}
@Provides
@Singleton
fun provideMoshi(
logger: PlatformLogger,
currentServerInteractor: GetCurrentServerInteractor
): Moshi {
val url = currentServerInteractor.get() ?: ""
return Moshi.Builder()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE)
.add(AttachmentAdapterFactory(Logger(logger, url)))
.add(java.lang.Long::class.java, ISO8601Date::class.java, TimestampAdapter(CalendarISO8601Converter()))
.add(Long::class.java, ISO8601Date::class.java, TimestampAdapter(CalendarISO8601Converter()))
.build()
.add(FallbackSealedClassJsonAdapter.ADAPTER_FACTORY)
.add(AppJsonAdapterFactory.INSTANCE)
.add(AttachmentAdapterFactory(Logger(logger, url)))
.add(
java.lang.Long::class.java,
ISO8601Date::class.java,
TimestampAdapter(CalendarISO8601Converter())
)
.add(
Long::class.java,
ISO8601Date::class.java,
TimestampAdapter(CalendarISO8601Converter())
)
.add(ReactionsAdapter())
.build()
}
@Provides
......@@ -268,21 +263,16 @@ class AppModule {
fun provideConfiguration(context: Application, client: OkHttpClient): SpannableConfiguration {
val res = context.resources
return SpannableConfiguration.builder(context)
.asyncDrawableLoader(AsyncDrawableLoader.builder()
.client(client)
.executorService(Executors.newCachedThreadPool())
.resources(res)
.build())
.theme(SpannableTheme.builder()
.linkColor(res.getColor(R.color.colorAccent))
.build())
.build()
.theme(SpannableTheme.builder()
.linkColor(res.getColor(R.color.colorAccent))
.build())
.build()
}
@Provides
@Singleton
fun provideMessageParser(context: Application, configuration: SpannableConfiguration): MessageParser {
return MessageParser(context, configuration)
fun provideMessageParser(context: Application, configuration: SpannableConfiguration, serverInteractor: GetCurrentServerInteractor, settingsInteractor: GetSettingsInteractor): MessageParser {
val url = serverInteractor.get()!!
return MessageParser(context, configuration, settingsInteractor.get(url))
}
@Provides
......@@ -294,11 +284,11 @@ class AppModule {
@Provides
@Singleton
fun provideAccountsRepository(preferences: SharedPreferences, moshi: Moshi): AccountsRepository =
SharedPreferencesAccountsRepository(preferences, moshi)
SharedPreferencesAccountsRepository(preferences, moshi)
@Provides
fun provideNotificationManager(context: Application) =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@Provides
@Singleton
......@@ -307,12 +297,12 @@ class AppModule {
@Provides
@Singleton
fun providePushManager(
context: Application,
groupedPushes: GroupedPush,
manager: NotificationManager,
moshi: Moshi,
getAccountInteractor: GetAccountInteractor,
getSettingsInteractor: GetSettingsInteractor): PushManager {
context: Application,
groupedPushes: GroupedPush,
manager: NotificationManager,
moshi: Moshi,
getAccountInteractor: GetAccountInteractor,
getSettingsInteractor: GetSettingsInteractor): PushManager {
return PushManager(groupedPushes, manager, moshi, getAccountInteractor, getSettingsInteractor, context)
}
......@@ -324,9 +314,9 @@ class AppModule {
@Provides
fun provideSendMessageJob(context: Application): JobInfo {
return JobInfo.Builder(MessageService.RETRY_SEND_MESSAGE_ID,
ComponentName(context, MessageService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build()
ComponentName(context, MessageService::class.java))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.build()
}
@Provides
......
package chat.rocket.android.helper
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
object AndroidPermissionsHelper {
const val WRITE_EXTERNAL_STORAGE_CODE = 1
fun checkPermission(context: Context, permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) ==
PackageManager.PERMISSION_GRANTED
}
fun requestPermission(context: Activity, permission: String, requestCode: Int) {
ActivityCompat.requestPermissions(context, arrayOf(permission), requestCode)
}
}
\ No newline at end of file
package chat.rocket.android.helper
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.net.Uri
import android.support.customtabs.CustomTabsIntent
import android.provider.Browser
import android.support.v4.content.res.ResourcesCompat
import android.text.Spanned
import android.text.style.ClickableSpan
......@@ -17,6 +14,8 @@ import android.text.style.ReplacementSpan
import android.util.Patterns
import android.view.View
import chat.rocket.android.R
import chat.rocket.android.server.domain.PublicSettings
import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiRepository
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
......@@ -29,23 +28,34 @@ import ru.noties.markwon.Markwon
import ru.noties.markwon.SpannableBuilder
import ru.noties.markwon.SpannableConfiguration
import ru.noties.markwon.renderer.SpannableMarkdownVisitor
import timber.log.Timber
import javax.inject.Inject
class MessageParser @Inject constructor(val context: Application, private val configuration: SpannableConfiguration) {
class MessageParser @Inject constructor(
private val context: Application,
private val configuration: SpannableConfiguration,
private val settings: PublicSettings
) {
private val parser = Markwon.createParser()
/**
* Render a markdown text message to Spannable.
* Render markdown and other rules on message to rich text with spans.
*
* @param message The [Message] object we're interested on rendering.
* @param selfUsername This user username.
*
* @return A Spannable with the parsed markdown.
*/
fun renderMarkdown(message: Message, selfUsername: String? = null): CharSequence {
val text = message.message
fun render(message: Message, selfUsername: String? = null): CharSequence {
var text: String = message.message
val mentions = mutableListOf<String>()
message.mentions?.forEach {
val mention = getMention(it)
mentions.add(mention)
if (it.username != null) {
text = text.replace("@${it.username}", mention)
}
}
val builder = SpannableBuilder()
val content = EmojiRepository.shortnameToUnicode(text, true)
val parentNode = parser.parse(toLenientMarkdown(content))
......@@ -53,7 +63,7 @@ class MessageParser @Inject constructor(val context: Application, private val co
parentNode.accept(LinkVisitor(builder))
parentNode.accept(EmojiVisitor(configuration, builder))
message.mentions?.let {
parentNode.accept(MentionVisitor(context, builder, it, selfUsername))
parentNode.accept(MentionVisitor(context, builder, mentions, selfUsername))
}
return builder.text()
......@@ -62,47 +72,61 @@ class MessageParser @Inject constructor(val context: Application, private val co
// Convert to a lenient markdown consistent with Rocket.Chat web markdown instead of the official specs.
private fun toLenientMarkdown(text: String): String {
return text.trim().replace("\\*(.+)\\*".toRegex()) { "**${it.groupValues[1].trim()}**" }
.replace("\\~(.+)\\~".toRegex()) { "~~${it.groupValues[1].trim()}~~" }
.replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" }
.replace("\\~(.+)\\~".toRegex()) { "~~${it.groupValues[1].trim()}~~" }
.replace("\\_(.+)\\_".toRegex()) { "_${it.groupValues[1].trim()}_" }
}
private fun getMention(user: SimpleUser): String {
return if (settings.useRealName()) {
user.name ?: "@${user.username}"
} else {
"@${user.username}"
}
}
class MentionVisitor(context: Context,
private val builder: SpannableBuilder,
private val mentions: List<SimpleUser>,
private val currentUser: String?) : AbstractVisitor() {
class MentionVisitor(
context: Context,
private val builder: SpannableBuilder,
private val mentions: List<String>,
private val currentUser: String?
) : AbstractVisitor() {
private val othersTextColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val othersBackgroundColor = ResourcesCompat.getColor(context.resources, android.R.color.transparent, context.theme)
private val myselfTextColor = ResourcesCompat.getColor(context.resources, R.color.white, context.theme)
private val myselfBackgroundColor = ResourcesCompat.getColor(context.resources, R.color.colorAccent, context.theme)
private val mentionPadding = context.resources.getDimensionPixelSize(R.dimen.padding_mention).toFloat()
private val mentionRadius = context.resources.getDimensionPixelSize(R.dimen.radius_mention).toFloat()
override fun visit(t: Text) {
val text = t.literal
val mentionsList = mentions.map { it.username }.toMutableList()
mentionsList.add("all")
mentionsList.add("here")
mentionsList.toList().forEach {
if (it != null) {
val mentionMe = it == currentUser || it == "all" || it == "here"
var offset = text.indexOf("@$it", 0, true)
while (offset > -1) {
val textColor = if (mentionMe) myselfTextColor else othersTextColor
val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor
val usernameSpan = MentionSpan(backgroundColor, textColor, mentionRadius, mentionPadding,
mentionMe)
// Add 1 to end offset to include the @.
val end = offset + it.length + 1
builder.setSpan(usernameSpan, offset, end, 0)
offset = text.indexOf("@$it", end, true)
}
val mentionsList = mentions.toMutableList().also {
it.add("@all")
it.add("@here")
}.toList()
mentionsList.forEach {
val mentionMe = it == currentUser || it == "@all" || it == "@here"
var offset = text.indexOf(it, 0, true)
while (offset > -1) {
val textColor = if (mentionMe) myselfTextColor else othersTextColor
val backgroundColor = if (mentionMe) myselfBackgroundColor else othersBackgroundColor
val usernameSpan = MentionSpan(backgroundColor, textColor, mentionRadius, mentionPadding,
mentionMe)
// Add 1 to end offset to include the @.
val end = offset + it.length + 1
builder.setSpan(usernameSpan, offset, end, 0)
offset = text.indexOf("@$it", end, true)
}
}
}
}
class EmojiVisitor(configuration: SpannableConfiguration, private val builder: SpannableBuilder)
: SpannableMarkdownVisitor(configuration, builder) {
class EmojiVisitor(
configuration: SpannableConfiguration,
private val builder: SpannableBuilder
) : SpannableMarkdownVisitor(configuration, builder) {
override fun visit(document: Document) {
val spannable = EmojiParser.parse(builder.text())
if (spannable is Spanned) {
......@@ -127,7 +151,7 @@ class MessageParser @Inject constructor(val context: Application, private val co
if (!link.startsWith("@") && link !in consumed) {
builder.setSpan(object : ClickableSpan() {
override fun onClick(view: View) {
with (view) {
with(view) {
val tabsbuilder = CustomTabsIntent.Builder()
tabsbuilder.setToolbarColor(ResourcesCompat.getColor(context.resources, R.color.colorPrimary, context.theme))
val customTabsIntent = tabsbuilder.build()
......@@ -150,11 +174,14 @@ class MessageParser @Inject constructor(val context: Application, private val co
}
}
class MentionSpan(private val backgroundColor: Int,
private val textColor: Int,
private val radius: Float,
padding: Float,
referSelf: Boolean) : ReplacementSpan() {
class MentionSpan(
private val backgroundColor: Int,
private val textColor: Int,
private val radius: Float,
padding: Float,
referSelf: Boolean
) : ReplacementSpan() {
private val padding: Float = if (referSelf) padding else 0F
override fun getSize(paint: Paint,
......
......@@ -54,17 +54,71 @@ object OauthHelper {
/**
* Returns the Gitlab Oauth URL.
*
* @param host The Gitlab host.
* @param clientId The Gitlab client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Gitlab Oauth URL.
*/
fun getGitlabOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://gitlab.com/oauth/authorize" +
fun getGitlabOauthUrl(
host: String? = "https://gitlab.com",
clientId: String,
serverUrl: String,
state: String
): String {
return host +
"/oauth/authorize" +
"?client_id=$clientId" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/gitlab?close" +
"&state=$state" +
"&response_type=code" +
"&scope=read_user"
}
}
\ No newline at end of file
/**
* Returns the Facebook Oauth URL.
*
* @param clientId The Facebook client ID.
* @param serverUrl The server URL.
* @param state An unguessable random string used to protect against forgery attacks.
* @return The Facebook Oauth URL.
*/
fun getFacebookOauthUrl(clientId: String, serverUrl: String, state: String): String {
return "https://facebook.com/v2.9/dialog/oauth" +
"?client_id=$clientId" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/facebook?close" +
"&state=$state" +
"&response_type=code" +
"&scope=email"
}
/**
* Returns the Custom Oauth URL.
*
* @param host The custom OAuth host.
* @param authorizePath The OAuth authorization path.
* @param clientId The custom OAuth client ID.
* @param serverUrl The server URL.
* @param serviceName The service name.
* @param state An unguessable random string used to protect against forgery attacks.
* @param scope The custom OAuth scope.
* @return The Custom Oauth URL.
*/
fun getCustomOauthUrl(
host: String,
authorizePath: String,
clientId: String,
serverUrl: String,
serviceName: String,
state: String,
scope: String
): String {
return host +
authorizePath +
"?client_id=$clientId" +
"&redirect_uri=${serverUrl.removeTrailingSlash()}/_oauth/$serviceName" +
"&state=$state" +
"&scope=$scope" +
"&response_type=code"
}
}
......@@ -7,11 +7,11 @@ interface LocalRepository {
fun save(key: String, value: Int)
fun save(key: String, value: Long)
fun save(key: String, value: Float)
fun get(key: String): String?
fun getBoolean(key: String): Boolean
fun getFloat(key: String): Float
fun getInt(key: String): Int
fun getLong(key: String): Long
fun get(key: String, defValue: String? = null): String?
fun getBoolean(key: String, defValue: Boolean = false): Boolean
fun getFloat(key: String, defValue: Float = -1f): Float
fun getInt(key: String, defValue: Int = -1): Int
fun getLong(key: String, defValue: Long = -1L): Long
fun clear(key: String)
fun clearAllFromServer(server: String)
......
package chat.rocket.android.infrastructure
import android.content.SharedPreferences
import androidx.core.content.edit
class SharedPrefsLocalRepository(private val preferences: SharedPreferences) : LocalRepository {
override fun getBoolean(key: String) = preferences.getBoolean(key, false)
override fun getBoolean(key: String, defValue: Boolean) = preferences.getBoolean(key, defValue)
override fun getFloat(key: String) = preferences.getFloat(key, -1f)
override fun getFloat(key: String, defValue: Float) = preferences.getFloat(key, defValue)
override fun getInt(key: String) = preferences.getInt(key, -1)
override fun getInt(key: String, defValue: Int) = preferences.getInt(key, defValue)
override fun getLong(key: String) = preferences.getLong(key, -1L)
override fun getLong(key: String, defValue: Long) = preferences.getLong(key, defValue)
override fun save(key: String, value: Int) = preferences.edit().putInt(key, value).apply()
override fun save(key: String, value: Int) = preferences.edit { putInt(key, value) }
override fun save(key: String, value: Float) = preferences.edit().putFloat(key, value).apply()
override fun save(key: String, value: Float) = preferences.edit { putFloat(key, value) }
override fun save(key: String, value: Long) = preferences.edit().putLong(key, value).apply()
override fun save(key: String, value: Long) = preferences.edit { putLong(key, value) }
override fun save(key: String, value: Boolean) = preferences.edit().putBoolean(key, value).apply()
override fun save(key: String, value: Boolean) = preferences.edit { putBoolean(key, value) }
override fun save(key: String, value: String?) = preferences.edit().putString(key, value).apply()
override fun save(key: String, value: String?) = preferences.edit { putString(key, value) }
override fun get(key: String): String? = preferences.getString(key, null)
override fun get(key: String, defValue: String?): String? = preferences.getString(key, defValue)
override fun clear(key: String) = preferences.edit().remove(key).apply()
override fun clear(key: String) = preferences.edit { remove(key) }
override fun clearAllFromServer(server: String) {
clear(LocalRepository.KEY_PUSH_TOKEN)
......
......@@ -135,7 +135,7 @@ class MainPresenter @Inject constructor(
navigator.toServerScreen()
}
fun changeStatus(userStatus: UserStatus) {
fun changeDefaultStatus(userStatus: UserStatus) {
launchUI(strategy) {
try {
client.setDefaultStatus(userStatus)
......@@ -186,9 +186,9 @@ class MainPresenter @Inject constructor(
private suspend fun subscribeMyselfUpdates() {
manager.addUserDataChannel(userDataChannel)
for (myself in userDataChannel) {
updateMyself(myself)
}
for (myself in userDataChannel) {
updateMyself(myself)
}
}
private suspend fun updateMyself(myself: Myself) {
......
......@@ -84,10 +84,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
override fun showUserStatus(userStatus: UserStatus) {
headerLayout.apply {
image_user_status.setImageDrawable(
DrawableHelper.getUserStatusDrawable(
userStatus,
this.context
)
DrawableHelper.getUserStatusDrawable(userStatus, this.context)
)
}
}
......@@ -141,7 +138,7 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector, HasSupp
accounts_list.layoutManager = LinearLayoutManager(this)
accounts_list.adapter = AccountsAdapter(accounts, object : Selector {
override fun onStatusSelected(userStatus: UserStatus) {
presenter.changeStatus(userStatus)
presenter.changeDefaultStatus(userStatus)
}
override fun onAccountSelected(serverUrl: String) {
......
......@@ -14,12 +14,14 @@ import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.getMembers
import javax.inject.Inject
class MembersPresenter @Inject constructor(private val view: MembersView,
private val navigator: MembersNavigator,
private val strategy: CancelStrategy,
private val serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory,
private val mapper: MemberViewModelMapper) {
class MembersPresenter @Inject constructor(
private val view: MembersView,
private val navigator: MembersNavigator,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory,
private val mapper: MemberViewModelMapper
) {
private val client: RocketChatClient = factory.create(serverInteractor.get()!!)
fun loadChatRoomsMembers(chatRoomId: String, chatRoomType: String, offset: Long = 0) {
......@@ -49,7 +51,7 @@ class MembersPresenter @Inject constructor(private val view: MembersView,
val realName = memberViewModel.realName.toString()
val username = "@${memberViewModel.username}"
val email = memberViewModel.email ?: ""
val utcOffset = memberViewModel.utcOffset.toString()
val utcOffset = memberViewModel.utcOffset.toString()
navigator.toMemberDetails(avatarUri, realName, username, email, utcOffset)
}
......
......@@ -25,7 +25,6 @@ import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_members.*
import javax.inject.Inject
fun newInstance(chatRoomId: String, chatRoomType: String): Fragment {
return MembersFragment().apply {
arguments = Bundle(1).apply {
......
......@@ -28,14 +28,21 @@ class ProfilePresenter @Inject constructor(private val view: ProfileView,
view.showLoading()
try {
val myself = retryIO("me") { client.me() }
myselfId = myself.id!!
val avatarUrl = serverUrl.avatarUrl(myself.username!!)
view.showProfile(
avatarUrl,
myself.name ?: "",
myself.username ?: "",
myself.emails?.get(0)?.address!!
)
val id = myself.id
val username = myself.username
if (id == null || username == null) {
view.showGenericErrorMessage()
} else {
myselfId = id
val avatarUrl = serverUrl.avatarUrl(username)
val email = myself.emails?.getOrNull(0)?.address
view.showProfile(
avatarUrl,
myself.name ?: "",
myself.username ?: "",
email
)
}
} catch (exception: RocketChatException) {
view.showMessage(exception)
} finally {
......
......@@ -2,7 +2,6 @@ package chat.rocket.android.profile.presentation
import chat.rocket.android.core.behaviours.LoadingView
import chat.rocket.android.core.behaviours.MessageView
import chat.rocket.core.model.Myself
interface ProfileView : LoadingView, MessageView {
......@@ -14,7 +13,7 @@ interface ProfileView : LoadingView, MessageView {
* @param username The user username.
* @param email The user email.
*/
fun showProfile(avatarUrl: String, name: String, username: String, email: String)
fun showProfile(avatarUrl: String, name: String, username: String, email: String?)
/**
* Shows a profile update successfully message
......
......@@ -55,18 +55,18 @@ class ProfileFragment : Fragment(), ProfileView, ActionMode.Callback {
super.onDestroyView()
}
override fun showProfile(avatarUrl: String, name: String, username: String, email: String) {
override fun showProfile(avatarUrl: String, name: String, username: String, email: String?) {
ui {
image_avatar.setImageURI(avatarUrl)
text_name.textContent = name
text_username.textContent = username
text_email.textContent = email
text_email.textContent = email ?: ""
text_avatar_url.textContent = ""
currentName = name
currentUsername = username
currentEmail = email
currentEmail = email ?: ""
currentAvatar = avatarUrl
profile_container.setVisible(true)
......
package chat.rocket.android.server.domain
import chat.rocket.common.model.User
interface ActiveUsersRepository {
fun save(url: String, activeUsers: List<User>)
fun get(url: String): List<User>
}
\ No newline at end of file
package chat.rocket.android.server.domain
import chat.rocket.common.model.User
import javax.inject.Inject
class GetActiveUsersInteractor @Inject constructor(private val repository: ActiveUsersRepository) {
fun isActiveUserOnRepository(url: String, user: User): Boolean {
return repository.get(url).any { user_ -> user_.id == user.id }
}
fun getAllActiveUsers(url: String): List<User> {
return repository.get(url)
}
fun getActiveUserById(url: String, id: String): User? {
return repository.get(url).find { user -> user.id == id }
}
fun getActiveUserByUsername(url: String, username: String): User? {
return repository.get(url).find { user -> user.username == username }
}
}
\ No newline at end of file
......@@ -8,13 +8,13 @@ import javax.inject.Inject
class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoomsRepository) {
/**
* Get all ChatRoom objects.
* Get all [ChatRoom].
*
* @param url The server url.
*
* @return All the ChatRoom objects.
* @return All the [ChatRoom] objects.
*/
fun get(url: String) = repository.get(url)
fun getAll(url: String) = repository.get(url)
/**
* Get a list of chat rooms that contains the name parameter.
......@@ -23,7 +23,7 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo
* @param name The name of chat room to look for or a chat room that contains this name.
* @return A list of ChatRoom objects with the given name.
*/
suspend fun getByName(url: String, name: String): List<ChatRoom> = withContext(CommonPool) {
suspend fun getAllByName(url: String, name: String): List<ChatRoom> = withContext(CommonPool) {
val allChatRooms = repository.get(url)
if (name.isEmpty()) {
return@withContext allChatRooms
......@@ -34,11 +34,11 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo
}
/**
* Get a specific room by its id.
* Get a specific [ChatRoom] by its id.
*
* @param serverUrl The server url where the room is.
* @param roomId The id of the room to get.
* @return The ChatRoom object or null if we couldn't find any.
* @return The [ChatRoom] object or null if we couldn't find any.
*/
suspend fun getById(serverUrl: String, roomId: String): ChatRoom? = withContext(CommonPool) {
val allChatRooms = repository.get(serverUrl)
......@@ -46,4 +46,43 @@ class GetChatRoomsInteractor @Inject constructor(private val repository: ChatRoo
it.id == roomId
}
}
/**
* Get a specific [ChatRoom] by its name.
*
* @param serverUrl The server url where the room is.
* @param name The name of the room to get.
* @return The [ChatRoom] object or null if we couldn't find any.
*/
fun getByName(serverUrl: String, name: String): ChatRoom? {
return getAll(serverUrl).toMutableList().find { chatRoom -> chatRoom.name == name }
}
/**
* Add a [ChatRoom].
*
* @param url The server url.
* @param chatRoom The [ChatRoom] to be added to the list.
*/
fun add(url: String, chatRoom: ChatRoom) {
val chatRooms: MutableList<ChatRoom> = getAll(url).toMutableList()
synchronized(this) {
chatRooms.add(chatRoom)
}
repository.save(url, chatRooms)
}
/**
* Removes a [ChatRoom].
*
* @param url The server url.
* @param chatRoom The [ChatRoom] to be removed from the list.
*/
fun remove(url: String, chatRoom: ChatRoom) {
val chatRooms: MutableList<ChatRoom> = getAll(url).toMutableList()
synchronized(this) {
chatRooms.removeAll { chatRoom_ -> chatRoom_.id == chatRoom.id }
}
repository.save(url, chatRooms)
}
}
\ No newline at end of file
......@@ -8,15 +8,17 @@ import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.withContext
import javax.inject.Inject
class RefreshSettingsInteractor @Inject constructor(private val factory: RocketChatClientFactory,
private val repository: SettingsRepository) {
class RefreshSettingsInteractor @Inject constructor(
private val factory: RocketChatClientFactory,
private val repository: SettingsRepository
) {
private var settingsFilter = arrayOf(
LDAP_ENABLE, CAS_ENABLE, CAS_LOGIN_URL,
ACCOUNT_REGISTRATION, ACCOUNT_LOGIN_FORM, ACCOUNT_PASSWORD_RESET, ACCOUNT_CUSTOM_FIELDS,
ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR,
ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, ACCOUNT_GITLAB,
ACCOUNT_TWITTER, ACCOUNT_WORDPRESS, ACCOUNT_GITLAB, ACCOUNT_GITLAB_URL,
SITE_URL, SITE_NAME, FAVICON_512, FAVICON_196, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS,
FAVORITE_ROOMS, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE, UPLOAD_WHITELIST_MIMETYPES,
......
package chat.rocket.android.server.domain
import chat.rocket.common.model.User
import javax.inject.Inject
class SaveActiveUsersInteractor @Inject constructor(
private val repository: ActiveUsersRepository,
private val getActiveUsersInteractor: GetActiveUsersInteractor
) {
fun save(url: String, activeUsers: List<User>) {
repository.save(url, activeUsers)
}
fun addActiveUser(url: String, user: User) {
val activeUserList: MutableList<User> =
getActiveUsersInteractor.getAllActiveUsers(url).toMutableList()
synchronized(this) {
activeUserList.add(user)
}
save(url, activeUserList)
}
fun updateActiveUser(url: String, user: User) {
getActiveUsersInteractor.getActiveUserById(url, user.id)?.let {
val newUser = User(
id = user.id,
name = user.name ?: it.name,
username = user.username ?: it.username,
status = user.status ?: it.status,
emails = user.emails ?: it.emails,
utcOffset = user.utcOffset ?: it.utcOffset
)
val activeUserList: MutableList<User> =
getActiveUsersInteractor.getAllActiveUsers(url).toMutableList()
synchronized(this) {
activeUserList.removeAll { user_ -> user_.id == user.id }
}
activeUserList.add(newUser)
save(url, activeUserList)
}
}
}
\ No newline at end of file
......@@ -26,6 +26,7 @@ const val ACCOUNT_METEOR = "Accounts_OAuth_Meteor"
const val ACCOUNT_TWITTER = "Accounts_OAuth_Twitter"
const val ACCOUNT_WORDPRESS = "Accounts_OAuth_Wordpress"
const val ACCOUNT_GITLAB = "Accounts_OAuth_Gitlab"
const val ACCOUNT_GITLAB_URL = "API_Gitlab_URL"
const val SITE_URL = "Site_Url"
const val SITE_NAME = "Site_Name"
......@@ -68,6 +69,7 @@ fun PublicSettings.isLinkedinAuthenticationEnabled(): Boolean = this[ACCOUNT_LIN
fun PublicSettings.isMeteorAuthenticationEnabled(): Boolean = this[ACCOUNT_METEOR]?.value == true
fun PublicSettings.isTwitterAuthenticationEnabled(): Boolean = this[ACCOUNT_TWITTER]?.value == true
fun PublicSettings.isGitlabAuthenticationEnabled(): Boolean = this[ACCOUNT_GITLAB]?.value == true
fun PublicSettings.gitlabUrl(): String? = this[ACCOUNT_GITLAB_URL]?.value as String?
fun PublicSettings.isWordpressAuthenticationEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value == true
fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true
......
package chat.rocket.android.server.infraestructure
import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.User
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.realtime.subscribeSubscriptions
import chat.rocket.core.internal.realtime.subscribeRooms
import chat.rocket.core.internal.realtime.subscribeUserData
import chat.rocket.core.internal.realtime.subscribeActiveUsers
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.unsubscribe
import chat.rocket.core.internal.realtime.socket.connect
import chat.rocket.core.internal.realtime.socket.disconnect
import chat.rocket.core.internal.realtime.socket.model.State
import chat.rocket.core.internal.realtime.socket.model.StreamMessage
import chat.rocket.core.internal.realtime.subscribeRoomMessages
import chat.rocket.core.internal.realtime.subscribeRooms
import chat.rocket.core.internal.realtime.subscribeSubscriptions
import chat.rocket.core.internal.realtime.subscribeUserDataChanges
import chat.rocket.core.internal.rest.chatRooms
import chat.rocket.core.model.Message
import chat.rocket.core.model.Myself
......@@ -28,11 +30,13 @@ class ConnectionManager(internal val client: RocketChatClient) {
private val roomAndSubscriptionChannels = ArrayList<Channel<StreamMessage<BaseRoom>>>()
private val roomMessagesChannels = LinkedHashMap<String, Channel<Message>>()
private val userDataChannels = ArrayList<Channel<Myself>>()
private val activeUsersChannels = ArrayList<Channel<User>>()
private val subscriptionIdMap = HashMap<String, String>()
private var subscriptionId: String? = null
private var roomsId: String? = null
private var userId: String? = null
private var userDataId: String? = null
private var activeUserId: String? = null
fun connect() {
if (connectJob?.isActive == true && (state !is State.Disconnected)) {
......@@ -60,9 +64,13 @@ class ConnectionManager(internal val client: RocketChatClient) {
Timber.d("Subscribed to rooms: $id")
roomsId = id
}
client.subscribeUserDataChanges { _, id ->
Timber.d("Subscribed to the user: $id")
userId = id
client.subscribeUserData { _, id ->
Timber.d("Subscribed to the userData id: $id")
userDataId = id
}
client.subscribeActiveUsers { _, id ->
Timber.d("Subscribed to the activeUser id: $id")
activeUserId = id
}
resubscribeRooms()
......@@ -115,6 +123,14 @@ class ConnectionManager(internal val client: RocketChatClient) {
}
}
launch(parent = connectJob) {
for (user in client.activeUsersChannel) {
Timber.d("Got activeUsers")
for (channel in activeUsersChannels) {
channel.send(user)
}
}
}
client.connect()
// Broadcast initial state...
......@@ -154,6 +170,10 @@ class ConnectionManager(internal val client: RocketChatClient) {
fun removeUserDataChannel(channel: Channel<Myself>) = userDataChannels.remove(channel)
fun addActiveUserChannel(channel: Channel<User>) = activeUsersChannels.add(channel)
fun removeActiveUserChannel(channel: Channel<User>) = activeUsersChannels.remove(channel)
fun subscribeRoomMessages(roomId: String, channel: Channel<Message>) {
val oldSub = roomMessagesChannels.put(roomId, channel)
if (oldSub != null) {
......
package chat.rocket.android.server.infraestructure
import chat.rocket.android.server.domain.ActiveUsersRepository
import chat.rocket.common.model.User
class MemoryActiveUsersRepository : ActiveUsersRepository {
val cache = HashMap<String, List<User>>()
override fun save(url: String, activeUsers: List<User>) {
cache[url] = activeUsers
}
override fun get(url: String): List<User> = cache[url] ?: emptyList()
}
\ No newline at end of file
......@@ -7,6 +7,7 @@ import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.VersionInfo
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.android.util.retryIO
import chat.rocket.common.RocketChatInvalidProtocolException
import chat.rocket.core.RocketChatClient
import chat.rocket.core.internal.rest.serverInfo
import kotlinx.coroutines.experimental.Deferred
......@@ -18,17 +19,18 @@ abstract class CheckServerPresenter constructor(private val strategy: CancelStra
private val factory: RocketChatClientFactory,
private val view: VersionCheckView) {
private lateinit var currentServer: String
private val client: RocketChatClient by lazy {
factory.create(currentServer)
}
private lateinit var client: RocketChatClient
internal fun checkServerInfo(serverUrl: String): Job {
return launchUI(strategy) {
try {
currentServer = serverUrl
client = factory.create(currentServer)
val version = checkServerVersion(serverUrl).await()
when (version) {
is Version.VersionOk -> {
Timber.i("Your version is nice! (Requires: 0.62.0, Yours: ${version.version})")
view.versionOk()
}
is Version.RecommendedVersionWarning -> {
Timber.i("Your server ${version.version} is bellow recommended version ${BuildConfig.RECOMMENDED_SERVER_VERSION}")
......@@ -41,6 +43,14 @@ abstract class CheckServerPresenter constructor(private val strategy: CancelStra
}
} catch (ex: Exception) {
Timber.d(ex, "Error getting server info")
when(ex) {
is RocketChatInvalidProtocolException -> {
view.errorInvalidProtocol()
}
else -> {
view.errorCheckingServerVersion()
}
}
}
}
}
......
......@@ -41,7 +41,7 @@ private const val INTENT_SERVER_URL = "INTENT_SERVER_URL"
private const val INTENT_CHAT_ROOM_NAME = "INTENT_CHAT_ROOM_NAME"
private const val INTENT_CHAT_ROOM_TYPE = "INTENT_CHAT_ROOM_TYPE"
fun Context.changeServerIntent(serverUrl: String?): Intent {
fun Context.changeServerIntent(serverUrl: String? = null): Intent {
return Intent(this, ChangeServerActivity::class.java).apply {
serverUrl?.let { url ->
putExtra(INTENT_SERVER_URL, url)
......
package chat.rocket.android.settings.about.ui
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import chat.rocket.android.BuildConfig
import chat.rocket.android.R
import chat.rocket.android.util.extensions.textContent
......@@ -19,15 +19,13 @@ class AboutActivity : AppCompatActivity() {
}
private fun setupViews() {
val versionName = resources.getString(R.string.msg_version) +" "+BuildConfig.VERSION_NAME
val versionCode = resources.getString(R.string.msg_build)+" #"+ BuildConfig.VERSION_CODE
text_version_name.text = versionName
text_build_number.text = versionCode
text_version_name.text = getString(R.string.msg_version, BuildConfig.VERSION_NAME)
text_build_number.text = getString(R.string.msg_build, BuildConfig.VERSION_CODE)
}
private fun setupToolbar() {
setSupportActionBar(toolbar)
text_change_password.textContent = resources.getString(R.string.title_about)
text_change_password.textContent = getString(R.string.title_about)
}
override fun onBackPressed() {
......
......@@ -11,10 +11,12 @@ import chat.rocket.core.internal.rest.me
import chat.rocket.core.internal.rest.updateProfile
import javax.inject.Inject
class PasswordPresenter @Inject constructor (private val view: PasswordView,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory){
class PasswordPresenter @Inject constructor(
private val view: PasswordView,
private val strategy: CancelStrategy,
serverInteractor: GetCurrentServerInteractor,
factory: RocketChatClientFactory
) {
private val serverUrl = serverInteractor.get()!!
private val client: RocketChatClient = factory.create(serverUrl)
......
package chat.rocket.android.util.extensions
import android.graphics.Color
import android.util.Patterns
import timber.log.Timber
fun String.removeTrailingSlash(): String {
return if (isNotEmpty() && this[length - 1] == '/') {
this.replace("/+$", "")
this.substring(0, length - 1)
} else {
this
}
}
fun String.sanitize(): String {
val tmp = this.trim()
return tmp.removeTrailingSlash()
}
fun String.avatarUrl(avatar: String, isGroupOrChannel: Boolean = false, format: String = "jpeg"): String {
return if (isGroupOrChannel) {
"${removeTrailingSlash()}/avatar/%23${avatar.removeTrailingSlash()}?format=$format"
......@@ -27,4 +34,14 @@ fun String.termsOfServiceUrl() = "${removeTrailingSlash()}/terms-of-service"
fun String.privacyPolicyUrl() = "${removeTrailingSlash()}/privacy-policy"
fun String.isValidUrl(): Boolean = Patterns.WEB_URL.matcher(this).matches()
\ No newline at end of file
fun String.isValidUrl(): Boolean = Patterns.WEB_URL.matcher(this).matches()
fun String.parseColor(): Int {
return try {
Color.parseColor(this)
} catch (exception: IllegalArgumentException) {
// Log the exception and get the white color.
Timber.e(exception)
Color.parseColor("white")
}
}
\ No newline at end of file
......@@ -15,6 +15,7 @@ import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import chat.rocket.android.R
// TODO: Remove. Use KTX instead.
fun View.setVisible(visible: Boolean) {
visibility = if (visible) {
View.VISIBLE
......@@ -28,30 +29,42 @@ fun View.isVisible(): Boolean {
}
fun ViewGroup.inflate(@LayoutRes resource: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(resource, this, attachToRoot)
LayoutInflater.from(context).inflate(resource, this, attachToRoot)
fun AppCompatActivity.addFragment(tag: String, layoutId: Int, newInstance: () -> Fragment) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
supportFragmentManager.beginTransaction()
.replace(layoutId, fragment, tag)
.commit()
.replace(layoutId, fragment, tag)
.commit()
}
fun AppCompatActivity.addFragmentBackStack(tag: String, layoutId: Int,
newInstance: () -> Fragment) {
fun AppCompatActivity.addFragmentBackStack(
tag: String,
layoutId: Int,
newInstance: () -> Fragment
) {
val fragment = supportFragmentManager.findFragmentByTag(tag) ?: newInstance()
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left,
R.anim.enter_from_left, R.anim.exit_to_right)
.replace(layoutId, fragment, tag)
.addToBackStack(tag)
.commit()
.setCustomAnimations(
R.anim.enter_from_right, R.anim.exit_to_left,
R.anim.enter_from_left, R.anim.exit_to_right
)
.replace(layoutId, fragment, tag)
.addToBackStack(tag)
.commit()
}
fun AppCompatActivity.toPreviousView() {
supportFragmentManager.popBackStack()
}
fun Activity.hideKeyboard() {
if (currentFocus != null) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(currentFocus.windowToken, InputMethodManager.RESULT_UNCHANGED_SHOWN)
imm.hideSoftInputFromWindow(
currentFocus.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
)
}
}
......@@ -61,16 +74,16 @@ fun Activity.showKeyboard(view: View) {
}
fun Activity.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) =
showToast(getString(resource), duration)
showToast(getString(resource), duration)
fun Activity.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) =
Toast.makeText(this, message, duration).show()
Toast.makeText(this, message, duration).show()
fun Fragment.showToast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) =
showToast(getString(resource), duration)
showToast(getString(resource), duration)
fun Fragment.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) =
activity?.showToast(message, duration)
activity?.showToast(message, duration)
fun RecyclerView.isAtBottom(): Boolean {
val manager: RecyclerView.LayoutManager? = layoutManager
......
......@@ -30,7 +30,7 @@ private const val JSON_CREDENTIAL_SECRET = "credentialSecret"
const val INTENT_OAUTH_CREDENTIAL_TOKEN = "credential_token"
const val INTENT_OAUTH_CREDENTIAL_SECRET = "credential_secret"
// Shows a WebView to the user authenticate with your Gitlab credentials.
// Shows a WebView to the user authenticate with its OAuth credential.
class OauthWebViewActivity : AppCompatActivity() {
private lateinit var webPageUrl: String
private lateinit var state: String
......@@ -78,6 +78,7 @@ class OauthWebViewActivity : AppCompatActivity() {
private fun setupWebView() {
with(web_view.settings) {
javaScriptEnabled = true
domStorageEnabled = true
// TODO Remove this workaround that is required to make Google OAuth to work. We should use Custom Tabs instead. See https://github.com/RocketChat/Rocket.Chat.Android/issues/968
if (webPageUrl.contains("google")) {
userAgentString = "Mozilla/5.0 (Linux; Android 4.1.1; Galaxy Nexus Build/JRO03C) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/43.0.2357.65 Mobile Safari/535.19"
......
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:fillColor="#9EA2A8"
android:fillType="evenOdd"
android:pathData="M2.4,0h1.2v12h-1.2z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#9EA2A8"
android:fillType="evenOdd"
android:pathData="M0,2.4h12v1.2h-12z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#9EA2A8"
android:fillType="evenOdd"
android:pathData="M0,8.4h12v1.2h-12z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#9EA2A8"
android:fillType="evenOdd"
android:pathData="M8.4,0h1.2v12h-1.2z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M1.5,5.5h9v6h-9z"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#9EA2A8"
android:fillType="evenOdd"/>
<path
android:pathData="M2.5,5.5L9.5,5.5L9.5,4C9.5,2.067 7.933,0.5 6,0.5C4.067,0.5 2.5,2.067 2.5,4L2.5,5.5Z"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#9EA2A8"
android:fillType="evenOdd"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:fillColor="#FFFFD100"
android:fillType="evenOdd"
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:pathData="M6,6m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#00000000"
android:fillType="evenOdd"
android:pathData="M12,12m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2" />
</vector>
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:fillColor="#FFFF2A57"
android:fillType="evenOdd"
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:pathData="M6,6m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#00000000"
android:fillType="evenOdd"
android:pathData="M12,12m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2" />
</vector>
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:fillColor="#FFCBCED1"
android:fillType="evenOdd"
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:pathData="M6,6m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#00000000"
android:fillType="evenOdd"
android:pathData="M12,12m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2" />
</vector>
</vector>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:width="12dp"
android:height="12dp"
android:viewportHeight="12"
android:viewportWidth="12">
<path
android:fillColor="#FF2DE0A5"
android:fillType="evenOdd"
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:pathData="M6,6m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:strokeColor="#00000000"
android:strokeWidth="1" />
<path
android:fillColor="#00000000"
android:fillType="evenOdd"
android:pathData="M12,12m-11,0a11,11 0,1 1,22 0a11,11 0,1 1,-22 0"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2" />
</vector>
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="10dp"
android:viewportWidth="10.0"
android:viewportHeight="10.0">
<path
android:pathData="M5,5m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:fillType="evenOdd"
android:fillColor="#FFFFFF"
android:strokeWidth="1"/>
</vector>
\ No newline at end of file
......@@ -72,6 +72,21 @@
app:layout_constraintTop_toBottomOf="@+id/button_cas"
tools:visibility="visible" />
<TextView
android:id="@+id/text_forgot_your_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="8dp"
android:gravity="center"
android:textColorLink="@color/colorAccent"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_new_to_rocket_chat"
tools:visibility="visible" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
......@@ -99,7 +114,7 @@
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_new_to_rocket_chat"
app:layout_constraintTop_toBottomOf="@+id/text_forgot_your_password"
tools:visibility="visible">
<TextView
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".authentication.resetpassword.ui.ResetPasswordFragment">
<TextView
android:id="@+id/text_headline"
style="@style/Authentication.Headline.TextView"
android:layout_centerHorizontal="true"
android:text="@string/title_reset_password" />
<EditText
android:id="@+id/text_email"
style="@style/Authentication.EditText"
android:layout_below="@id/text_headline"
android:layout_marginTop="32dp"
android:drawableStart="@drawable/ic_email_black_24dp"
android:hint="@string/msg_email"
android:imeOptions="actionDone"
android:inputType="textEmailAddress" />
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"
app:indicatorName="BallPulseIndicator"
tools:visibility="visible" />
<Button
android:id="@+id/button_reset_password"
style="@style/Authentication.Button"
android:layout_alignParentBottom="true"
android:text="@string/title_reset_password" />
</RelativeLayout>
\ No newline at end of file
......@@ -18,24 +18,30 @@
android:id="@+id/text_server_url"
style="@style/Authentication.EditText"
android:layout_below="@id/text_headline"
android:layout_marginStart="-4dp"
android:layout_marginStart="-6dp"
android:layout_marginTop="32dp"
android:layout_toEndOf="@id/text_server_protocol"
android:layout_toEndOf="@id/protocol_container"
android:cursorVisible="false"
android:hint="@string/default_server"
android:imeOptions="actionDone"
android:digits="0123456789abcdefghijklmnopqrstuvwxyz.-/:"
android:inputType="textUri"
android:paddingEnd="0dp"
android:paddingStart="4dp" />
android:inputType="text|textUri"
android:paddingEnd="0dp" />
<TextView
android:id="@+id/text_server_protocol"
style="@style/Authentication.TextView"
<FrameLayout
android:id="@+id/protocol_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/style_edit_text_authentication"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_below="@id/text_headline"
android:layout_marginTop="32dp"
android:gravity="center_vertical"
android:text="@string/default_protocol" />
android:layout_marginTop="32dp">
<Spinner
android:id="@+id/text_server_protocol"
android:spinnerMode="dropdown"
android:layout_width="120dp"
android:layout_height="50dp"
android:backgroundTint="@color/actionMenuColor" />
</FrameLayout>
<com.wang.avi.AVLoadingIndicatorView
android:id="@+id/view_loading"
......
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_layout"
......@@ -11,7 +12,10 @@
android:id="@+id/view_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator"
......@@ -19,9 +23,12 @@
<FrameLayout
android:id="@+id/message_list_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/layout_message_composer">
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer">
<include
android:id="@+id/layout_message_list"
......@@ -31,11 +38,57 @@
</FrameLayout>
<ImageView
android:id="@+id/image_chat_icon"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/ic_chat_black_24dp"
android:tint="@color/icon_grey"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_chat_title"
app:layout_constraintVertical_chainStyle="packed"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/text_chat_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/msg_no_chat_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/image_chat_icon"
app:layout_constraintBottom_toTopOf="@id/text_chat_description"
android:textSize="20sp"
android:layout_marginTop="24dp"
android:textStyle="bold"
android:textColor="@color/colorSecondaryText"
android:visibility="gone"
tools:visibility="visible"/>
<TextView
android:id="@+id/text_chat_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/msg_no_chat_description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_chat_title"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:layout_marginTop="16dp"
android:textAlignment="center"
android:textSize="16sp"
android:textColor="@color/colorSecondaryTextLight"
android:visibility="gone"
tools:visibility="visible"/>
<chat.rocket.android.widget.autocompletion.ui.SuggestionsView
android:id="@+id/suggestions_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:background="@color/suggestion_background_color" />
<include
......@@ -43,13 +96,13 @@
layout="@layout/message_composer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
app:layout_constraintBottom_toBottomOf="parent" />
<View
android:id="@+id/view_dim"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@+id/layout_message_composer"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:background="@color/colorDim"
android:visibility="gone" />
......@@ -58,7 +111,7 @@
layout="@layout/message_attachment_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/layout_message_composer"
app:layout_constraintBottom_toTopOf="@id/layout_message_composer"
android:layout_margin="5dp"
android:visibility="gone" />
......@@ -77,4 +130,4 @@
tools:text="connected"
tools:visibility="visible" />
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -16,6 +16,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"
app:indicatorColor="@color/black"
app:indicatorName="BallPulseIndicator" />
......@@ -33,15 +34,15 @@
android:id="@+id/connection_status_text"
android:layout_width="match_parent"
android:layout_height="32dp"
android:alpha="0"
android:background="@color/colorPrimary"
android:elevation="4dp"
android:textColor="@color/white"
android:gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="@color/white"
android:visibility="gone"
android:alpha="0"
tools:alpha="1"
tools:visibility="visible"
tools:text="connected"/>
tools:text="connected"
tools:visibility="visible" />
</RelativeLayout>
\ No newline at end of file
......@@ -53,8 +53,7 @@
android:layout_marginTop="4dp"
app:layout_constraintTop_toBottomOf="@id/text_author_name"
app:layout_constraintStart_toStartOf="@id/text_author_name"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
app:layout_constraintEnd_toEndOf="parent" />
<include
layout="@layout/layout_reactions"
......
......@@ -15,7 +15,7 @@
android:paddingStart="16dp"
android:paddingTop="8dp"
android:drawablePadding="10dp"
android:drawableStart="@drawable/ic_status_online_24dp"
android:drawableStart="@drawable/ic_status_online_12dp"
android:text="@string/action_online"
android:background="?selectableItemBackground"/>
......@@ -28,7 +28,7 @@
android:paddingStart="16dp"
android:paddingTop="8dp"
android:drawablePadding="10dp"
android:drawableStart="@drawable/ic_status_away_24dp"
android:drawableStart="@drawable/ic_status_away_12dp"
android:text="@string/action_away"
android:background="?selectableItemBackground"/>
......@@ -41,7 +41,7 @@
android:paddingStart="16dp"
android:paddingTop="8dp"
android:drawablePadding="10dp"
android:drawableStart="@drawable/ic_status_busy_24dp"
android:drawableStart="@drawable/ic_status_busy_12dp"
android:text="@string/action_busy"
android:background="?selectableItemBackground"/>
......@@ -54,7 +54,7 @@
android:paddingStart="16dp"
android:paddingTop="8dp"
android:drawablePadding="10dp"
android:drawableStart="@drawable/ic_status_invisible_24dp"
android:drawableStart="@drawable/ic_status_invisible_12dp"
android:text="@string/action_invisible"
android:background="?selectableItemBackground"/>
......
......@@ -5,62 +5,70 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingBottom="@dimen/chat_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/chat_item_top_and_bottom_padding"
android:paddingBottom="@dimen/chat_item_top_and_bottom_padding">
android:paddingStart="@dimen/screen_edge_left_and_right_padding"
android:paddingTop="@dimen/chat_item_top_and_bottom_padding">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/image_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
app:roundedCornerRadius="3dp"
android:layout_marginTop="6dp"
android:layout_marginTop="5dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
app:layout_constraintTop_toTopOf="parent"
app:roundedCornerRadius="3dp" />
<ImageView
android:id="@+id/image_chat_icon"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@+id/image_avatar"
app:layout_constraintTop_toTopOf="@+id/image_avatar"
tools:src="@drawable/ic_hashtag_12dp" />
<TextView
android:id="@+id/text_chat_name"
style="@style/ChatRoom.Name.TextView"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/image_avatar"
android:layout_marginStart="8dp"
android:textDirection="locale"
tools:text="General"/>
app:layout_constraintStart_toEndOf="@+id/image_chat_icon"
tools:text="general" />
<TextView
android:id="@+id/text_last_message_date_time"
style="@style/Timestamp.TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
app:layout_constraintBaseline_toBaselineOf="@+id/text_chat_name"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="@+id/text_chat_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/text_chat_name"
tools:text="11:45 AM" />
<TextView
android:id="@+id/text_last_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="2"
android:layout_marginTop="2dp"
app:layout_constraintStart_toStartOf="@id/text_chat_name"
app:layout_constraintTop_toBottomOf="@id/text_chat_name"
app:layout_constraintEnd_toStartOf="@id/layout_unread_messages_badge"
android:textDirection="locale"
tools:text="You: Type something that is very big and need at least to lines, or maybe even more"/>
app:layout_constraintEnd_toStartOf="@+id/layout_unread_messages_badge"
app:layout_constraintStart_toStartOf="@+id/image_chat_icon"
app:layout_constraintTop_toBottomOf="@+id/text_chat_name"
tools:text="Filipe de Lima Brito: Type something that is very big and need at least to lines, or maybe even more" />
<include
android:id="@+id/layout_unread_messages_badge"
layout="@layout/unread_messages_badge"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
app:layout_constraintTop_toTopOf="@id/text_last_message"
app:layout_constraintEnd_toEndOf="parent"/>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/text_last_message" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/file_attachment_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingBottom="@dimen/message_item_top_and_bottom_padding"
android:paddingEnd="@dimen/screen_edge_left_and_right_padding"
android:paddingStart="@dimen/screen_edge_left_and_right_padding">
<TextView
android:id="@+id/text_file_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:textColor="@color/colorAccent"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:drawableStart="@drawable/ic_files_24dp"
android:drawablePadding="6dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="This is a very, very, very long filename, to test how the layout will work on very very very long filenames.pdf" />
<include
layout="@layout/layout_reactions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/text_file_name"
app:layout_constraintTop_toBottomOf="@id/text_file_name" />
</android.support.constraint.ConstraintLayout>
\ No newline at end of file
......@@ -3,7 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:background="@color/default_background">
<android.support.constraint.ConstraintLayout
android:id="@+id/composer"
......
......@@ -43,8 +43,8 @@
<ImageView
android:id="@+id/image_user_status"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_width="12dp"
android:layout_height="12dp"
app:layout_constraintStart_toStartOf="parent" />
<TextView
......
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_save_image"
android:title="Save to Gallery"
app:showAsAction="never" />
</menu>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -9,6 +9,7 @@
<!-- Text colors -->
<color name="colorPrimaryText">#DE000000</color>
<color name="colorSecondaryText">#787878</color>
<color name="colorSecondaryTextLight">#c1c1c1</color>
<!-- User status colors -->
<color name="colorUserStatusOnline">#2FE1A8</color>
......@@ -44,4 +45,8 @@
<!-- Suggestions -->
<color name="suggestion_background_color">@android:color/white</color>
<color name="icon_grey">#AFADAF</color>
<!-- Default Background Color -->
<color name="default_background">#FAFAFA</color>
</resources>
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