Lots of progress on reworked conversations

This commit is contained in:
Mario Danic 2020-01-03 22:58:01 +01:00
parent af04c95bab
commit 9d6feca3f9
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
16 changed files with 380 additions and 368 deletions

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
<module fileurl="file://$PROJECT_DIR$/talk-android.iml" filepath="$PROJECT_DIR$/talk-android.iml" />
</modules>
</component>
</project>

View File

@ -19,7 +19,7 @@
*/ */
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'findbugs' //apply plugin: 'findbugs'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
@ -82,9 +82,26 @@ android {
} }
} }
dataBinding { /*buildFeatures {
enabled = true // Determines whether to enable support for Jetpack Compose.
} compose = false
// Determines whether to generate a BuildConfig class.
buildConfig = true
// Determines whether to support View Binding.
// Note that the viewBinding.enabled property is now deprecated.
viewBinding = true
// Determines whether to support Data Binding.
// Note that the dataBinding.enabled property is now deprecated.
dataBinding = true
// Determines whether to generate binder classes for your AIDL files.
aidl = true
// Determines whether to support RenderScript.
renderScript = true
// Determines whether to support injecting custom variables into the modules R class.
resValues = true
// Determines whether to support shader AOT compilation.
shaders = true
}*/
androidExtensions { androidExtensions {
experimental = true experimental = true
@ -134,27 +151,6 @@ android {
htmlOutput file("$project.buildDir/reports/lint/lint.html") htmlOutput file("$project.buildDir/reports/lint/lint.html")
disable 'MissingTranslation' disable 'MissingTranslation'
} }
task findbugs(type: FindBugs) {
ignoreFailures = false
effort = "max"
reportLevel = "medium"
classes = fileTree("$project.buildDir/intermediates/javac/gplayDebug/classes/com/nextcloud")
excludeFilter = file("${project.rootDir}/findbugs-filter.xml")
source = fileTree('src/main/java')
pluginClasspath = project.configurations.findbugsPlugins
classpath = files()
include '**/*.java'
exclude '**/gen/**'
reports {
xml.enabled = false
html.enabled = true
html {
destination = file("$project.buildDir/reports/findbugs/findbugs.html")
}
}
}
} }
ext { ext {
@ -242,7 +238,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.biometric:biometric:1.0.0' implementation 'androidx.biometric:biometric:1.0.1'
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
@ -278,7 +274,7 @@ dependencies {
kapt 'io.requery:requery-processor:1.5.1' kapt 'io.requery:requery-processor:1.5.1'
implementation 'net.orange-box.storebox:storebox-lib:1.4.0' implementation 'net.orange-box.storebox:storebox-lib:1.4.0'
compileOnly 'org.projectlombok:lombok:1.18.10' compileOnly 'org.projectlombok:lombok:1.18.10'
annotationProcessor 'org.projectlombok:lombok:1.18.10' annotationProcessor "org.projectlombok:lombok:1.18.10"
kapt "org.projectlombok:lombok:1.18.10" kapt "org.projectlombok:lombok:1.18.10"
implementation 'com.jakewharton:butterknife:10.2.0' implementation 'com.jakewharton:butterknife:10.2.0'
@ -287,6 +283,7 @@ dependencies {
implementation 'eu.davidea:flexible-adapter:5.1.0' implementation 'eu.davidea:flexible-adapter:5.1.0'
implementation 'eu.davidea:flexible-adapter-ui:1.0.0' implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
implementation 'eu.davidea:flexible-adapter-livedata:1.0.0-b3' implementation 'eu.davidea:flexible-adapter-livedata:1.0.0-b3'
implementation 'com.otaliastudios:elements:0.3.7'
implementation 'org.webrtc:google-webrtc:1.0.23295' implementation 'org.webrtc:google-webrtc:1.0.23295'
implementation 'com.yarolegovich:lovely-dialog:1.1.0' implementation 'com.yarolegovich:lovely-dialog:1.1.0'
implementation 'com.yarolegovich:lovelyinput:1.0.9' implementation 'com.yarolegovich:lovelyinput:1.0.9'
@ -327,9 +324,6 @@ dependencies {
androidTestImplementation('androidx.test.espresso:espresso-core:3.3.0-alpha02', { androidTestImplementation('androidx.test.espresso:espresso-core:3.3.0-alpha02', {
exclude group: 'com.android.support', module: 'support-annotations' exclude group: 'com.android.support', module: 'support-annotations'
}) })
findbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.10.0'
findbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.4.7'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0'
implementation 'com.github.Kennyc1012:BottomSheet:2.4.1' implementation 'com.github.Kennyc1012:BottomSheet:2.4.1'
implementation 'com.google.firebase:firebase-messaging:20.1.0'
} }

View File

@ -19,6 +19,6 @@
*/ */
dependencies { dependencies {
implementation "androidx.work:work-gcm:2.3.0-beta01" implementation "androidx.work:work-gcm:2.3.0-beta02"
implementation "com.google.firebase:firebase-messaging:20.1.0" implementation "com.google.firebase:firebase-messaging:20.1.0"
} }

View File

@ -0,0 +1,172 @@
/*
*
* * Nextcloud Talk application
* *
* * @author Mario Danic
* * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.nextcloud.talk.adapters
import android.content.Context
import android.graphics.drawable.Drawable
import android.text.TextUtils
import android.text.format.DateUtils
import android.view.View
import android.view.ViewGroup
import coil.api.load
import coil.transform.CircleCropTransformation
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.newarch.services.GlobalService
import com.nextcloud.talk.newarch.utils.Images
import com.nextcloud.talk.utils.ApiUtils
import com.otaliastudios.elements.Element
import com.otaliastudios.elements.Page
import com.otaliastudios.elements.Presenter
import kotlinx.android.synthetic.main.rv_item_conversation_with_last_message.view.*
import org.koin.core.KoinComponent
import org.koin.core.inject
open class ConversationsPresenter(context: Context, onElementClick: ((Page, Holder, Element<Conversation>) -> Unit)?) : Presenter<Conversation>(context, onElementClick), KoinComponent {
private val globalService: GlobalService by inject()
override val elementTypes: Collection<Int>
get() = listOf(0)
override fun onCreate(parent: ViewGroup, elementType: Int): Holder {
return Holder(getLayoutInflater().inflate(R.layout.rv_item_conversation_with_last_message, parent, false))
}
override fun onBind(page: Page, holder: Holder, element: Element<Conversation>, payloads: List<Any>) {
super.onBind(page, holder, element, payloads)
val conversation = element.data
val user = globalService.currentUserLiveData.value
user?.let { user ->
conversation?.let { conversation ->
val appContext = NextcloudTalkApplication.sharedApplication!!.applicationContext
if (conversation.changing) {
holder.itemView.actionProgressBar!!.visibility = View.VISIBLE
} else {
holder.itemView.actionProgressBar!!.visibility = View.GONE
}
holder.itemView.dialogName!!.text = conversation.displayName
if (conversation.unreadMessages > 0) {
holder.itemView.dialogUnreadBubble!!.visibility = View.VISIBLE
if (conversation.unreadMessages < 100) {
holder.itemView.dialogUnreadBubble!!.text = conversation.unreadMessages.toLong()
.toString()
} else {
holder.itemView.dialogUnreadBubble!!.text = context.getString(R.string.nc_99_plus)
}
if (conversation.unreadMention) {
holder.itemView.dialogUnreadBubble!!.background =
context.getDrawable(R.drawable.bubble_circle_unread_mention)
} else {
holder.itemView.dialogUnreadBubble!!.background =
context.getDrawable(R.drawable.bubble_circle_unread)
}
} else {
holder.itemView.dialogUnreadBubble!!.visibility = View.GONE
}
if (conversation.hasPassword) {
holder.itemView.passwordProtectedRoomImageView!!.visibility = View.VISIBLE
} else {
holder.itemView.passwordProtectedRoomImageView!!.visibility = View.GONE
}
if (conversation.favorite) {
holder.itemView.favoriteConversationImageView!!.visibility = View.VISIBLE
} else {
holder.itemView.favoriteConversationImageView!!.visibility = View.GONE
}
if (conversation.lastMessage != null) {
holder.itemView.dialogDate!!.visibility = View.VISIBLE
holder.itemView.dialogDate!!.text = DateUtils.getRelativeTimeSpanString(
conversation.lastActivity * 1000L,
System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE
)
if (!TextUtils.isEmpty(
conversation.lastMessage!!.systemMessage
) || Conversation.ConversationType.SYSTEM_CONVERSATION == conversation.type
) {
holder.itemView.dialogLastMessage!!.text = conversation.lastMessage!!.text
} else {
var authorDisplayName = ""
conversation.lastMessage!!.activeUser = user
val text: String
if (conversation.lastMessage!!
.messageType == ChatMessage.MessageType.REGULAR_TEXT_MESSAGE && (!(Conversation.ConversationType.ONE_TO_ONE_CONVERSATION).equals(
conversation.type) || conversation.lastMessage!!.actorId == user.userId)
) {
if (conversation.lastMessage!!.actorId == user.userId) {
text = String.format(
appContext.getString(R.string.nc_formatted_message_you),
conversation.lastMessage!!.lastMessageDisplayText
)
} else {
authorDisplayName = if (!TextUtils.isEmpty(conversation.lastMessage!!.actorDisplayName))
conversation.lastMessage!!.actorDisplayName
else if ("guests" == conversation.lastMessage!!.actorType)
appContext.getString(R.string.nc_guest)
else
""
text = String.format(
appContext.getString(R.string.nc_formatted_message),
authorDisplayName,
conversation.lastMessage!!.lastMessageDisplayText
)
}
} else {
text = conversation.lastMessage!!.lastMessageDisplayText
}
holder.itemView.dialogLastMessage.text = text
}
} else {
holder.itemView.dialogDate.visibility = View.GONE
holder.itemView.dialogLastMessage.setText(R.string.nc_no_messages_yet)
}
val conversationDrawable: Drawable? = Images().getImageForConversation(context, conversation)
conversationDrawable?.let {
holder.itemView.dialogAvatar.load(conversationDrawable)
}?: run {
holder.itemView.dialogAvatar.load(ApiUtils.getUrlForAvatarWithName(
user.baseUrl,
conversation.name, R.dimen.avatar_size))
{
addHeader("Authorization", user.getCredentials())
transformations(CircleCropTransformation())
}
}
}
}
}
}

View File

@ -0,0 +1,30 @@
/*
*
* * Nextcloud Talk application
* *
* * @author Mario Danic
* * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.nextcloud.talk.adapters
import androidx.lifecycle.LiveData
import com.nextcloud.talk.models.json.conversations.Conversation
import com.otaliastudios.elements.extensions.LiveDataSource
class ConversationsSource(data: LiveData<List<Conversation>>, elementType: Int) : LiveDataSource<Conversation>(data, elementType) {
}

View File

@ -24,7 +24,9 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.TextUtils import android.text.TextUtils
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.Log
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView
import coil.api.load import coil.api.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import com.nextcloud.talk.R import com.nextcloud.talk.R
@ -212,9 +214,18 @@ class ConversationItem(
addHeader("Authorization", user.getCredentials()) addHeader("Authorization", user.getCredentials())
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
}
} }
override fun onViewAttached(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?, holder: ConversationItemViewHolder?, position: Int) {
super.onViewAttached(adapter, holder, position)
Log.d("MAriO", model.displayName!!)
}
override fun onViewDetached(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>?, holder: ConversationItemViewHolder?, position: Int) {
super.onViewDetached(adapter, holder, position)
Log.d("MAriO DETACH", model.displayName!!)
} }
override fun filter(constraint: String): Boolean { override fun filter(constraint: String): Boolean {

View File

@ -54,7 +54,7 @@ public class MentionAutocompleteCallback implements AutocompleteCallback<Mention
if (range == null) return false; if (range == null) return false;
int start = range[0]; int start = range[0];
int end = range[1]; int end = range[1];
String replacement = item.getLabel(); String replacement = item.label;
StringBuilder replacementStringBuilder = new StringBuilder(item.getLabel()); StringBuilder replacementStringBuilder = new StringBuilder(item.getLabel());
for (EmojiRange emojiRange : EmojiUtils.emojis(replacement)) { for (EmojiRange emojiRange : EmojiUtils.emojis(replacement)) {

View File

@ -47,7 +47,7 @@ import java.util.*
abstract class BaseController : ButterKnifeController(), ComponentCallbacks { abstract class BaseController : ButterKnifeController(), ComponentCallbacks {
val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this) open val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this)
val appPreferences: AppPreferences by inject() val appPreferences: AppPreferences by inject()
val context: Context by inject() val context: Context by inject()

View File

@ -20,63 +20,44 @@
package com.nextcloud.talk.newarch.features.conversationsList package com.nextcloud.talk.newarch.features.conversationsList
import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.view.* import android.view.*
import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener import androidx.lifecycle.observe
import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import butterknife.OnClick import butterknife.OnClick
import com.afollestad.materialdialogs.LayoutMode.WRAP_CONTENT
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.R.drawable import com.nextcloud.talk.R.drawable
import com.nextcloud.talk.adapters.items.ConversationItem import com.nextcloud.talk.adapters.ConversationsPresenter
import com.nextcloud.talk.controllers.ContactsController import com.nextcloud.talk.controllers.ContactsController
import com.nextcloud.talk.controllers.SettingsController import com.nextcloud.talk.controllers.SettingsController
import com.nextcloud.talk.controllers.bottomsheet.items.BasicListItemWithImage import com.nextcloud.talk.controllers.bottomsheet.items.BasicListItemWithImage
import com.nextcloud.talk.controllers.bottomsheet.items.listItemsWithImage
import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseView import com.nextcloud.talk.newarch.conversationsList.mvp.BaseView
import com.nextcloud.talk.newarch.features.conversationsList.ConversationsListViewNetworkState.*
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
import com.nextcloud.talk.utils.ConductorRemapping import com.nextcloud.talk.utils.ConductorRemapping
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.ShareUtils
import com.nextcloud.talk.utils.animations.SharedElementTransition import com.nextcloud.talk.utils.animations.SharedElementTransition
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import eu.davidea.flexibleadapter.FlexibleAdapter import com.otaliastudios.elements.*
import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemClickListener import com.uber.autodispose.lifecycle.LifecycleScopeProvider
import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemLongClickListener
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.IFlexible
import kotlinx.android.synthetic.main.controller_conversations_rv.view.* import kotlinx.android.synthetic.main.controller_conversations_rv.view.*
import kotlinx.android.synthetic.main.fast_scroller.view.* import kotlinx.android.synthetic.main.message_state.view.*
import kotlinx.android.synthetic.main.view_states.view.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.parceler.Parcels import org.parceler.Parcels
import java.util.* import java.util.*
class ConversationsListView : BaseView(), OnQueryTextListener, class ConversationsListView : BaseView() {
OnItemClickListener, OnItemLongClickListener {
override val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this)
private lateinit var viewModel: ConversationsListViewModel private lateinit var viewModel: ConversationsListViewModel
val factory: ConversationListViewModelFactory by inject() val factory: ConversationListViewModelFactory by inject()
private val recyclerViewAdapter = FlexibleAdapter(mutableListOf(), this, true)
private var searchItem: MenuItem? = null private var searchItem: MenuItem? = null
private var settingsItem: MenuItem? = null private var settingsItem: MenuItem? = null
private var searchView: SearchView? = null private var searchView: SearchView? = null
@ -88,19 +69,11 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_conversation_plus_filter, menu) inflater.inflate(R.menu.menu_conversation_plus_filter, menu)
searchItem = menu.findItem(R.id.action_search) searchItem = menu.findItem(R.id.action_search)
initSearchView()
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
if (recyclerViewAdapter.hasFilter()) {
searchItem?.expandActionView()
searchView?.setQuery(viewModel.searchQuery.value, false)
recyclerViewAdapter.filterItems()
}
settingsItem = menu.findItem(R.id.action_settings) settingsItem = menu.findItem(R.id.action_settings)
searchItem?.isVisible = searchItem?.isVisible == false && !recyclerViewAdapter.isEmpty
viewModel.loadAvatar() viewModel.loadAvatar()
} }
@ -128,7 +101,7 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
} }
} }
private fun initSearchView() { /*private fun initSearchView() {
val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchView = MenuItemCompat.getActionView(searchItem) as SearchView searchView = MenuItemCompat.getActionView(searchItem) as SearchView
searchView!!.maxWidth = Integer.MAX_VALUE searchView!!.maxWidth = Integer.MAX_VALUE
@ -154,7 +127,7 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
return onQueryTextSubmit(newText) return onQueryTextSubmit(newText)
} }*/
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -164,114 +137,58 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
actionBar?.show() actionBar?.show()
viewModel = viewModelProvider(factory).get(ConversationsListViewModel::class.java) viewModel = viewModelProvider(factory).get(ConversationsListViewModel::class.java)
val view = super.onCreateView(inflater, container) val view = super.onCreateView(inflater, container)
val adapter = Adapter.builder(this)
.addSource(Source.fromLiveData(viewModel.conversationsLiveData))
.addPresenter(ConversationsPresenter(context, ::onElementClick))
.addPresenter(Presenter.forLoadingIndicator(context, R.layout.loading_state))
.addPresenter(Presenter.forEmptyIndicator(context, R.layout.message_state))
.addPresenter(Presenter.forErrorIndicator(context, R.layout.message_state) { view, throwable ->
view.messageStateTextView.setText(R.string.nc_oops)
view.messageStateImageView.setImageDrawable(context.getDrawable(drawable.ic_announcement_white_24dp))
})
.into(view.recyclerView)
view.recyclerView.initRecyclerView(SmoothScrollLinearLayoutManager(activity), adapter, false)
view.apply { view.apply {
recyclerView.initRecyclerView( recyclerView.initRecyclerView(SmoothScrollLinearLayoutManager(activity), adapter, false)
SmoothScrollLinearLayoutManager(activity), recyclerViewAdapter, false)
recyclerViewAdapter.fastScroller = fast_scroller
swipeRefreshLayoutView.setOnRefreshListener { swipeRefreshLayoutView.setOnRefreshListener {
view.swipeRefreshLayoutView.isRefreshing = false view.swipeRefreshLayoutView.isRefreshing = false
viewModel.loadConversations() viewModel.loadConversations()
} }
swipeRefreshLayoutView.setColorSchemeResources(R.color.colorPrimary) swipeRefreshLayoutView.setColorSchemeResources(R.color.colorPrimary)
fast_scroller.setBubbleTextCreator { position ->
var displayName =
(recyclerViewAdapter.getItem(position) as ConversationItem).model.displayName
if (displayName!!.length > 8) {
displayName = displayName.substring(0, 4) + "..."
} }
displayName viewModel.avatar.observe(this@ConversationsListView) { avatar ->
} settingsItem?.icon = avatar
}
viewModel.apply {
currentUserAvatar.observe(this@ConversationsListView, Observer { value ->
settingsItem?.icon = value
})
conversationsLiveData.observe(this@ConversationsListView, Observer {
val isListEmpty = it.isNullOrEmpty()
if (isListEmpty) {
view.stateWithMessageView?.errorStateTextView?.text =
resources?.getText(R.string.nc_conversations_empty)
view.stateWithMessageView?.errorStateImageView?.setImageResource(drawable.ic_logo)
}
view.stateWithMessageView?.visibility = if (isListEmpty && networkStateLiveData.value != LOADING) View.VISIBLE else View.GONE
if (view.floatingActionButton?.isShown == false) {
view.floatingActionButton?.show()
}
searchItem?.isVisible = searchItem?.isVisible == false && !isListEmpty
val newConversations = mutableListOf<ConversationItem>()
for (conversation in it) {
newConversations.add(
ConversationItem(
conversation, globalService.currentUserLiveData.value!!,
activity!!
)
)
}
recyclerViewAdapter.updateDataSet(
newConversations as List<IFlexible<ConversationItem.ConversationItemViewHolder>>, false
)
})
networkStateLiveData.observe(this@ConversationsListView, Observer { value ->
when (value) {
LOADING -> {
view.post {
view.loadingStateView?.visibility = View.VISIBLE
view.recyclerView?.visibility = View.GONE
view.stateWithMessageView?.visibility = View.GONE
view.floatingActionButton?.visibility = View.GONE
}
}
LOADED -> {
// awesome, but we delegate the magic stuff to the data handler
view.post {
view.loadingStateView?.visibility = View.GONE
view.recyclerView?.visibility = View.VISIBLE
view.stateWithMessageView?.visibility = if (recyclerViewAdapter.isEmpty) View.VISIBLE else View.GONE
view.floatingActionButton?.visibility = View.VISIBLE
if (view.floatingActionButton?.isShown == false) {
view.floatingActionButton?.show()
}
}
}
FAILED -> {
// probably offline, so what? :)
view.post {
view.loadingStateView?.visibility = View.GONE
view.recyclerView?.visibility = View.VISIBLE
view.floatingActionButton?.visibility = View.GONE
view.stateWithMessageView?.visibility = if (recyclerViewAdapter.isEmpty) View.VISIBLE else View.GONE
}
}
else -> {
// We should not be here
}
}
})
searchQuery.observe(this@ConversationsListView, Observer {
recyclerViewAdapter.setFilter(it)
recyclerViewAdapter.filterItems(500)
})
} }
return view return view
} }
private fun onElementClick(page: Page, holder: Presenter.Holder, element: Element<Conversation>) {
val conversation = element.data
val user = viewModel.globalService.currentUserLiveData.value
user?.let { user ->
conversation?.let { conversation ->
val bundle = Bundle()
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, user)
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation.token)
bundle.putString(BundleKeys.KEY_ROOM_ID, conversation.conversationId)
bundle.putParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION, Parcels.wrap(conversation))
ConductorRemapping.remapChatController(
router, user.id!!, conversation.token!!,
bundle, false
)
}
}
}
override fun getLayoutId(): Int { override fun getLayoutId(): Int {
return R.layout.controller_conversations_rv return R.layout.controller_conversations_rv
} }
@ -281,13 +198,6 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
openNewConversationScreen() openNewConversationScreen()
} }
@OnClick(R.id.stateWithMessageView)
fun onStateWithMessageViewClick() {
if (view?.floatingActionButton?.isVisible == true) {
openNewConversationScreen()
}
}
private fun openNewConversationScreen() { private fun openNewConversationScreen() {
val bundle = Bundle() val bundle = Bundle()
bundle.putBoolean(BundleKeys.KEY_NEW_CONVERSATION, true) bundle.putBoolean(BundleKeys.KEY_NEW_CONVERSATION, true)
@ -298,31 +208,6 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
) )
} }
private fun getShareIntentForConversation(conversation: Conversation): Intent {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(
Intent.EXTRA_SUBJECT,
String.format(
context.getString(R.string.nc_share_subject),
context.getString(R.string.nc_app_name)
)
)
// TODO, make sure we ask for password if needed
putExtra(
Intent.EXTRA_TEXT, ShareUtils.getStringForIntent(
context, null, conversation
)
)
type = "text/plain"
}
// TODO filter our own app once we're there
return Intent.createChooser(sendIntent, context.getString(R.string.nc_share_link))
}
private fun getConversationMenuItemsForConversation(conversation: Conversation): MutableList<BasicListItemWithImage> { private fun getConversationMenuItemsForConversation(conversation: Conversation): MutableList<BasicListItemWithImage> {
val items = mutableListOf<BasicListItemWithImage>() val items = mutableListOf<BasicListItemWithImage>()
@ -381,88 +266,4 @@ class ConversationsListView : BaseView(), OnQueryTextListener,
super.onRestoreViewState(view, savedViewState) super.onRestoreViewState(view, savedViewState)
viewModel.loadConversations() viewModel.loadConversations()
} }
override fun onItemLongClick(position: Int) {
val clickedItem = recyclerViewAdapter.getItem(position)
clickedItem?.let {
val conversation = (it as ConversationItem).model
activity?.let { activity ->
MaterialDialog(activity, BottomSheet(WRAP_CONTENT)).show {
cornerRadius(res = R.dimen.corner_radius)
title(text = conversation.displayName)
listItemsWithImage(getConversationMenuItemsForConversation(conversation)
) { dialog,
index, item ->
when (item.iconRes) {
drawable.ic_star_border_black_24dp -> {
viewModel.changeFavoriteValueForConversation(conversation, false)
}
drawable.ic_star_black_24dp -> {
viewModel.changeFavoriteValueForConversation(conversation, true)
}
drawable.ic_share_black_24dp -> {
startActivity(getShareIntentForConversation(conversation))
}
drawable.ic_exit_to_app_black_24dp -> {
MaterialDialog(activity).show {
title(R.string.nc_leave)
message(R.string.nc_leave_message)
positiveButton(R.string.nc_simple_leave) { dialog ->
viewModel.leaveConversation(conversation)
}
negativeButton(R.string.nc_cancel)
icon(drawable.ic_exit_to_app_black_24dp)
}
}
drawable.ic_delete_grey600_24dp -> {
MaterialDialog(activity).show {
title(R.string.nc_delete)
message(text = conversation.deleteWarningMessage)
positiveButton(R.string.nc_delete_call) { dialog ->
viewModel.deleteConversation(conversation)
}
negativeButton(R.string.nc_cancel)
icon(
drawable = DisplayUtils.getTintedDrawable(
resources!!, drawable
.ic_delete_grey600_24dp, R.color.nc_darkRed
)
)
}
}
else -> {
}
}
}
}
}
}
}
override fun onItemClick(
view: View?,
position: Int
): Boolean {
val clickedItem = recyclerViewAdapter.getItem(position)
if (clickedItem != null) {
val conversationItem = clickedItem as ConversationItem
val conversation = conversationItem.model
val bundle = Bundle()
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationItem.user)
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation.token)
bundle.putString(BundleKeys.KEY_ROOM_ID, conversation.conversationId)
bundle.putParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION, Parcels.wrap(conversation))
ConductorRemapping.remapChatController(
router, conversationItem.user.id!!, conversation.token!!,
bundle, false
)
}
return true
}
} }

