Commit fa5346fc authored by Łukasz Rutkowski's avatar Łukasz Rutkowski

Merge branch 'develop' into reaction-long-click

# Conflicts:
#	app/src/main/java/chat/rocket/android/chatroom/adapter/MessageReactionsAdapter.kt
parents 43f6b737 4b1f14a7
version: 2
build:
machine:
java: oraclejdk8
jobs:
build-kotlin-sdk:
docker:
- image: circleci/android:api-27-alpha
- image: circleci/android:api-28-alpha
environment:
JVM_OPTS: -Xmx3200m
JAVA_TOOL_OPTIONS: -Xmx5024m
steps:
- checkout
- run:
......@@ -18,8 +21,9 @@ jobs:
command: pushd app/ ; ./build-sdk.sh ; popd
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- ~/.gradle/caches
- ~/.gradle/wrapper
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "util/build.gradle" }}-{{ checksum "draw/build.gradle" }}-{{ checksum "emoji/build.gradle" }}-{{ checksum "suggestions/build.gradle" }}
- save_cache:
paths:
- app/libs/
......@@ -30,47 +34,51 @@ jobs:
destination: libs
code-analysis:
docker:
- image: circleci/android:api-27-alpha
- image: circleci/android:api-28-alpha
environment:
JVM_OPTS: -Xmx3200m
JAVA_TOOL_OPTIONS: -Xmx5024m
steps:
- checkout
- run:
name: ANDROID_HOME
command: echo "sdk.dir="$ANDROID_HOME > local.properties
command: echo "sdk.dir="$ANDROID_HOME > local.properties
- run:
name: checkout Rocket.Chat.Kotlin.SDK
command: git clone https://github.com/RocketChat/Rocket.Chat.Kotlin.SDK.git ../Rocket.Chat.Kotlin.SDK
- restore_cache:
key: kotlin-sdk-{{ .Revision }}
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "util/build.gradle" }}-{{ checksum "draw/build.gradle" }}-{{ checksum "emoji/build.gradle" }}-{{ checksum "suggestions/build.gradle" }}
- run:
name: Download Dependencies
command: ./gradlew androidDependencies --quiet --console=plain
command: ./gradlew --no-daemon androidDependencies --quiet --console=plain
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- ~/.gradle/caches
- ~/.gradle/wrapper
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "util/build.gradle" }}-{{ checksum "draw/build.gradle" }}-{{ checksum "emoji/build.gradle" }}-{{ checksum "suggestions/build.gradle" }}
- run:
name: Run Lint #, Checkstyles, PMD, Findbugs...
command: ./gradlew lint
command: ./gradlew --no-daemon lint
- run:
name: Run Unit test
command: ./gradlew test
command: ./gradlew --no-daemon test
- run:
name: Compile Instrumentation test
command: ./gradlew assembleAndroidTest
command: ./gradlew --no-daemon assembleAndroidTest
- store_artifacts:
path: app/build/reports/
destination: reports
build-play-apk:
docker:
- image: circleci/android:api-27-alpha
- image: circleci/android:api-28-alpha
environment:
JVM_OPTS: -Xmx3200m
JAVA_TOOL_OPTIONS: -Xmx5024m
steps:
- checkout
- run:
name: ANDROID_HOME
command: echo "sdk.dir="$ANDROID_HOME > local.properties
- run:
name: restore files from ENV
command: |
......@@ -82,28 +90,32 @@ jobs:
- restore_cache:
key: kotlin-sdk-{{ .Revision }}
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "util/build.gradle" }}-{{ checksum "draw/build.gradle" }}-{{ checksum "emoji/build.gradle" }}-{{ checksum "suggestions/build.gradle" }}
- run:
name: Download Dependencies
command: ./gradlew androidDependencies --quiet --console=plain
command: ./gradlew --no-daemon androidDependencies --quiet --console=plain
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- ~/.gradle/caches
- ~/.gradle/wrapper
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "util/build.gradle" }}-{{ checksum "draw/build.gradle" }}-{{ checksum "emoji/build.gradle" }}-{{ checksum "suggestions/build.gradle" }}
- run:
name: Build APK
command: |
./gradlew assemblePlayRelease --info --console=plain --stacktrace
./gradlew --no-daemon assemblePlayRelease --info --console=plain --stacktrace
- store_artifacts:
path: app/build/outputs/apk
destination: apks
build-foss-apk:
docker:
- image: circleci/android:api-27-alpha
- image: circleci/android:api-28-alpha
environment:
JVM_OPTS: -Xmx3200m
JAVA_TOOL_OPTIONS: -Xmx5024m
steps:
- checkout
- run:
name: ANDROID_HOME
command: echo "sdk.dir="$ANDROID_HOME > local.properties
- run:
name: restore files from ENV
command: |
......@@ -115,18 +127,19 @@ jobs:
- restore_cache:
key: kotlin-sdk-{{ .Revision }}
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "util/build.gradle" }}-{{ checksum "draw/build.gradle" }}-{{ checksum "emoji/build.gradle" }}-{{ checksum "suggestions/build.gradle" }}
- run:
name: Download Dependencies
command: ./gradlew androidDependencies --quiet --console=plain
command: ./gradlew --no-daemon androidDependencies --quiet --console=plain
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}
- ~/.gradle/caches
- ~/.gradle/wrapper
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}-{{ checksum "player/build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "util/build.gradle" }}-{{ checksum "draw/build.gradle" }}-{{ checksum "emoji/build.gradle" }}-{{ checksum "suggestions/build.gradle" }}
- run:
name: Build APK
command: |
./gradlew assembleFossRelease --info --console=plain --stacktrace
./gradlew --no-daemon assembleFossRelease --info --console=plain --stacktrace
- store_artifacts:
path: app/build/outputs/apk
destination: apks
......
......@@ -11,7 +11,7 @@ This repository contains all the code related to the Android native application
## How to build
- You need to download the latest [Android Studio Preview](https://developer.android.com/studio/preview/) version since the stable IDE version does not support the [JetPack](https://developer.android.com/jetpack/) that is beeing used on this application.
- You need to download the latest [Android Studio Preview](https://developer.android.com/studio/preview/) version since the stable IDE version does not support the [JetPack](https://developer.android.com/jetpack/) that is being used on this application.
- 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.
......@@ -39,7 +39,7 @@ cd Rocket.Chat.Android/app
## Bug report & Feature request
Please report via [GitHub issue](https://github.com/RocketChat/Rocket.Chat.Android/issues) :)
Are you having a technical issue trying to compile the app, or setting up Push Notifications? Please use our Community Support channel for that: https://forums.rocket.chat/c/community-support. The issues are only suppose to be used for bugs, improvements and features in the native Android application.
## Coding Style
......
......@@ -94,8 +94,8 @@ if ! check_git_dirty && ! check_last_commit && [ -f "${CURRENT_DIR}"/libs/common
exit 0
fi
cd "${SDK_DIR}" && ./gradlew common:assemble && cd "${CURRENT_DIR}"
cd "${SDK_DIR}" && ./gradlew core:assemble && cd "${CURRENT_DIR}"
cd "${SDK_DIR}" && ./gradlew --no-daemon common:assemble && cd "${CURRENT_DIR}"
cd "${SDK_DIR}" && ./gradlew --no-daemon core:assemble && cd "${CURRENT_DIR}"
rm "${CURRENT_DIR}"/libs/common* "${CURRENT_DIR}"/libs/core*
......
......@@ -16,8 +16,8 @@ android {
applicationId "chat.rocket.android"
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
versionCode 2048
versionName "3.0.0"
versionCode 2049
versionName "3.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
......
This diff is collapsed.
......@@ -113,25 +113,26 @@ interface LoginOptionsView : LoadingView, MessageView {
// CAS account.
/**
* Shows the CAS button if the sign in/sign out via CAS protocol is enabled by the server
* settings.
* Adds a CAS button into accounts container.
*
* REMARK: We must set up the CAS button listener before showing it [setupCasButtonListener].
* @param casUrl The CAS url.
* @param casToken The CAS token
* @param serviceName The SAML service name.
* @param serviceNameColor The SAML service name color (just stylizing).
* @param buttonColor The SAML button color (just stylizing).
* @see [showAccountsView]
*/
fun enableLoginByCas()
/**
* Setups the CAS button.
*
* @param casUrl The CAS URL to authenticate with.
* @param casToken The requested token to be sent to the CAS server.
*/
fun setupCasButtonListener(casUrl: String, casToken: String)
fun addCasButton(
caslUrl: String,
casToken: String,
serviceName: String,
serviceNameColor: Int,
buttonColor: Int
)
// Custom OAuth account.
/**
* Adds a custom OAuth button in the accounts container.
* Adds a custom OAuth button into accounts container.
*
* @customOauthUrl The custom OAuth url.
* @state A random string generated by the app, which you'll verify later
......@@ -151,12 +152,13 @@ interface LoginOptionsView : LoadingView, MessageView {
// SAML account.
/**
* Adds a SAML button in the accounts container.
* Adds a SAML button into accounts container.
*
* @samlUrl The SAML url.
* @serviceName The SAML service name.
* @serviceNameColor The SAML service name color (just stylizing).
* @buttonColor The SAML button color (just stylizing).
* @param samlUrl The SAML url.
* @param samlToken The SAML token.
* @param serviceName The SAML service name.
* @param serviceNameColor The SAML service name color (just stylizing).
* @param buttonColor The SAML button color (just stylizing).
* @see [showAccountsView]
*/
fun addSamlButton(
......
......@@ -40,6 +40,9 @@ private const val GITLAB_OAUTH_URL = "gitlab_oauth_url"
private const val WORDPRESS_OAUTH_URL = "wordpress_oauth_url"
private const val CAS_LOGIN_URL = "cas_login_url"
private const val CAS_TOKEN = "cas_token"
private const val CAS_SERVICE_NAME = "cas_service_name"
private const val CAS_SERVICE_NAME_TEXT_COLOR = "cas_service_name_text_color"
private const val CAS_SERVICE_BUTTON_COLOR = "cas_service_button_color"
private const val CUSTOM_OAUTH_URL = "custom_oauth_url"
private const val CUSTOM_OAUTH_SERVICE_NAME = "custom_oauth_service_name"
private const val CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR = "custom_oauth_service_name_text_color"
......@@ -69,6 +72,9 @@ fun newInstance(
wordpressOauthUrl: String? = null,
casLoginUrl: String? = null,
casToken: String? = null,
casServiceName: String? = null,
casServiceNameTextColor: Int = 0,
casServiceButtonColor: Int = 0,
customOauthUrl: String? = null,
customOauthServiceName: String? = null,
customOauthServiceNameTextColor: Int = 0,
......@@ -95,6 +101,9 @@ fun newInstance(
putString(WORDPRESS_OAUTH_URL, wordpressOauthUrl)
putString(CAS_LOGIN_URL, casLoginUrl)
putString(CAS_TOKEN, casToken)
putString(CAS_SERVICE_NAME, casServiceName)
putInt(CAS_SERVICE_NAME_TEXT_COLOR, casServiceNameTextColor)
putInt(CAS_SERVICE_BUTTON_COLOR, casServiceButtonColor)
putString(CUSTOM_OAUTH_URL, customOauthUrl)
putString(CUSTOM_OAUTH_SERVICE_NAME, customOauthServiceName)
putInt(CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR, customOauthServiceNameTextColor)
......@@ -127,6 +136,9 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
private var wordpressOauthUrl: String? = null
private var casLoginUrl: String? = null
private var casToken: String? = null
private var casServiceName: String? = null
private var casServiceNameTextColor: Int = 0
private var casServiceButtonColor: Int = 0
private var customOauthUrl: String? = null
private var customOauthServiceName: String? = null
private var customOauthServiceTextColor: Int = 0
......@@ -157,6 +169,9 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
wordpressOauthUrl = bundle.getString(WORDPRESS_OAUTH_URL)
casLoginUrl = bundle.getString(CAS_LOGIN_URL)
casToken = bundle.getString(CAS_TOKEN)
casServiceName = bundle.getString(CAS_SERVICE_NAME)
casServiceNameTextColor = bundle.getInt(CAS_SERVICE_NAME_TEXT_COLOR)
casServiceButtonColor = bundle.getInt(CAS_SERVICE_BUTTON_COLOR)
customOauthUrl = bundle.getString(CUSTOM_OAUTH_URL)
customOauthServiceName = bundle.getString(CUSTOM_OAUTH_SERVICE_NAME)
customOauthServiceTextColor = bundle.getInt(CUSTOM_OAUTH_SERVICE_NAME_TEXT_COLOR)
......@@ -200,6 +215,7 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
setupCas()
setupCustomOauth()
setupSaml()
setupAccountsView()
setupLoginWithEmailView()
setupCreateNewAccountView()
}
......@@ -235,19 +251,17 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
setupWordpressButtonListener(wordpressOauthUrl.toString(), state.toString())
enableLoginByWordpress()
}
if (totalSocialAccountsEnabled > 0) {
showAccountsView()
if (totalSocialAccountsEnabled > 3) {
setupExpandAccountsView()
}
}
}
private fun setupCas() {
if (casLoginUrl != null && casToken != null) {
setupCasButtonListener(casLoginUrl.toString(), casToken.toString())
enableLoginByCas()
if (casLoginUrl != null && casToken != null && casServiceName != null) {
addCasButton(
casLoginUrl.toString(),
casToken.toString(),
casServiceName.toString(),
casServiceNameTextColor,
casServiceButtonColor
)
}
}
......@@ -275,6 +289,15 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
}
}
private fun setupAccountsView() {
if (totalSocialAccountsEnabled > 0) {
showAccountsView()
if (totalSocialAccountsEnabled > 3) {
setupExpandAccountsView()
}
}
}
private fun setupLoginWithEmailView() {
if (isLoginFormEnabled) {
showLoginWithEmailButton()
......@@ -319,10 +342,17 @@ class LoginOptionsFragment : Fragment(), LoginOptionsView {
setupButtonListener(button_wordpress, wordpressUrl, state, REQUEST_CODE_FOR_OAUTH)
// CAS service account.
override fun enableLoginByCas() = enableAccountButton(button_cas)
override fun setupCasButtonListener(casUrl: String, casToken: String) =
setupButtonListener(button_cas, casUrl, casToken, REQUEST_CODE_FOR_CAS)
override fun addCasButton(
caslUrl: String,
casToken: String,
serviceName: String,
serviceNameColor: Int,
buttonColor: Int
) {
val button = getCustomServiceButton(serviceName, serviceNameColor, buttonColor)
setupButtonListener(button, caslUrl, casToken, REQUEST_CODE_FOR_CAS)
accounts_container.addView(button)
}
// Custom OAuth account.
override fun addCustomOauthButton(
......
......@@ -43,6 +43,9 @@ class OnBoardingPresenter @Inject constructor(
wordpressOauthUrl,
casLoginUrl,
casToken,
casServiceName,
casServiceNameTextColor,
casServiceButtonColor,
customOauthUrl,
customOauthServiceName,
customOauthServiceNameTextColor,
......@@ -73,9 +76,8 @@ class OnBoardingPresenter @Inject constructor(
view.showLoading()
try {
withContext(DefaultDispatcher) {
refreshSettingsInteractor.refresh(serverUrl)
setupConnectionInfo(serverUrl)
refreshSettingsInteractor.refresh(serverUrl)
// preparing next fragment before showing it
checkEnabledAccounts(serverUrl)
......
......@@ -30,6 +30,9 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
wordpressOauthUrl: String? = null,
casLoginUrl: String? = null,
casToken: String? = null,
casServiceName: String? = null,
casServiceNameTextColor: Int = 0,
casServiceButtonColor: Int = 0,
customOauthUrl: String? = null,
customOauthServiceName: String? = null,
customOauthServiceNameTextColor: Int = 0,
......@@ -59,6 +62,9 @@ class AuthenticationNavigator(internal val activity: AuthenticationActivity) {
wordpressOauthUrl,
casLoginUrl,
casToken,
casServiceName,
casServiceNameTextColor,
casServiceButtonColor,
customOauthUrl,
customOauthServiceName,
customOauthServiceNameTextColor,
......
......@@ -53,6 +53,9 @@ class ServerPresenter @Inject constructor(
wordpressOauthUrl,
casLoginUrl,
casToken,
casServiceName,
casServiceNameTextColor,
casServiceButtonColor,
customOauthUrl,
customOauthServiceName,
customOauthServiceNameTextColor,
......@@ -92,8 +95,6 @@ class ServerPresenter @Inject constructor(
withContext(DefaultDispatcher) {
refreshSettingsInteractor.refresh(serverUrl)
setupConnectionInfo(serverUrl)
// preparing next fragment before showing it
checkEnabledAccounts(serverUrl)
checkIfLoginFormIsEnabled()
......
......@@ -245,7 +245,7 @@ class ServerFragment : Fragment(), ServerView {
serverUrlDisposable = text_server_url.asObservable()
.filter { it.isNotBlank() }
.subscribe {
if (it.toString().isValidUrl()) {
if ("$protocol${it.toString()}".isValidUrl()) {
enableButtonConnect()
} else {
disableButtonConnect()
......
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.uimodel.ActionsAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ButtonAction
import kotlinx.android.synthetic.main.item_actions_attachment.view.*
import androidx.recyclerview.widget.LinearLayoutManager
import timber.log.Timber
class ActionsAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null,
var actionAttachmentOnClickListener: ActionAttachmentOnClickListener
) : BaseViewHolder<ActionsAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(actions_attachment_container)
}
}
override fun bindViews(data: ActionsAttachmentUiModel) {
val actions = data.actions
val alignment = data.buttonAlignment
Timber.d("no of actions : ${actions.size} : $actions")
with(itemView) {
title.text = data.title ?: ""
actions_list.layoutManager = LinearLayoutManager(itemView.context,
when (alignment) {
"horizontal" -> LinearLayoutManager.HORIZONTAL
else -> LinearLayoutManager.VERTICAL //Default
}, false)
actions_list.adapter = ActionsListAdapter(actions, actionAttachmentOnClickListener)
}
}
}
interface ActionAttachmentOnClickListener {
fun onActionClicked(view: View, action: Action)
}
\ No newline at end of file
......@@ -70,4 +70,4 @@ class ActionsListAdapter(actions: List<Action>, var actionAttachmentOnClickListe
val action = actions[position]
holder.bindAction(action)
}
}
\ No newline at end of file
}
package chat.rocket.android.chatroom.adapter
import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.uimodel.AudioAttachmentUiModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_attachment.view.*
class AudioAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<AudioAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(attachment_container)
image_attachment.isVisible = false
audio_video_attachment.isVisible = true
}
}
override fun bindViews(data: AudioAttachmentUiModel) {
with(itemView) {
file_name.text = data.attachmentTitle
audio_video_attachment.setOnClickListener { view ->
data.attachmentUrl.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.net.Uri
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.uimodel.AuthorAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.util.extensions.content
import chat.rocket.common.util.ifNull
import kotlinx.android.synthetic.main.item_author_attachment.view.*
class AuthorAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<AuthorAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(author_attachment_container)
}
}
override fun bindViews(data: AuthorAttachmentUiModel) {
with(itemView) {
data.icon?.let { icon ->
author_icon.isVisible = true
author_icon.setImageURI(icon)
}.ifNull {
author_icon.isGone = true
}
author_icon.setImageURI(data.icon)
text_author_name.content = data.name
data.fields?.let { fields ->
text_fields.content = fields
text_fields.isVisible = true
}.ifNull {
text_fields.isGone = true
}
text_author_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(data.attachmentUrl)))
}
}
}
}
\ No newline at end of file
......@@ -18,7 +18,9 @@ import chat.rocket.android.util.extensions.toList
import chat.rocket.core.model.Message
import chat.rocket.core.model.isSystemMessage
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.flexbox.JustifyContent
abstract class BaseViewHolder<T : BaseUiModel<*>>(
itemView: View,
......@@ -41,13 +43,12 @@ abstract class BaseViewHolder<T : BaseUiModel<*>>(
private fun bindReactions() {
data?.let {
val recyclerView = itemView.findViewById(R.id.recycler_view_reactions) as RecyclerView
val adapter: MessageReactionsAdapter
if (recyclerView.adapter == null) {
adapter = MessageReactionsAdapter()
val adapter: MessageReactionsAdapter = if (recyclerView.adapter == null) {
MessageReactionsAdapter()
} else {
adapter = recyclerView.adapter as MessageReactionsAdapter
adapter.clear()
recyclerView.adapter as MessageReactionsAdapter
}
adapter.clear()
if (it.nextDownStreamMessage == null) {
adapter.listener = object : EmojiReactionListener {
......@@ -65,13 +66,16 @@ abstract class BaseViewHolder<T : BaseUiModel<*>>(
reactionListener?.onReactionLongClicked(emojiShortname, usernames)
}
}
val context = itemView.context
val manager = FlexboxLayoutManager(context, FlexDirection.ROW)
manager.justifyContent = JustifyContent.FLEX_START
recyclerView.layoutManager = manager
recyclerView.adapter = adapter
adapter.addReactions(it.reactions.filterNot { reactionUiModel ->
reactionUiModel.unicode.startsWith(":") && reactionUiModel.url.isNullOrEmpty()
})
if (it.reactions.isNotEmpty()) {
itemView.post { adapter.addReactions(it.reactions) }
}
}
}
}
......@@ -133,4 +137,4 @@ abstract class BaseViewHolder<T : BaseUiModel<*>>(
}
return true
}
}
\ No newline at end of file
}
......@@ -36,37 +36,13 @@ class ChatRoomAdapter(
val view = parent.inflate(R.layout.item_message)
MessageViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.IMAGE_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
ImageAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.AUDIO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
AudioAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.VIDEO_ATTACHMENT -> {
val view = parent.inflate(R.layout.message_attachment)
VideoAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.URL_PREVIEW -> {
val view = parent.inflate(R.layout.message_url_preview)
UrlPreviewViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.MESSAGE_ATTACHMENT -> {
BaseUiModel.ViewType.ATTACHMENT -> {
val view = parent.inflate(R.layout.item_message_attachment)
MessageAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.AUTHOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_author_attachment)
AuthorAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.COLOR_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_color_attachment)
ColorAttachmentViewHolder(view, actionsListener, reactionListener)
}
BaseUiModel.ViewType.GENERIC_FILE_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_file_attachment)
GenericFileAttachmentViewHolder(view, actionsListener, reactionListener)
AttachmentViewHolder(view, actionsListener, reactionListener, actionAttachmentOnClickListener)
}
BaseUiModel.ViewType.MESSAGE_REPLY -> {
val view = parent.inflate(R.layout.item_message_reply)
......@@ -74,10 +50,6 @@ class ChatRoomAdapter(
actionSelectListener?.openDirectMessage(roomName, permalink)
}
}
BaseUiModel.ViewType.ACTIONS_ATTACHMENT -> {
val view = parent.inflate(R.layout.item_actions_attachment)
ActionsAttachmentViewHolder(view, actionsListener, reactionListener, actionAttachmentOnClickListener)
}
else -> {
throw InvalidParameterException("TODO - implement for ${viewType.toViewType()}")
}
......@@ -113,26 +85,12 @@ class ChatRoomAdapter(
when (holder) {
is MessageViewHolder ->
holder.bind(dataSet[position] as MessageUiModel)
is ImageAttachmentViewHolder ->
holder.bind(dataSet[position] as ImageAttachmentUiModel)
is AudioAttachmentViewHolder ->
holder.bind(dataSet[position] as AudioAttachmentUiModel)
is VideoAttachmentViewHolder ->
holder.bind(dataSet[position] as VideoAttachmentUiModel)
is UrlPreviewViewHolder ->
holder.bind(dataSet[position] as UrlPreviewUiModel)
is MessageAttachmentViewHolder ->
holder.bind(dataSet[position] as MessageAttachmentUiModel)
is AuthorAttachmentViewHolder ->
holder.bind(dataSet[position] as AuthorAttachmentUiModel)
is ColorAttachmentViewHolder ->
holder.bind(dataSet[position] as ColorAttachmentUiModel)
is GenericFileAttachmentViewHolder ->
holder.bind(dataSet[position] as GenericFileAttachmentUiModel)
is MessageReplyViewHolder ->
holder.bind(dataSet[position] as MessageReplyUiModel)
is ActionsAttachmentViewHolder ->
holder.bind(dataSet[position] as ActionsAttachmentUiModel)
is AttachmentViewHolder ->
holder.bind(dataSet[position] as AttachmentUiModel)
}
}
......@@ -140,8 +98,7 @@ class ChatRoomAdapter(
val model = dataSet[position]
return when (model) {
is MessageUiModel -> model.messageId.hashCode().toLong()
is BaseFileAttachmentUiModel -> model.id
is AuthorAttachmentUiModel -> model.id
is AttachmentUiModel -> model.id
else -> return position.toLong()
}
}
......@@ -291,6 +248,9 @@ class ChatRoomAdapter(
R.id.action_menu_msg_react -> {
actionSelectListener?.showReactions(id)
}
R.id.action_message_permalink -> {
actionSelectListener?.copyPermalink(id)
}
else -> {
TODO("Not implemented")
}
......@@ -310,5 +270,6 @@ class ChatRoomAdapter(
fun showReactions(id: String)
fun openDirectMessage(roomName: String, message: String)
fun sendMessage(chatRoomId: String, text: String)
fun copyPermalink(id: String)
}
}
\ No newline at end of file
}
package chat.rocket.android.chatroom.adapter
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import chat.rocket.android.R
import chat.rocket.android.chatroom.uimodel.ColorAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.item_color_attachment.view.*
class ColorAttachmentViewHolder(itemView: View,
listener: BaseViewHolder.ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<ColorAttachmentUiModel>(itemView, listener, reactionListener) {
val drawable: Drawable = ColorDrawable(ContextCompat.getColor(itemView.context, R.color.quoteBar))
init {
with(itemView) {
setupActionMenu(color_attachment_container)
attachment_text.movementMethod = LinkMovementMethod()
}
}
override fun bindViews(data: ColorAttachmentUiModel) {
with(itemView) {
quote_bar.setColorFilter(data.color)
if (data.text.isNotEmpty()) {
attachment_text.isVisible = true
attachment_text.text = data.text
} else {
attachment_text.isVisible = false
}
if (data.fields.isNullOrEmpty()) {
text_fields.isVisible = false
} else {
text_fields.isVisible = true
text_fields.text = data.fields
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.content.Intent
import android.view.View
import androidx.core.net.toUri
import chat.rocket.android.chatroom.uimodel.GenericFileAttachmentUiModel
import chat.rocket.android.emoji.EmojiReactionListener
import chat.rocket.android.util.extensions.content
import kotlinx.android.synthetic.main.item_file_attachment.view.*
class GenericFileAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<GenericFileAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(file_attachment_container)
}
}
override fun bindViews(data: GenericFileAttachmentUiModel) {
with(itemView) {
text_file_name.content = data.attachmentTitle
text_file_name.setOnClickListener {
it.context.startActivity(Intent(Intent.ACTION_VIEW, data.attachmentUrl.toUri()))
}
}
}
}
\ No newline at end of file
package chat.rocket.android.chatroom.adapter
import android.view.View
import chat.rocket.android.chatroom.uimodel.ImageAttachmentUiModel
import chat.rocket.android.helper.ImageHelper
import chat.rocket.android.emoji.EmojiReactionListener
import com.facebook.drawee.backends.pipeline.Fresco
import kotlinx.android.synthetic.main.message_attachment.view.*
class ImageAttachmentViewHolder(
itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null
) : BaseViewHolder<ImageAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(attachment_container)
}
}
override fun bindViews(data: ImageAttachmentUiModel) {
with(itemView) {
val controller = Fresco.newDraweeControllerBuilder().apply {
setUri(data.attachmentUrl)
autoPlayAnimations = true
oldController = image_attachment.controller
}.build()
image_attachment.controller = controller
file_name.text = data.attachmentTitle
file_description.text = data.attachmentDescription
file_text.text = data.attachmentText
image_attachment.setOnClickListener {
ImageHelper.openImage(
context,
data.attachmentUrl,
data.attachmentTitle.toString()
)
}
}
}
}
\ No newline at end of file
......@@ -34,17 +34,17 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
}
else -> {
view = inflater.inflate(R.layout.item_reaction, parent, false)
SingleReactionViewHolder(view, listener)
ReactionViewHolder(view, listener)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is SingleReactionViewHolder) {
if (holder is ReactionViewHolder) {
holder.bind(reactions[position])
} else {
holder as AddReactionViewHolder
holder.bind(reactions[0].messageId)
holder.bind(reactions.first().messageId)
}
}
......@@ -72,7 +72,7 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
fun contains(reactionShortname: String) =
reactions.firstOrNull { it.shortname == reactionShortname } != null
class SingleReactionViewHolder(
class ReactionViewHolder(
view: View,
private val listener: EmojiReactionListener?
) : RecyclerView.ViewHolder(view), View.OnClickListener, View.OnLongClickListener {
......@@ -96,9 +96,11 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
this.reaction = reaction
with(itemView) {
if (reaction.url.isNullOrEmpty()) {
text_emoji.text = reaction.unicode
// The view at index 0 corresponds to the one to display unicode text emoji.
view_flipper_reaction.displayedChild = 0
text_emoji.text = reaction.unicode
} else {
// The view at index 1 corresponds to the one to display custom emojis which are images.
view_flipper_reaction.displayedChild = 1
val glideRequest = if (reaction.url!!.endsWith("gif", true)) {
GlideApp.with(context).asGif()
......@@ -109,17 +111,18 @@ class MessageReactionsAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>()
glideRequest.load(reaction.url).into(image_emoji)
}
text_count.text = reaction.count.toString()
val myself = localRepository.get(LocalRepository.CURRENT_USERNAME_KEY)
if (reaction.usernames.contains(myself)) {
val context = itemView.context
text_count.setTextColor(ContextCompat.getColor(context, R.color.colorAccent))
}
view_flipper_reaction.setOnClickListener(this@SingleReactionViewHolder)
text_count.setOnClickListener(this@SingleReactionViewHolder)
view_flipper_reaction.setOnLongClickListener(this@SingleReactionViewHolder)
text_count.setOnLongClickListener(this@SingleReactionViewHolder)
text_count.text = reaction.count.toString()
view_flipper_reaction.setOnClickListener(this@ReactionViewHolder)
text_count.setOnClickListener(this@ReactionViewHolder)
view_flipper_reaction.setOnLongClickListener(this@ReactionViewHolder)
text_count.setOnLongClickListener(this@ReactionViewHolder)
}
}
......
package chat.rocket.android.chatroom.adapter
import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.chatroom.uimodel.VideoAttachmentUiModel
import chat.rocket.android.player.PlayerActivity
import chat.rocket.android.emoji.EmojiReactionListener
import kotlinx.android.synthetic.main.message_attachment.view.*
class VideoAttachmentViewHolder(itemView: View,
listener: ActionsListener,
reactionListener: EmojiReactionListener? = null)
: BaseViewHolder<VideoAttachmentUiModel>(itemView, listener, reactionListener) {
init {
with(itemView) {
setupActionMenu(attachment_container)
image_attachment.isVisible = false
audio_video_attachment.isVisible = true
}
}
override fun bindViews(data: VideoAttachmentUiModel) {
with(itemView) {
file_name.text = data.attachmentTitle
audio_video_attachment.setOnClickListener { view ->
data.attachmentUrl.let { url ->
PlayerActivity.play(view.context, url)
}
}
}
}
}
\ No newline at end of file
......@@ -35,6 +35,7 @@ import chat.rocket.android.server.domain.useRealName
import chat.rocket.android.server.infraestructure.ConnectionManagerFactory
import chat.rocket.android.server.infraestructure.state
import chat.rocket.android.util.extension.compressImageAndGetByteArray
import chat.rocket.android.util.extension.getByteArray
import chat.rocket.android.util.extension.launchUI
import chat.rocket.android.util.extensions.avatarUrl
import chat.rocket.android.util.retryIO
......@@ -80,6 +81,7 @@ import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.withContext
import org.threeten.bp.Instant
import timber.log.Timber
import java.io.InputStream
import java.util.*
import javax.inject.Inject
......@@ -174,7 +176,9 @@ class ChatRoomPresenter @Inject constructor(
view.showLoading()
try {
if (offset == 0L) {
val localMessages = messagesRepository.getByRoomId(chatRoomId)
// FIXME - load just 50 messages from DB to speed up. We will reload from Network after that
// FIXME - We need to handle the pagination, first fetch from DB, then from network
val localMessages = messagesRepository.getRecentMessages(chatRoomId, 50)
val oldMessages = mapper.map(
localMessages, RoomUiModel(
roles = chatRoles,
......@@ -346,15 +350,51 @@ class ChatRoomPresenter @Inject constructor(
view.showFileSelection(settings.uploadMimeTypeFilter())
}
fun uploadFile(roomId: String, uri: Uri, msg: String, bitmap: Bitmap? = null) {
fun uploadImage(roomId: String, mimeType: String, uri: Uri, bitmap: Bitmap, msg: String) {
launchUI(strategy) {
view.showLoading()
try {
withContext(DefaultDispatcher) {
val fileName = uriInteractor.getFileName(uri) ?: uri.toString()
val mimeType = uriInteractor.getMimeType(uri)
val byteArray = bitmap?.compressImageAndGetByteArray(mimeType)
val fileSize = byteArray?.size ?: uriInteractor.getFileSize(uri)
if (fileName.isEmpty()) {
view.showInvalidFileMessage()
} else {
val byteArray =
bitmap.getByteArray(mimeType, 100, settings.uploadMaxFileSize())
retryIO("uploadFile($roomId, $fileName, $mimeType") {
client.uploadFile(
roomId,
fileName,
mimeType,
msg,
description = fileName
) {
byteArray.inputStream()
}
}
logMediaUploaded(mimeType)
}
}
} catch (ex: Exception) {
Timber.d(ex, "Error uploading image")
when (ex) {
is RocketChatException -> view.showMessage(ex)
else -> view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
}
}
fun uploadFile(roomId: String, mimeType: String, uri: Uri, msg: String) {
launchUI(strategy) {
view.showLoading()
try {
withContext(DefaultDispatcher) {
val fileName = uriInteractor.getFileName(uri) ?: uri.toString()
val fileSize = uriInteractor.getFileSize(uri)
val maxFileSizeAllowed = settings.uploadMaxFileSize()
when {
......@@ -370,7 +410,7 @@ class ChatRoomPresenter @Inject constructor(
msg,
description = fileName
) {
byteArray?.inputStream() ?: uriInteractor.getInputStream(uri)
uriInteractor.getInputStream(uri)
}
}
logMediaUploaded(mimeType)
......@@ -503,7 +543,7 @@ class ChatRoomPresenter @Inject constructor(
val messages =
retryIO(description = "history($chatRoomId, $roomType, $instant)") {
client.history(
chatRoomId, roomType, count = 50,
chatRoomId, roomType, count = 50,
oldest = instant
)
}
......@@ -620,6 +660,7 @@ class ChatRoomPresenter @Inject constructor(
try {
messagesRepository.getById(messageId)?.let { m ->
view.copyToClipboard(m.message)
view.showMessage(R.string.msg_message_copied)
}
} catch (e: RocketChatException) {
Timber.e(e)
......@@ -857,6 +898,42 @@ class ChatRoomPresenter @Inject constructor(
}
}
// TODO: move this to new interactor or FetchChatRoomsInteractor?
private suspend fun getChatRoomAsync(roomId: String): ChatRoom? = withContext(CommonPool) {
return@withContext dbManager.chatRoomDao().get(roomId)?.let {
with(it.chatRoom) {
ChatRoom(
id = id,
subscriptionId = subscriptionId,
type = roomTypeOf(type),
unread = unread,
broadcast = broadcast ?: false,
alert = alert,
fullName = fullname,
name = name,
favorite = favorite ?: false,
default = isDefault ?: false,
readonly = readonly,
open = open,
lastMessage = null,
archived = false,
status = null,
user = null,
userMentions = userMentions,
client = client,
announcement = null,
description = null,
groupMentions = groupMentions,
roles = null,
topic = null,
lastSeen = this.lastSeen,
timestamp = timestamp,
updatedAt = updatedAt
)
}
}
}
// TODO: move this to new interactor or FetchChatRoomsInteractor?
private suspend fun getChatRoomsAsync(name: String? = null): List<ChatRoom> = withContext(CommonPool) {
return@withContext dbManager.chatRoomDao().getAllSync().filter {
......@@ -939,6 +1016,24 @@ class ChatRoomPresenter @Inject constructor(
}
}
fun copyPermalink(messageId: String) {
launchUI(strategy) {
try {
messagesRepository.getById(messageId)?.let { message ->
getChatRoomAsync(message.roomId)?.let { chatRoom ->
val models = mapper.map(message)
models.firstOrNull()?.permalink?.let {
view.copyToClipboard(it)
view.showMessage(R.string.msg_permalink_copied)
}
}
}
} catch (ex: Exception) {
Timber.e(ex)
}
}
}
/**
* Send an emoji reaction to a message.
*/
......
......@@ -622,7 +622,6 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
ui {
val clipboard = it.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText("", message)
showToast(R.string.msg_message_copied)
}
}
......@@ -1087,6 +1086,10 @@ class ChatRoomFragment : Fragment(), ChatRoomView, EmojiKeyboardListener, EmojiR
}
}
override fun copyPermalink(id: String) {
presenter.copyPermalink(id)
}
override fun showReactions(id: String) {
presenter.showReactions(id)
}
......
......@@ -7,7 +7,7 @@ import androidx.core.view.isVisible
import chat.rocket.android.emoji.internal.GlideApp
import chat.rocket.android.util.extensions.getFileName
import chat.rocket.android.util.extensions.getMimeType
import com.bumptech.glide.load.resource.gif.GifDrawable
import chat.rocket.common.util.ifNull
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
......@@ -15,10 +15,12 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
imagePreview.isVisible = false
audioVideoAttachment.isVisible = false
textFile.isVisible = false
lateinit var mimeType: String
var bitmap: Bitmap? = null
activity?.let { context ->
uri.getMimeType(context).let { mimeType ->
uri.getMimeType(context).let {
mimeType = it
description.text.clear()
when {
mimeType.startsWith("image") -> {
......@@ -27,7 +29,6 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
.with(context)
.asGif()
.load(uri)
.override(imagePreview.width, imagePreview.height)
.fitCenter()
.into(imagePreview)
} else {
......@@ -35,7 +36,6 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
.with(context)
.asBitmap()
.load(uri)
.override(imagePreview.width, imagePreview.height)
.fitCenter()
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(
......@@ -59,12 +59,22 @@ fun ChatRoomFragment.showFileAttachmentDialog(uri: Uri) {
}
sendButton.setOnClickListener {
presenter.uploadFile(
chatRoomId,
uri,
(citation ?: "") + description.text.toString(),
bitmap
)
bitmap?.let { bitmap ->
presenter.uploadImage(
chatRoomId,
mimeType,
uri,
bitmap,
(citation ?: "") + description.text.toString()
)
}.ifNull {
presenter.uploadFile(
chatRoomId,
mimeType,
uri,
(citation ?: "") + description.text.toString()
)
}
alertDialog.dismiss()
}
cancelButton.setOnClickListener { alertDialog.dismiss() }
......
......@@ -73,4 +73,4 @@ class MessageActionsBottomSheet : BottomSheetDialogFragment() {
}
}
}
}
\ No newline at end of file
}
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ActionsAttachment
data class ActionsAttachmentUiModel(
override val attachmentUrl: String,
val title: String?,
val actions: List<Action>,
val buttonAlignment: String,
override val message: Message,
override val rawData: ActionsAttachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseAttachmentUiModel<ActionsAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.ACTIONS_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_actions_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.actions.Action
data class AttachmentUiModel(
override val message: Message,
override val rawData: Attachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message?,
override var isTemporary: Boolean,
override var unread: Boolean?,
override var currentDayMarkerText: String,
override var showDayMarker: Boolean,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var permalink: String,
val id: Long,
val title: CharSequence?,
val description: CharSequence?,
val authorName: CharSequence?,
val text: CharSequence?,
val color: Int?,
val imageUrl: String?,
val videoUrl: String?,
val audioUrl: String?,
val titleLink: String?,
val messageLink: String?,
val type: String?,
// TODO - attachments
val timestamp: CharSequence?,
val authorIcon: String?,
val authorLink: String?,
val fields: CharSequence?,
val buttonAlignment: String?,
val actions: List<Action>?
) : BaseUiModel<Attachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_message_attachment
val hasTitle: Boolean
get() = !title.isNullOrEmpty()
val hasDescription: Boolean
get() = !description.isNullOrEmpty()
val hasText: Boolean
get() = !text.isNullOrEmpty()
val hasImage: Boolean
get() = imageUrl.orEmpty().isNotEmpty()
val hasVideo: Boolean
get() = videoUrl.orEmpty().isNotEmpty()
val hasAudio: Boolean
get() = audioUrl.orEmpty().isNotEmpty()
val hasAudioOrVideo: Boolean
get() = hasAudio || hasVideo
val hasFile: Boolean
get() = type.orEmpty().contentEquals("file") && titleLink.orEmpty().isNotEmpty()
val hasTitleLink: Boolean
get() = titleLink.orEmpty().isNotEmpty()
val hasMedia: Boolean
get() = hasImage || hasAudioOrVideo || hasFile
val hasMessage: Boolean
get() = messageLink.orEmpty().isNotEmpty()
val hasAuthorName: Boolean
get() = !authorName.isNullOrEmpty()
val hasAuthorLink: Boolean
get() = authorLink.orEmpty().isNotEmpty()
val hasAuthorIcon: Boolean
get() = authorIcon.orEmpty().isNotEmpty()
val hasFields: Boolean
get() = !fields.isNullOrEmpty()
val hasActions: Boolean
get() = actions != null && actions.isNotEmpty()
}
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.AudioAttachment
data class AudioAttachmentUiModel(
override val message: Message,
override val rawData: AudioAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseFileAttachmentUiModel<AudioAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.AUDIO_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.AuthorAttachment
data class AuthorAttachmentUiModel(
override val attachmentUrl: String,
val id: Long,
val name: CharSequence?,
val icon: String?,
val fields: CharSequence?,
override val message: Message,
override val rawData: AuthorAttachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseAttachmentUiModel<AuthorAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.AUTHOR_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_author_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
interface BaseFileAttachmentUiModel<out T> : BaseAttachmentUiModel<T> {
val attachmentTitle: CharSequence
val id: Long
}
\ No newline at end of file
......@@ -17,20 +17,14 @@ interface BaseUiModel<out T> {
var currentDayMarkerText: String
var showDayMarker: Boolean
var menuItemsToHide: MutableList<Int>
var permalink: String
enum class ViewType(val viewType: Int) {
MESSAGE(0),
SYSTEM_MESSAGE(1),
URL_PREVIEW(2),
IMAGE_ATTACHMENT(3),
VIDEO_ATTACHMENT(4),
AUDIO_ATTACHMENT(5),
MESSAGE_ATTACHMENT(6),
AUTHOR_ATTACHMENT(7),
COLOR_ATTACHMENT(8),
GENERIC_FILE_ATTACHMENT(9),
MESSAGE_REPLY(10),
ACTIONS_ATTACHMENT(11)
ATTACHMENT(3),
MESSAGE_REPLY(4)
}
}
......
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.ColorAttachment
data class ColorAttachmentUiModel(
override val attachmentUrl: String,
val id: Long,
val color: Int,
val text: CharSequence,
val fields: CharSequence? = null,
override val message: Message,
override val rawData: ColorAttachment,
override val messageId: String,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean?,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseAttachmentUiModel<ColorAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.COLOR_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_color_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.GenericFileAttachment
data class GenericFileAttachmentUiModel(
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<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseFileAttachmentUiModel<GenericFileAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.GENERIC_FILE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_file_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.ImageAttachment
data class ImageAttachmentUiModel(
override val message: Message,
override val rawData: ImageAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
val attachmentText: String?,
val attachmentDescription: String?,
override val id: Long,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseFileAttachmentUiModel<ImageAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.IMAGE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
data class MessageAttachmentUiModel(
override val message: Message,
override val rawData: Message,
override val messageId: String,
var senderName: String?,
val time: CharSequence?,
val content: CharSequence,
val isPinned: Boolean,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
var messageLink: String? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseUiModel<Message> {
override val viewType: Int
get() = BaseUiModel.ViewType.MESSAGE_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.item_message_attachment
}
\ No newline at end of file
......@@ -15,7 +15,8 @@ data class MessageReplyUiModel(
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
override var showDayMarker: Boolean,
override var permalink: String
) : BaseUiModel<MessageReply> {
override val viewType: Int
get() = BaseUiModel.ViewType.MESSAGE_REPLY.viewType
......
......@@ -20,7 +20,8 @@ data class MessageUiModel(
override var unread: Boolean? = null,
var isFirstUnread: Boolean,
override var isTemporary: Boolean = false,
override var menuItemsToHide: MutableList<Int> = mutableListOf()
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var permalink: String
) : BaseMessageUiModel<Message> {
override val viewType: Int
get() = BaseUiModel.ViewType.MESSAGE.viewType
......
......@@ -19,7 +19,8 @@ data class UrlPreviewUiModel(
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
override var showDayMarker: Boolean,
override var permalink: String
) : BaseUiModel<Url> {
override val viewType: Int
get() = BaseUiModel.ViewType.URL_PREVIEW.viewType
......
package chat.rocket.android.chatroom.uimodel
import chat.rocket.android.R
import chat.rocket.core.model.Message
import chat.rocket.core.model.attachment.VideoAttachment
data class VideoAttachmentUiModel(
override val message: Message,
override val rawData: VideoAttachment,
override val messageId: String,
override val attachmentUrl: String,
override val attachmentTitle: CharSequence,
override val id: Long,
override var reactions: List<ReactionUiModel>,
override var nextDownStreamMessage: BaseUiModel<*>? = null,
override var preview: Message? = null,
override var isTemporary: Boolean = false,
override var unread: Boolean? = null,
override var menuItemsToHide: MutableList<Int> = mutableListOf(),
override var currentDayMarkerText: String,
override var showDayMarker: Boolean
) : BaseFileAttachmentUiModel<VideoAttachment> {
override val viewType: Int
get() = BaseUiModel.ViewType.VIDEO_ATTACHMENT.viewType
override val layoutId: Int
get() = R.layout.message_attachment
}
\ No newline at end of file
......@@ -2,7 +2,9 @@ package chat.rocket.android.chatroom.uimodel.suggestion
import chat.rocket.android.suggestions.model.SuggestionModel
class ChatRoomSuggestionUiModel(text: String,
val fullName: String,
val name: String,
searchList: List<String>) : SuggestionModel(text, searchList, false)
\ No newline at end of file
class ChatRoomSuggestionUiModel(
text: String,
val fullName: String,
val name: String,
searchList: List<String>
) : SuggestionModel(text, searchList, false)
......@@ -2,6 +2,8 @@ package chat.rocket.android.chatroom.uimodel.suggestion
import chat.rocket.android.suggestions.model.SuggestionModel
class CommandSuggestionUiModel(text: String,
val description: String,
searchList: List<String>) : SuggestionModel(text, searchList)
\ No newline at end of file
class CommandSuggestionUiModel(
text: String,
val description: String,
searchList: List<String>
) : SuggestionModel(text, searchList)
\ No newline at end of file
......@@ -3,13 +3,15 @@ package chat.rocket.android.chatroom.uimodel.suggestion
import chat.rocket.android.suggestions.model.SuggestionModel
import chat.rocket.common.model.UserStatus
class PeopleSuggestionUiModel(val imageUri: String?,
text: String,
val username: String,
val name: String,
val status: UserStatus?,
pinned: Boolean = false,
searchList: List<String>) : SuggestionModel(text, searchList, pinned) {
class PeopleSuggestionUiModel(
val imageUri: String?,
text: String,
val username: String,
val name: String,
val status: UserStatus?,
pinned: Boolean = false,
searchList: List<String>
) : SuggestionModel(text, searchList, pinned) {
override fun toString(): String {
return "PeopleSuggestionUiModel(imageUri='$imageUri', username='$username', name='$name', status=$status, pinned=$pinned)"
......
......@@ -24,6 +24,7 @@ import chat.rocket.common.model.roomTypeOf
import chat.rocket.common.model.userStatusOf
import chat.rocket.core.model.Room
import chat.rocket.core.model.SpotlightResult
import ru.noties.markwon.Markwon
class RoomUiModelMapper(
private val context: Application,
......@@ -119,6 +120,8 @@ class RoomUiModelMapper(
type is RoomType.DirectMessage) } else { null }
val open = open
val lastMessageMarkdown = lastMessage?.let { Markwon.markdown(context, it.toString()).toString() }
RoomUiModel(
id = id,
name = roomName,
......@@ -128,7 +131,7 @@ class RoomUiModelMapper(
date = timestamp,
unread = unread,
alert = isUnread,
lastMessage = lastMessage,
lastMessage = lastMessageMarkdown,
status = status,
username = if (type is RoomType.DirectMessage) name else null
)
......
......@@ -33,7 +33,7 @@ class RoomViewHolder(itemView: View, private val listener: (RoomUiModel) -> Unit
if (room.lastMessage != null) {
text_last_message.isVisible = true
text_last_message.text = Markwon.markdown(context, room.lastMessage.toString()).toString()
text_last_message.text = room.lastMessage
} else {
text_last_message.isGone = true
}
......
......@@ -19,17 +19,21 @@ class RoomsAdapter(private val listener: (RoomUiModel) -> Unit) : RecyclerView.A
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<*> {
if (viewType == VIEW_TYPE_ROOM) {
val view = parent.inflate(R.layout.item_chat)
return RoomViewHolder(view, listener)
} else if (viewType == VIEW_TYPE_HEADER) {
val view = parent.inflate(R.layout.item_chatroom_header)
return HeaderViewHolder(view)
} else if (viewType == VIEW_TYPE_LOADING) {
val view = parent.inflate(R.layout.item_loading)
return LoadingViewHolder(view)
return when (viewType) {
VIEW_TYPE_ROOM -> {
val view = parent.inflate(R.layout.item_chat)
RoomViewHolder(view, listener)
}
VIEW_TYPE_HEADER -> {
val view = parent.inflate(R.layout.item_chatroom_header)
HeaderViewHolder(view)
}
VIEW_TYPE_LOADING -> {
val view = parent.inflate(R.layout.item_loading)
LoadingViewHolder(view)
}
else -> throw IllegalStateException("View type must be either Room, Header or Loading")
}
throw IllegalStateException("View type must be either Room, Header or Loading")
}
override fun getItemCount() = values.size
......
......@@ -19,7 +19,7 @@ interface LocalComponent {
fun build(): LocalComponent
}
fun inject(adapter: MessageReactionsAdapter.SingleReactionViewHolder)
fun inject(adapter: MessageReactionsAdapter.ReactionViewHolder)
fun inject(adapter: MessageReactionsAdapter.AddReactionViewHolder)
/*@Component.Builder
......
......@@ -40,6 +40,10 @@ import chat.rocket.android.server.domain.PermissionsRepository
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.domain.BasicAuthRepository
import chat.rocket.android.server.domain.GetBasicAuthInteractor
import chat.rocket.android.server.domain.SaveBasicAuthInteractor
import chat.rocket.android.server.infraestructure.SharedPrefsBasicAuthRepository
import chat.rocket.android.server.infraestructure.DatabaseMessageMapper
import chat.rocket.android.server.infraestructure.DatabaseMessagesRepository
import chat.rocket.android.server.infraestructure.JobSchedulerInteractorImpl
......@@ -53,6 +57,7 @@ import chat.rocket.android.server.infraestructure.SharedPrefsConnectingServerRep
import chat.rocket.android.server.infraestructure.SharedPrefsCurrentServerRepository
import chat.rocket.android.util.AppJsonAdapterFactory
import chat.rocket.android.util.HttpLoggingInterceptor
import chat.rocket.android.util.BasicAuthenticatorInterceptor
import chat.rocket.android.util.TimberLogger
import chat.rocket.common.internal.FallbackSealedClassJsonAdapter
import chat.rocket.common.internal.ISO8601Date
......@@ -106,9 +111,22 @@ class AppModule {
@Provides
@Singleton
fun provideOkHttpClient(logger: HttpLoggingInterceptor): OkHttpClient {
fun provideBasicAuthenticatorInterceptor(
getBasicAuthInteractor: GetBasicAuthInteractor,
saveBasicAuthInteractor: SaveBasicAuthInteractor
): BasicAuthenticatorInterceptor {
return BasicAuthenticatorInterceptor(
getBasicAuthInteractor,
saveBasicAuthInteractor
)
}
@Provides
@Singleton
fun provideOkHttpClient(logger: HttpLoggingInterceptor, basicAuthenticator: BasicAuthenticatorInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(logger)
.addInterceptor(basicAuthenticator)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
......@@ -273,6 +291,14 @@ class AppModule {
return MessageParser(context, configuration, settingsInteractor.get(url))
}
@Provides
@Singleton
fun provideBasicAuthRepository (
preferences: SharedPreferences,
moshi: Moshi
): BasicAuthRepository =
SharedPrefsBasicAuthRepository(preferences, moshi)
@Provides
@Singleton
fun provideAccountsRepository(
......
......@@ -40,7 +40,7 @@ abstract class MessageDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(field: AttachmentFieldEntity)
@Insert(onConflict = OnConflictStrategy.IGNORE)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun insert(reaction: ReactionEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
......
......@@ -23,7 +23,7 @@ import chat.rocket.android.db.model.UserEntity
AttachmentFieldEntity::class, AttachmentActionEntity::class, UrlEntity::class,
ReactionEntity::class, MessagesSync::class
],
version = 9,
version = 10,
exportSchema = true
)
abstract class RCDatabase : RoomDatabase() {
......
package chat.rocket.android.db.model
import android.content.Context
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import chat.rocket.android.R
import chat.rocket.android.util.extension.orFalse
import chat.rocket.android.util.extensions.isNotNullNorEmpty
import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.AudioAttachment
import chat.rocket.core.model.attachment.AuthorAttachment
import chat.rocket.core.model.attachment.ColorAttachment
import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.MessageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.attachment.actions.ActionsAttachment
import chat.rocket.core.model.attachment.actions.ButtonAction
import timber.log.Timber
@Entity(tableName = "attachments",
foreignKeys = [
......@@ -115,150 +109,54 @@ data class AttachmentActionEntity(
var id: Long? = null
}
fun Attachment.asEntity(msgId: String): List<BaseMessageEntity> {
return when(this) {
is ImageAttachment -> listOf(asEntity(msgId))
is VideoAttachment -> listOf(asEntity(msgId))
is AudioAttachment -> listOf(asEntity(msgId))
is AuthorAttachment -> asEntity(msgId)
is ColorAttachment -> asEntity(msgId)
is MessageAttachment -> listOf(asEntity(msgId))
is GenericFileAttachment -> listOf(asEntity(msgId))
is ActionsAttachment -> asEntity(msgId)
else -> {
Timber.d("Missing conversion for: ${javaClass.canonicalName}")
emptyList()
}
}
}
fun ImageAttachment.asEntity(msgId: String): AttachmentEntity =
AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
title = title,
description = description,
text = text,
titleLink = titleLink,
titleLinkDownload = titleLinkDownload.orFalse(),
imageUrl = url,
imageType = type,
imageSize = size
)
fun VideoAttachment.asEntity(msgId: String): AttachmentEntity =
AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
title = title,
description = description,
text = text,
titleLink = titleLink,
titleLinkDownload = titleLinkDownload.orFalse(),
videoUrl = url,
videoType = type,
videoSize = size
)
fun AudioAttachment.asEntity(msgId: String): AttachmentEntity =
AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
title = title,
description = description,
text = text,
titleLink = titleLink,
titleLinkDownload = titleLinkDownload.orFalse(),
audioUrl = url,
audioType = type,
audioSize = size
)
fun AuthorAttachment.asEntity(msgId: String): List<BaseMessageEntity> {
fun Attachment.asEntity(msgId: String, context: Context): List<BaseMessageEntity> {
val attachmentId = "${msgId}_${hashCode()}"
val list = mutableListOf<BaseMessageEntity>()
val attachment = AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
authorLink = url,
authorIcon = authorIcon,
authorName = authorName,
hasFields = fields?.isNotEmpty() == true
)
list.add(attachment)
fields?.forEach { field ->
val entity = AttachmentFieldEntity(
attachmentId = attachment._id,
title = field.title,
value = field.value
)
list.add(entity)
}
val text = mapAttachmentText(text, attachments?.firstOrNull(), context)
return list
}
fun ColorAttachment.asEntity(msgId: String): List<BaseMessageEntity> {
val list = mutableListOf<BaseMessageEntity>()
val attachment = AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
color = color.rawColor,
fallback = fallback,
hasFields = fields?.isNotEmpty() == true
val entity = AttachmentEntity(
_id = attachmentId,
messageId = msgId,
title = title,
type = type,
description = description,
text = text,
titleLink = titleLink,
titleLinkDownload = titleLinkDownload.orFalse(),
imageUrl = imageUrl,
imageType = imageType,
imageSize = imageSize,
videoUrl = videoUrl,
videoType = videoType,
videoSize = videoSize,
audioUrl = audioUrl,
audioType = audioType,
audioSize = audioSize,
authorLink = authorLink,
authorIcon = authorIcon,
authorName = authorName,
color = color?.rawColor,
fallback = fallback,
thumbUrl = thumbUrl,
messageLink = messageLink,
timestamp = timestamp,
buttonAlignment = buttonAlignment,
hasActions = actions?.isNotEmpty() == true,
hasFields = fields?.isNotEmpty() == true
)
list.add(attachment)
list.add(entity)
fields?.forEach { field ->
val entity = AttachmentFieldEntity(
attachmentId = attachment._id,
title = field.title,
value = field.value
attachmentId = attachmentId,
title = field.title,
value = field.value
)
list.add(entity)
}
return list
}
// TODO - how to model An message attachment with attachments???
fun MessageAttachment.asEntity(msgId: String): AttachmentEntity =
AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
authorName = author,
authorIcon = icon,
text = text,
thumbUrl = thumbUrl,
color = color?.rawColor,
messageLink = url,
timestamp = timestamp
)
fun GenericFileAttachment.asEntity(msgId: String): AttachmentEntity =
AttachmentEntity(
_id = "${msgId}_${hashCode()}",
messageId = msgId,
title = title,
description = description,
text = text,
titleLink = titleLink,
titleLinkDownload = titleLinkDownload ?: false
)
fun ActionsAttachment.asEntity(msgId: String): List<BaseMessageEntity> {
val list = mutableListOf<BaseMessageEntity>()
val attachmentId = "${msgId}_${hashCode()}"
val attachment = AttachmentEntity(
_id = attachmentId,
messageId = msgId,
title = title,
hasActions = true,
buttonAlignment = buttonAlignment
)
list.add(attachment)
actions.forEach { action ->
actions?.forEach { action ->
when (action) {
is ButtonAction -> AttachmentActionEntity(
attachmentId = attachmentId,
......@@ -275,4 +173,20 @@ fun ActionsAttachment.asEntity(msgId: String): List<BaseMessageEntity> {
}?.let { list.add(it) }
}
return list
}
\ No newline at end of file
}
fun mapAttachmentText(text: String?, attachment: Attachment?, context: Context): String? {
return if (attachment != null) {
when {
attachment.imageUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_photo)
attachment.videoUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_video)
attachment.audioUrl.isNotNullNorEmpty() -> context.getString(R.string.msg_preview_audio)
attachment.titleLink.isNotNullNorEmpty() &&
attachment.type?.contentEquals("file") == true ->
context.getString(R.string.msg_preview_file)
else -> text
}
} else {
text
}
}
......@@ -35,7 +35,7 @@ object ImageHelper {
// TODO - implement a proper image viewer with a proper Transition
// TODO - We should definitely write our own ImageViewer
fun openImage(context: Context, imageUrl: String, imageName: String) {
fun openImage(context: Context, imageUrl: String, imageName: String? = "") {
var imageViewer: ImageViewer? = null
val request =
ImageRequestBuilder.newBuilderWithSource(imageUrl.toUri())
......
......@@ -17,7 +17,7 @@ class MessageHelper @Inject constructor(
private val currentServer = serverInteractor.get()!!
private val settings: PublicSettings = getSettingsInteractor.get(currentServer)
fun createPermalink(message: Message, chatRoom: ChatRoom): String {
fun createPermalink(message: Message, chatRoom: ChatRoom, markdownSyntax: Boolean = true): String {
val type = when (chatRoom.type) {
is RoomType.PrivateGroup -> "group"
is RoomType.Channel -> "channel"
......@@ -30,7 +30,8 @@ class MessageHelper @Inject constructor(
} else {
chatRoom.name
}
return "[ ]($currentServer/$type/$name?msg=${message.id}) "
val permalink = "$currentServer/$type/$name?msg=${message.id}"
return if (markdownSyntax) "[ ]($permalink) " else permalink
}
fun messageIdFromPermalink(permalink: String): String? {
......
package chat.rocket.android.main.presentation
import android.content.Context
import chat.rocket.android.R
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.db.DatabaseManagerFactory
import chat.rocket.android.emoji.Emoji
......
......@@ -232,6 +232,15 @@ class MainActivity : AppCompatActivity(), MainView, HasActivityInjector,
toolbar.setNavigationOnClickListener { openDrawer() }
}
fun showLogoutDialog() {
val builder = AlertDialog.Builder(this)
builder.setTitle(R.string.action_logout)
builder.setMessage(R.string.title_confirmation)
builder.setPositiveButton(R.string.action_logout) { _, _ -> presenter.logout()}
.setNegativeButton(R.string.action_stay) { dialog, _ -> dialog.cancel() }
builder.create().show()
}
fun setAvatar(avatarUrl: String) {
headerLayout.image_avatar.setImageURI(avatarUrl)
}
......
......@@ -64,6 +64,6 @@ internal fun MainActivity.onNavDrawerItemSelected(menuItem: MenuItem) {
R.id.menu_action_profile -> presenter.toUserProfile()
R.id.menu_action_settings -> presenter.toSettings()
R.id.menu_action_admin_panel -> presenter.toAdminPanel()
R.id.menu_action_logout -> presenter.logout()
R.id.menu_action_logout -> showLogoutDialog()
}
}
package chat.rocket.android.server.domain
import chat.rocket.android.server.domain.model.BasicAuth
interface BasicAuthRepository {
fun save(basicAuth: BasicAuth)
fun load(): List<BasicAuth>
}
package chat.rocket.android.server.domain
import javax.inject.Inject
class GetBasicAuthInteractor @Inject constructor(val repository: BasicAuthRepository) {
fun getAll() = repository.load().listIterator()
}
package chat.rocket.android.server.domain
import chat.rocket.android.server.domain.model.BasicAuth
import javax.inject.Inject
class SaveBasicAuthInteractor @Inject constructor(val repository: BasicAuthRepository) {
fun save(basicAuth: BasicAuth) = repository.save(basicAuth)
}
package chat.rocket.android.server.domain.model
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class BasicAuth(
val host: String,
val credentials: String
)
......@@ -12,18 +12,10 @@ import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message
import chat.rocket.core.model.Reactions
import chat.rocket.core.model.attachment.Attachment
import chat.rocket.core.model.attachment.AudioAttachment
import chat.rocket.core.model.attachment.AuthorAttachment
import chat.rocket.core.model.attachment.Color
import chat.rocket.core.model.attachment.ColorAttachment
import chat.rocket.core.model.attachment.DEFAULT_COLOR_STR
import chat.rocket.core.model.attachment.Field
import chat.rocket.core.model.attachment.GenericFileAttachment
import chat.rocket.core.model.attachment.ImageAttachment
import chat.rocket.core.model.attachment.MessageAttachment
import chat.rocket.core.model.attachment.VideoAttachment
import chat.rocket.core.model.attachment.actions.Action
import chat.rocket.core.model.attachment.actions.ActionsAttachment
import chat.rocket.core.model.attachment.actions.ButtonAction
import chat.rocket.core.model.messageTypeOf
import chat.rocket.core.model.url.Meta
......@@ -141,44 +133,57 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
val list = mutableListOf<Attachment>()
attachments.forEach { attachment ->
with(attachment) {
when {
imageUrl != null -> {
ImageAttachment(title, description, text, titleLink, titleLinkDownload, imageUrl, type, imageSize)
}
videoUrl != null -> {
VideoAttachment(title, description, text, titleLink, titleLinkDownload, videoUrl, type, videoSize)
}
audioUrl != null -> {
AudioAttachment(title, description, text, titleLink, titleLinkDownload, audioUrl, type, audioSize)
}
titleLink != null -> {
GenericFileAttachment(title, description, text, titleLink, titleLink, titleLinkDownload)
}
text != null && color != null && fallback != null -> {
ColorAttachment(Color.Custom(color), text, fallback)
}
text != null -> {
// TODO how to model message with attachments
MessageAttachment(authorName, authorIcon, text, thumbUrl,
color?.let { Color.Custom(it) }, messageLink, null, timestamp)
}
authorLink != null -> {
mapAuthorAttachment(this)
}
hasFields -> {
mapColorAttachmentWithFields(this)
}
hasActions -> {
mapActionAttachment(this)
}
else -> null
}?.let { list.add(it) }
val fields = if (hasFields) {
withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(attachment._id)
}.map { Field(it.title, it.value) }
} else {
null
}
val actions = if (hasActions) {
withContext(CommonPool) {
dbManager.messageDao().getAttachmentActions(attachment._id)
}.mapNotNull { mapAction(it) }
} else {
null
}
val attachment = Attachment(
title = title,
type = type,
description = description,
authorName = authorName,
text = text,
thumbUrl = thumbUrl,
color = color?.let { Color.Custom(color) },
titleLink = titleLink,
titleLinkDownload = titleLinkDownload,
imageUrl = imageUrl,
imageType = imageType,
imageSize = imageSize,
videoUrl = videoUrl,
videoType = videoType,
videoSize = videoSize,
audioUrl = audioUrl,
audioType = audioType,
audioSize = audioSize,
messageLink = messageLink,
attachments = null, // HOW TO MAP THIS
timestamp = timestamp,
authorIcon = authorIcon,
authorLink = authorLink,
fields = fields,
fallback = fallback,
buttonAlignment = if (actions != null && actions.isNotEmpty()) buttonAlignment ?: "vertical" else null,
actions = actions
)
list.add(attachment)
}
}
return list
}
private suspend fun mapColorAttachmentWithFields(entity: AttachmentEntity): ColorAttachment {
/*private suspend fun mapColorAttachmentWithFields(entity: AttachmentEntity): ColorAttachment {
val fields = withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(entity._id)
}.map { Field(it.title, it.value) }
......@@ -199,7 +204,7 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
// TODO - remove the default "vertical" value from here...
ActionsAttachment(title, actions, buttonAlignment ?: "vertical")
}
}
}*/
private fun mapAction(action: AttachmentActionEntity): Action? {
return when (action.type) {
......@@ -210,12 +215,12 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
}
}
private suspend fun mapAuthorAttachment(attachment: AttachmentEntity): AuthorAttachment {
/*private suspend fun mapAuthorAttachment(attachment: AttachmentEntity): AuthorAttachment {
val fields = withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(attachment._id)
}.map { Field(it.title, it.value) }
return with(attachment) {
AuthorAttachment(authorLink!!, authorIcon, authorName, fields)
}
}
}*/
}
\ No newline at end of file
package chat.rocket.android.server.infraestructure
import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.Operation
import chat.rocket.android.db.model.MessagesSync
import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.core.model.Message
......@@ -61,9 +62,7 @@ class DatabaseMessagesRepository(
}
override suspend fun saveLastSyncDate(roomId: String, timeMillis: Long) {
withContext(dbManager.dbContext) {
dbManager.messageDao().saveLastSync(MessagesSync(roomId, timeMillis))
}
dbManager.sendOperation(Operation.SaveLastSync(MessagesSync(roomId, timeMillis)))
}
override suspend fun getLastSyncDate(roomId: String): Long? = withContext(CommonPool) {
......
package chat.rocket.android.server.infraestructure
import android.content.SharedPreferences
import androidx.core.content.edit
import chat.rocket.android.server.domain.BasicAuthRepository
import chat.rocket.android.server.domain.model.BasicAuth
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
private const val BASICAUTHS_KEY = "BASICAUTHS_KEY"
class SharedPrefsBasicAuthRepository(
private val preferences: SharedPreferences,
private val moshi: Moshi
) : BasicAuthRepository {
override fun save(basicAuth: BasicAuth) {
val newList = load().filter { basicAuth -> basicAuth.host != basicAuth.host }
.toMutableList()
newList.add(0, basicAuth)
save(newList)
}
override fun load(): List<BasicAuth> {
val json = preferences.getString(BASICAUTHS_KEY, "[]")
val type = Types.newParameterizedType(List::class.java, BasicAuth::class.java)
val adapter = moshi.adapter<List<BasicAuth>>(type)
return adapter.fromJson(json) ?: emptyList()
}
private fun save(basicAuths: List<BasicAuth>) {
val type = Types.newParameterizedType(List::class.java, BasicAuth::class.java)
val adapter = moshi.adapter<List<BasicAuth>>(type)
preferences.edit {
putString(BASICAUTHS_KEY, adapter.toJson(basicAuths))
}
}
}
......@@ -61,6 +61,9 @@ abstract class CheckServerPresenter constructor(
internal var wordpressOauthUrl: String? = null
internal var casLoginUrl: String? = null
internal var casToken: String? = null
internal var casServiceName: String? = null
internal var casServiceNameTextColor: Int = 0
internal var casServiceButtonColor: Int = 0
internal var customOauthUrl: String? = null
internal var customOauthServiceName: String? = null
internal var customOauthServiceNameTextColor: Int = 0
......@@ -79,6 +82,31 @@ abstract class CheckServerPresenter constructor(
settings = it
}
client = factory.create(serverUrl)
state = ""
facebookOauthUrl = null
githubOauthUrl = null
googleOauthUrl = null
linkedinOauthUrl = null
gitlabOauthUrl = null
wordpressOauthUrl = null
casLoginUrl = null
casToken = null
casServiceName = null
casServiceNameTextColor = 0
casServiceButtonColor = 0
customOauthUrl = null
customOauthServiceName = null
customOauthServiceNameTextColor = 0
customOauthServiceButtonColor= 0
samlUrl = null
samlToken = null
samlServiceName = null
samlServiceNameTextColor = 0
samlServiceButtonColor = 0
totalSocialAccountsEnabled = 0
isLoginFormEnabled = false
isNewAccountCreationEnabled = false
}
internal fun checkServerInfo(serverUrl: String): Job {
......@@ -125,7 +153,7 @@ abstract class CheckServerPresenter constructor(
if (services.isNotEmpty()) {
state = OauthHelper.getState()
checkEnabledOauthAccounts(services, serverUrl)
checkEnabledCasAccounts(serverUrl)
checkEnabledCasAccounts(services, serverUrl)
checkEnabledCustomOauthAccounts(services, serverUrl)
checkEnabledSamlAccounts(services, serverUrl)
}
......@@ -227,11 +255,25 @@ abstract class CheckServerPresenter constructor(
}
}
private fun checkEnabledCasAccounts(serverUrl: String) {
private fun checkEnabledCasAccounts(services: List<Map<String,Any>>, serverUrl: String) {
if (settings.isCasAuthenticationEnabled()) {
casToken = generateRandomString(17)
casLoginUrl = settings.casLoginUrl().casUrl(serverUrl, casToken.toString())
totalSocialAccountsEnabled++
getCasServices(services).let {
for (serviceMap in it) {
casServiceName = getServiceName(serviceMap)
val serviceNameTextColor = getServiceNameColor(serviceMap)
val serviceButtonColor = getServiceButtonColor(serviceMap)
if (casServiceName != null &&
serviceNameTextColor != null &&
serviceButtonColor != null
) {
casServiceNameTextColor = serviceNameTextColor
casServiceButtonColor = serviceButtonColor
totalSocialAccountsEnabled++
}
}
}
}
}
......@@ -244,8 +286,9 @@ abstract class CheckServerPresenter constructor(
val clientId = getOauthClientId(serviceMap)
val scope = getCustomOauthScope(serviceMap)
val serviceNameTextColor =
getServiceNameColorForCustomOauthOrSaml(serviceMap)
getServiceNameColor(serviceMap)
val serviceButtonColor = getServiceButtonColor(serviceMap)
if (customOauthServiceName != null &&
host != null &&
authorizePath != null &&
......@@ -276,9 +319,9 @@ abstract class CheckServerPresenter constructor(
samlToken = generateRandomString(17)
for (serviceMap in it) {
val provider = getSamlProvider(serviceMap)
samlServiceName = getSamlServiceName(serviceMap)
samlServiceName = getServiceName(serviceMap)
val serviceNameTextColor =
getServiceNameColorForCustomOauthOrSaml(serviceMap)
getServiceNameColor(serviceMap)
val serviceButtonColor = getServiceButtonColor(serviceMap)
if (provider != null &&
......@@ -369,6 +412,14 @@ abstract class CheckServerPresenter constructor(
private fun getCustomOauthServiceName(serviceMap: Map<String, Any>): String? =
serviceMap["service"] as? String
/**
* Returns a CAS service list.
*
* @return A CAS service list, otherwise an empty list if there is no CAS service.
*/
private fun getCasServices(listMap: List<Map<String, Any>>): List<Map<String, Any>> =
listMap.filter { map -> map["service"] == "cas" }
/**
* Returns a SAML OAuth service list.
*
......@@ -388,26 +439,27 @@ abstract class CheckServerPresenter constructor(
/**
* Returns the text of the SAML service.
* REMARK: This can be used SAML or CAS.
*
* @param serviceMap The service map to get the text of the SAML service.
* @return The text of the SAML service, otherwise null.
*/
private fun getSamlServiceName(serviceMap: Map<String, Any>): String? =
private fun getServiceName(serviceMap: Map<String, Any>): String? =
serviceMap["buttonLabelText"] as? String
/**
* Returns the text color of the service name.
* REMARK: This can be used for custom OAuth or SAML.
* REMARK: This can be used for custom OAuth, SAML or CAS.
*
* @param serviceMap The service map to get the text color from.
* @return The text color of the service (custom OAuth or SAML), otherwise null.
*/
private fun getServiceNameColorForCustomOauthOrSaml(serviceMap: Map<String, Any>): Int? =
private fun getServiceNameColor(serviceMap: Map<String, Any>): Int? =
(serviceMap["buttonLabelColor"] as? String)?.parseColor()
/**
* Returns the button color of the service name.
* REMARK: This can be used for custom OAuth or SAML.
* REMARK: This can be used for custom OAuth, SAML or CAS.
*
* @param serviceMap The service map to get the button color from.
* @return The button color of the service (custom OAuth or SAML), otherwise null.
......
package chat.rocket.android.util
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.Credentials
import java.io.IOException
import chat.rocket.android.server.domain.model.BasicAuth
import chat.rocket.android.server.domain.GetBasicAuthInteractor
import chat.rocket.android.server.domain.SaveBasicAuthInteractor
import javax.inject.Inject
/**
* An OkHttp interceptor which adds Authorization header based on URI userInfo
* part. Can be applied as an
* [application interceptor][OkHttpClient.interceptors]
* or as a [ ][OkHttpClient.networkInterceptors].
*/
class BasicAuthenticatorInterceptor @Inject constructor (
private val getBasicAuthInteractor: GetBasicAuthInteractor,
private val saveBasicAuthInteractor: SaveBasicAuthInteractor
): Interceptor {
private val credentials = HashMap<String, String>()
init {
val basicAuths = getBasicAuthInteractor.getAll()
for (basicAuth in basicAuths){
credentials[basicAuth.host] = basicAuth.credentials
}
}
private fun saveCredentials(host: String, basicCredentials: String) {
saveBasicAuthInteractor.save(
BasicAuth(
host,
basicCredentials
)
)
credentials[host] = basicCredentials
}
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val url = request.url()
val host = url.host()
val username = url.username()
if (!username.isNullOrEmpty()) {
saveCredentials(host, Credentials.basic(username, url.password()))
request = request.newBuilder().url(
url.newBuilder().username("").password("").build()
).build()
}
credentials[host]?.let {
request = request.newBuilder().header("Authorization", it).build()
}
return chain.proceed(request)
}
}
package chat.rocket.android.util.extensions
inline fun CharSequence?.isNotNullNorEmpty(block: (CharSequence) -> Unit) {
inline fun CharSequence?.ifNotNullNorEmpty(block: (CharSequence) -> Unit) {
if (this != null && this.isNotEmpty()) {
block(this)
}
}
\ No newline at end of file
}
fun CharSequence?.isNotNullNorEmpty(): Boolean = this != null && this.isNotEmpty()
\ No newline at end of file
......@@ -74,4 +74,6 @@ fun String.lowercaseUrl(): String? {
val newScheme = httpUrl?.scheme()?.toLowerCase()
return httpUrl?.newBuilder()?.scheme(newScheme)?.build()?.toString()
}
\ No newline at end of file
}
fun String?.isNotNullNorEmpty(): Boolean = this != null && this.isNotEmpty()
\ No newline at end of file
package chat.rocket.android.util.extensions
val <T> T.exhaustive: T
get() = this
\ No newline at end of file
......@@ -4,10 +4,12 @@ import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.res.ResourcesCompat
import android.view.View
import androidx.core.view.isVisible
import chat.rocket.android.R
import timber.log.Timber
fun View.openTabbedUrl(url: String) {
fun View.openTabbedUrl(url: String?) {
if (url == null) return
with(this) {
val uri = url.ensureScheme()
val tabsbuilder = CustomTabsIntent.Builder()
......@@ -30,4 +32,27 @@ private fun String.ensureScheme(): Uri? {
}
return Uri.parse(url.lowercaseUrl())
}
inline var List<View>.isVisible: Boolean
get() {
var visible = true
forEach { view ->
if (!view.isVisible) {
visible = false
}
}
return visible
}
set(value) {
forEach { view ->
view.isVisible = value
}
}
fun List<View>.setOnClickListener(listener: (v: View) -> Unit) {
forEach { view ->
view.setOnClickListener(listener)
}
}
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@color/actionMenuColor"
android:pathData="M9.548,14.23l-2.651,2.652a2.676,2.676 0,0 1,-3.78 0,2.676 2.676,0 0,1 0,-3.78L6.91,9.311a2.677,2.677 0,0 1,3.781 0,0.669 0.669,0 0,0 0.945,-0.946 4.015,4.015 0,0 0,-5.67 0l-3.792,3.792a4.014,4.014 0,0 0,0 5.67,4.014 4.014,0 0,0 5.67,0l2.65,-2.65a0.669,0.669 0,0 0,-0.945 -0.947zM17.828,2.173a4.014,4.014 0,0 0,-5.67 0L9.506,4.824a0.668,0.668 0,1 0,0.946 0.945l2.651,-2.651a2.676,2.676 0,0 1,3.78 0,2.676 2.676,0 0,1 0,3.78L13.09,10.69a2.678,2.678 0,0 1,-3.781 0,0.668 0.668,0 1,0 -0.945,0.945 4.015,4.015 0,0 0,5.67 0l3.793,-3.792a4.014,4.014 0,0 0,0 -5.67z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="1055.0303"
android:viewportWidth="1055.0303">
android:viewportWidth="247.7922"
android:viewportHeight="247.7922">
<group
android:translateX="281.28394"
android:translateY="271.51514">
android:translateX="69.8961"
android:translateY="69.8961">
<path
android:fillColor="#FFDB2323"
android:pathData="M491.3,255.3c0,-24.1 -7.2,-47.2 -21.4,-68.7c-12.8,-19.3 -30.7,-36.4 -53.2,-50.7c-43.5,-27.8 -100.6,-43.1 -160.9,-43.1c-20.1,0 -40,1.7 -59.2,5.1c-11.9,-11.2 -25.9,-21.2 -40.7,-29.2c-79,-38.3 -144.6,-0.9 -144.6,-0.9s60.9,50.1 51,93.9c-27.3,27 -42,59.6 -42,93.6c0,0.1 0,0.2 0,0.3c0,0.1 0,0.2 0,0.3c0,33.9 14.8,66.6 42,93.6c9.9,43.9 -51,93.9 -51,93.9s65.5,37.4 144.6,-0.9c14.8,-8 28.8,-18 40.7,-29.2c19.2,3.4 39.1,5.1 59.2,5.1c60.3,0 117.4,-15.3 160.9,-43.1c22.5,-14.4 40.4,-31.5 53.2,-50.7c14.2,-21.5 21.4,-44.6 21.4,-68.7c0,-0.1 0,-0.2 0,-0.3C491.3,255.6 491.3,255.4 491.3,255.3z" />
android:fillType="evenOdd"
android:pathData="M93.921,43.28L93.922,43.283C93.922,43.282 93.922,43.282 93.922,43.282C93.922,43.281 93.921,43.281 93.921,43.28ZM32.924,10.95C36.19,12.769 39.278,15.071 41.914,17.629C46.165,16.857 50.547,16.468 54.993,16.468C68.302,16.468 80.921,19.969 90.522,26.325C95.494,29.618 99.445,33.525 102.266,37.939C105.408,42.857 107,48.145 107,53.812C107,59.327 105.408,64.618 102.266,69.535C99.445,73.951 95.494,77.856 90.522,81.149C80.921,87.505 68.303,91.004 54.993,91.004C50.547,91.004 46.166,90.615 41.914,89.844C39.277,92.401 36.19,94.704 32.924,96.523C15.472,105.288 1,96.729 1,96.729C1,96.729 14.455,85.274 12.267,75.231C6.247,69.043 2.985,61.58 2.985,53.662C2.985,45.893 6.248,38.43 12.267,32.241C14.455,22.201 1.004,10.748 1,10.744C1.004,10.741 15.475,2.186 32.924,10.95Z"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="54"
android:endY="89.495674"
android:startX="54"
android:startY="-4.593772"
android:type="linear">
<item
android:color="#FFDB2323"
android:offset="0" />
<item
android:color="#FFDB2323"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:pathData="M255.9,124.2c113.9,0 206.3,59 206.3,131.8c0,72.8 -92.4,131.8 -206.3,131.8c-25.4,0 -49.7,-2.9 -72.1,-8.3c-22.8,27.4 -73,65.6 -121.7,53.3c15.9,-17 39.4,-45.8 34.3,-93.2c-29.2,-22.7 -46.8,-51.8 -46.8,-83.5C49.6,183.2 142,124.2 255.9,124.2" />
android:fillType="nonZero"
android:pathData="M22.066,71.46C16.128,66.689 12.564,60.582 12.564,53.926C12.564,38.654 31.331,26.273 54.482,26.273C77.633,26.273 96.4,38.654 96.4,53.926C96.4,69.199 77.633,81.58 54.482,81.58C48.776,81.58 43.337,80.828 38.379,79.466L34.754,83.031C32.785,84.968 30.476,86.721 28.07,88.102C24.881,89.699 21.731,90.57 18.615,90.836C18.791,90.51 18.953,90.18 19.127,89.854C22.758,83.031 23.738,76.9 22.066,71.46Z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
<path
android:fillColor="#FFDB2323"
android:pathData="M255.9,256m-27.4,0a27.4,27.4 0,1 1,54.8 0a27.4,27.4 0,1 1,-54.8 0" />
android:fillType="nonZero"
android:pathData="M35.209,53.736m-6.264,0a6.264,6.264 0,1 1,12.527 0a6.264,6.264 0,1 1,-12.527 0"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="35.2085"
android:endY="58.20792"
android:startX="35.2085"
android:startY="46.44125"
android:type="linear">
<item
android:color="#FFDB2323"
android:offset="0" />
<item
android:color="#FFDB2323"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFDB2323"
android:pathData="M351.2,256m-27.4,0a27.4,27.4 0,1 1,54.8 0a27.4,27.4 0,1 1,-54.8 0" />
android:fillType="nonZero"
android:pathData="M54.482,53.736m-6.264,0a6.264,6.264 0,1 1,12.527 0a6.264,6.264 0,1 1,-12.527 0"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="54.4815"
android:endY="58.20792"
android:startX="54.4815"
android:startY="46.44125"
android:type="linear">
<item
android:color="#FFDB2323"
android:offset="0" />
<item
android:color="#FFDB2323"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFDB2323"
android:pathData="M160.6,256m-27.4,0a27.4,27.4 0,1 1,54.8 0a27.4,27.4 0,1 1,-54.8 0" />
android:fillType="nonZero"
android:pathData="M75.682,53.736m-6.264,0a6.264,6.264 0,1 1,12.527 0a6.264,6.264 0,1 1,-12.527 0"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="75.6815"
android:endY="58.20792"
android:startX="75.6815"
android:startY="46.44125"
android:type="linear">
<item
android:color="#FFDB2323"
android:offset="0" />
<item
android:color="#FFDB2323"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
</group>
</vector>
\ No newline at end of file
......@@ -133,15 +133,6 @@
android:textSize="16sp"
android:visibility="gone"
tools:visibility="visible" />
<Button
android:id="@+id/button_cas"
style="@style/Authentication.Button"
android:layout_marginTop="10dp"
android:clickable="false"
android:text="@string/action_login_or_sign_up"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
......
This diff is collapsed.
This diff is collapsed.
......@@ -2,4 +2,4 @@
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view_reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
\ No newline at end of file
android:layout_height="wrap_content" />
......@@ -12,6 +12,11 @@
android:icon="@drawable/ic_action_message_reply_24dp"
android:title="@string/action_msg_reply" />
<item
android:id="@+id/action_message_permalink"
android:icon="@drawable/ic_action_message_link_24dp"
android:title="@string/action_msg_copy_permalink" />
<item
android:id="@+id/action_message_quote"
android:icon="@drawable/ic_action_message_quote_24dp"
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -59,4 +59,6 @@
<!-- Default Background Color -->
<color name="default_background">#FAFAFA</color>
<color name="reaction_text">#868585</color>
</resources>
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