View File

@ -60,9 +60,8 @@ class ConversationsListViewModel constructor(
private val conversationsLoadingLock = ReentrantLock() private val conversationsLoadingLock = ReentrantLock()
var messageData: String? = null var messageData: String? = null
val searchQuery = MutableLiveData<String>()
val networkStateLiveData: MutableLiveData<ConversationsListViewNetworkState> = MutableLiveData(ConversationsListViewNetworkState.LOADING) val networkStateLiveData: MutableLiveData<ConversationsListViewNetworkState> = MutableLiveData(ConversationsListViewNetworkState.LOADING)
val currentUserAvatar: MutableLiveData<Drawable> = MutableLiveData(DisplayUtils.getRoundedDrawable(context.getDrawable(R.drawable.ic_settings_white_24dp))) val avatar: MutableLiveData<Drawable> = MutableLiveData(DisplayUtils.getRoundedDrawable(context.getDrawable(R.drawable.ic_settings_white_24dp)))
val conversationsLiveData = Transformations.switchMap(globalService.currentUserLiveData) { val conversationsLiveData = Transformations.switchMap(globalService.currentUserLiveData) {
if (networkStateLiveData.value != ConversationsListViewNetworkState.LOADING) { if (networkStateLiveData.value != ConversationsListViewNetworkState.LOADING) {
networkStateLiveData.postValue(ConversationsListViewNetworkState.LOADING) networkStateLiveData.postValue(ConversationsListViewNetworkState.LOADING)
@ -163,12 +162,12 @@ class ConversationsListViewModel constructor(
operationUser?.let { operationUser?.let {
viewModelScope.launch { viewModelScope.launch {
val url = ApiUtils.getUrlForAvatarWithNameAndPixels(it.baseUrl, it.userId, 512) val url = ApiUtils.getUrlForAvatarWithNameAndPixels(it.baseUrl, it.userId, 256)
val drawable = Coil.get((url)) { val drawable = Coil.get((url)) {
addHeader("Authorization", it.getCredentials()) addHeader("Authorization", it.getCredentials())
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
currentUserAvatar.postValue(drawable) avatar.postValue(drawable)
} }
} }
} }

View File

@ -26,11 +26,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true"> android:animateLayoutChanges="true">
<include
layout="@layout/view_states"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayoutView" android:id="@+id/swipeRefreshLayoutView"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -59,6 +54,4 @@
app:srcCompat="@drawable/ic_add_white_24px" app:srcCompat="@drawable/ic_add_white_24px"
app:tint="@color/white" /> app:tint="@color/white" />
<include layout="@layout/fast_scroller" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ /*
~ * Nextcloud Talk application
~ *
~ * @author Mario Danic
~ * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
~ *
~ * This program is free software: you can redistribute it and/or modify
~ * it under the terms of the GNU General Public License as published by
~ * the Free Software Foundation, either version 3 of the License, or
~ * at your option) any later version.
~ *
~ * This program is distributed in the hope that it will be useful,
~ * but WITHOUT ANY WARRANTY; without even the implied warranty of
~ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ * GNU General Public License for more details.
~ *
~ * You should have received a copy of the GNU General Public License
~ * along with this program. If not, see <http://www.gnu.org/licenses/>.
~ */
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/loadingStateView"
android:layout_width="@dimen/item_height"
android:layout_height="@dimen/item_height"
android:layout_centerInParent="true"
android:layout_gravity="center"
android:layout_margin="@dimen/activity_horizontal_margin"
android:indeterminate="true"
android:indeterminateTint="@color/colorPrimary"
android:indeterminateTintMode="src_in" />
</RelativeLayout>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ /*
~ * Nextcloud Talk application
~ *
~ * @author Mario Danic
~ * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
~ *
~ * This program is free software: you can redistribute it and/or modify
~ * it under the terms of the GNU General Public License as published by
~ * the Free Software Foundation, either version 3 of the License, or
~ * at your option) any later version.
~ *
~ * This program is distributed in the hope that it will be useful,
~ * but WITHOUT ANY WARRANTY; without even the implied warranty of
~ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ * GNU General Public License for more details.
~ *
~ * You should have received a copy of the GNU General Public License
~ * along with this program. If not, see <http://www.gnu.org/licenses/>.
~ */
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent">
<ImageView
android:id="@+id/messageStateImageView"
android:layout_width="@dimen/item_height"
android:layout_height="@dimen/item_height"
android:layout_above="@id/messageStateTextView"
android:layout_centerHorizontal="true"
android:src="@drawable/ic_logo"
android:tint="@color/colorPrimary"
/>
<TextView
android:id="@+id/messageStateTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="8dp"
android:textAlignment="center"
android:text="@string/nc_conversations_empty"
android:textSize="20sp"
/>
</RelativeLayout>

View File

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Talk application
~
~ @author Mario Danic
~ Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ProgressBar
android:id="@+id/loadingStateView"
android:layout_width="@dimen/item_height"
android:layout_height="@dimen/item_height"
android:layout_gravity="center"
android:layout_margin="@dimen/activity_horizontal_margin"
android:indeterminate="true"
android:layout_centerInParent="true"
android:indeterminateTint="@color/colorPrimary"
android:indeterminateTintMode="src_in"
android:visibility="gone"
/>
<RelativeLayout
android:id="@+id/stateWithMessageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
>
<ImageView
android:id="@+id/errorStateImageView"
android:layout_width="@dimen/item_height"
android:layout_height="@dimen/item_height"
android:layout_above="@id/errorStateTextView"
android:layout_centerHorizontal="true"
android:src="@drawable/ic_announcement_white_24dp"
android:tint="@color/colorPrimary"
/>
<TextView
android:id="@+id/errorStateTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="8dp"
android:textAlignment="center"
android:textSize="20sp"
/>
</RelativeLayout>
</RelativeLayout>

View File

@ -316,6 +316,8 @@
<string name="nc_not_defined_error">Unknown error</string> <string name="nc_not_defined_error">Unknown error</string>
<string name="nc_unauthorized_error">Unauthorized</string> <string name="nc_unauthorized_error">Unauthorized</string>
<string name="nc_oops">Ooops, something went wrong.</string>
<string name="nc_general_settings">General</string> <string name="nc_general_settings">General</string>
<string name="nc_allow_guests">Allow guests</string> <string name="nc_allow_guests">Allow guests</string>
<string name="nc_last_moderator_title">Could not leave conversation</string> <string name="nc_last_moderator_title">Could not leave conversation</string>