mirror of
https://github.com/nextcloud/talk-android
synced 2025-07-12 15:24:09 +01:00
Merge pull request #2028 from nextcloud/feat/1047/message-search
Message search
This commit is contained in:
commit
5d482e4857
@ -172,6 +172,10 @@
|
||||
android:name=".shareditems.activities.SharedItemsActivity"
|
||||
android:theme="@style/AppTheme"/>
|
||||
|
||||
<activity
|
||||
android:name=".messagesearch.MessageSearchActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<receiver android:name=".receivers.PackageReplacedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
|
@ -67,6 +67,8 @@ import eu.davidea.viewholders.FlexibleViewHolder;
|
||||
public class ConversationItem extends AbstractFlexibleItem<ConversationItem.ConversationItemViewHolder> implements
|
||||
ISectionable<ConversationItem.ConversationItemViewHolder, GenericTextHeaderItem>, IFilterable<String> {
|
||||
|
||||
public static final int VIEW_TYPE = R.layout.rv_item_conversation_with_last_message;
|
||||
|
||||
private static final float STATUS_SIZE_IN_DP = 9f;
|
||||
|
||||
private final Conversation conversation;
|
||||
@ -75,6 +77,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
|
||||
private GenericTextHeaderItem header;
|
||||
private final Status status;
|
||||
|
||||
|
||||
public ConversationItem(Conversation conversation, UserEntity userEntity, Context activityContext, Status status) {
|
||||
this.conversation = conversation;
|
||||
this.userEntity = userEntity;
|
||||
@ -114,6 +117,11 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
|
||||
return R.layout.rv_item_conversation_with_last_message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType() {
|
||||
return VIEW_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConversationItemViewHolder createViewHolder(View view, FlexibleAdapter<IFlexible> adapter) {
|
||||
return new ConversationItemViewHolder(view, adapter);
|
||||
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.databinding.RvItemLoadMoreBinding
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
|
||||
object LoadMoreResultsItem :
|
||||
AbstractFlexibleItem<LoadMoreResultsItem.ViewHolder>(),
|
||||
IFilterable<String> {
|
||||
|
||||
// layout is used as view type for uniqueness
|
||||
const val VIEW_TYPE: Int = R.layout.rv_item_load_more
|
||||
|
||||
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemLoadMoreBinding
|
||||
|
||||
init {
|
||||
binding = RvItemLoadMoreBinding.bind(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_load_more
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
|
||||
): ViewHolder = ViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>?
|
||||
) {
|
||||
// nothing, it's immutable
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean = true
|
||||
|
||||
override fun getItemViewType(): Int {
|
||||
return VIEW_TYPE
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is LoadMoreResultsItem
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return 0
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableString
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.databinding.RvItemSearchMessageBinding
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFilterable
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
|
||||
data class MessageResultItem constructor(
|
||||
private val context: Context,
|
||||
private val currentUser: UserEntity,
|
||||
val messageEntry: SearchMessageEntry,
|
||||
private val showHeader: Boolean = false
|
||||
) :
|
||||
AbstractFlexibleItem<MessageResultItem.ViewHolder>(),
|
||||
IFilterable<String>,
|
||||
ISectionable<MessageResultItem.ViewHolder, GenericTextHeaderItem> {
|
||||
|
||||
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) :
|
||||
FlexibleViewHolder(view, adapter) {
|
||||
var binding: RvItemSearchMessageBinding
|
||||
|
||||
init {
|
||||
binding = RvItemSearchMessageBinding.bind(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLayoutRes(): Int = R.layout.rv_item_search_message
|
||||
|
||||
override fun createViewHolder(
|
||||
view: View,
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
|
||||
): ViewHolder = ViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>?
|
||||
) {
|
||||
holder.binding.conversationTitle.text = messageEntry.title
|
||||
bindMessageExcerpt(holder)
|
||||
loadImage(holder)
|
||||
}
|
||||
|
||||
private fun bindMessageExcerpt(holder: ViewHolder) {
|
||||
val messageSpannable = SpannableString(messageEntry.messageExcerpt)
|
||||
val highlightColor = ContextCompat.getColor(context, R.color.colorPrimary)
|
||||
val highlightedSpan = DisplayUtils.searchAndColor(messageSpannable, messageEntry.searchTerm, highlightColor)
|
||||
holder.binding.messageExcerpt.text = highlightedSpan
|
||||
}
|
||||
|
||||
private fun loadImage(holder: ViewHolder) {
|
||||
DisplayUtils.loadAvatarPlaceholder(holder.binding.thumbnail)
|
||||
if (messageEntry.thumbnailURL != null) {
|
||||
val imageRequest = DisplayUtils.getImageRequestForUrl(
|
||||
messageEntry.thumbnailURL,
|
||||
currentUser
|
||||
)
|
||||
DisplayUtils.loadImage(holder.binding.thumbnail, imageRequest)
|
||||
}
|
||||
}
|
||||
|
||||
override fun filter(constraint: String?): Boolean = true
|
||||
|
||||
override fun getItemViewType(): Int {
|
||||
return VIEW_TYPE
|
||||
}
|
||||
|
||||
companion object {
|
||||
// layout is used as view type for uniqueness
|
||||
const val VIEW_TYPE: Int = R.layout.rv_item_search_message
|
||||
}
|
||||
|
||||
override fun getHeader(): GenericTextHeaderItem = MessagesTextHeaderItem(context)
|
||||
.apply {
|
||||
isHidden = showHeader // FlexibleAdapter needs this hack for some reason
|
||||
}
|
||||
|
||||
override fun setHeader(header: GenericTextHeaderItem?) {
|
||||
// nothing, header is always the same
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.adapters.items
|
||||
|
||||
import android.content.Context
|
||||
import com.nextcloud.talk.R
|
||||
|
||||
class MessagesTextHeaderItem(context: Context) : GenericTextHeaderItem(context.getString(R.string.messages)) {
|
||||
companion object {
|
||||
/**
|
||||
* "Random" value, just has to be different than other view types
|
||||
*/
|
||||
const val VIEW_TYPE = 1120391230
|
||||
}
|
||||
|
||||
override fun getItemViewType(): Int = VIEW_TYPE
|
||||
}
|
@ -45,6 +45,7 @@ import com.nextcloud.talk.models.json.signaling.SignalingOverall;
|
||||
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
|
||||
import com.nextcloud.talk.models.json.status.StatusOverall;
|
||||
import com.nextcloud.talk.models.json.statuses.StatusesOverall;
|
||||
import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchOverall;
|
||||
import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall;
|
||||
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
|
||||
|
||||
@ -65,7 +66,6 @@ import retrofit2.http.FieldMap;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
@ -519,4 +519,12 @@ public interface NcApi {
|
||||
Observable<ReactionsOverall> getReactions(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("reaction") String reaction);
|
||||
|
||||
@GET
|
||||
Observable<UnifiedSearchOverall> performUnifiedSearch(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("term") String term,
|
||||
@Query("from") String fromUrl,
|
||||
@Query("limit") Integer limit,
|
||||
@Query("cursor") Integer cursor);
|
||||
}
|
||||
|
@ -213,7 +213,6 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
|
||||
.contextModule(ContextModule(applicationContext))
|
||||
.databaseModule(DatabaseModule())
|
||||
.restModule(RestModule(applicationContext))
|
||||
.userModule(UserModule())
|
||||
.arbitraryStorageModule(ArbitraryStorageModule())
|
||||
.build()
|
||||
}
|
||||
|
@ -130,6 +130,7 @@ import com.nextcloud.talk.events.UserMentionClickEvent
|
||||
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
||||
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
|
||||
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
|
||||
import com.nextcloud.talk.messagesearch.MessageSearchActivity
|
||||
import com.nextcloud.talk.models.database.CapabilitiesUtil
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
import com.nextcloud.talk.models.json.chat.ChatMessage
|
||||
@ -1345,91 +1346,21 @@ class ChatController(args: Bundle) :
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
if (resultCode != RESULT_OK) {
|
||||
if (resultCode != RESULT_OK && (requestCode != REQUEST_CODE_MESSAGE_SEARCH)) {
|
||||
Log.e(TAG, "resultCode for received intent was != ok")
|
||||
return
|
||||
}
|
||||
|
||||
if (requestCode == REQUEST_CODE_CHOOSE_FILE) {
|
||||
try {
|
||||
checkNotNull(intent)
|
||||
filesToUpload.clear()
|
||||
intent.clipData?.let {
|
||||
for (index in 0 until it.itemCount) {
|
||||
filesToUpload.add(it.getItemAt(index).uri.toString())
|
||||
}
|
||||
} ?: run {
|
||||
checkNotNull(intent.data)
|
||||
intent.data.let {
|
||||
filesToUpload.add(intent.data.toString())
|
||||
}
|
||||
}
|
||||
require(filesToUpload.isNotEmpty())
|
||||
|
||||
val filenamesWithLinebreaks = StringBuilder("\n")
|
||||
|
||||
for (file in filesToUpload) {
|
||||
val filename = UriUtils.getFileName(Uri.parse(file), context)
|
||||
filenamesWithLinebreaks.append(filename).append("\n")
|
||||
}
|
||||
|
||||
val confirmationQuestion = when (filesToUpload.size) {
|
||||
1 -> context?.resources?.getString(R.string.nc_upload_confirm_send_single)?.let {
|
||||
String.format(it, title)
|
||||
}
|
||||
else -> context?.resources?.getString(R.string.nc_upload_confirm_send_multiple)?.let {
|
||||
String.format(it, title)
|
||||
}
|
||||
}
|
||||
|
||||
LovelyStandardDialog(activity)
|
||||
.setPositiveButtonColorRes(R.color.nc_darkGreen)
|
||||
.setTitle(confirmationQuestion)
|
||||
.setMessage(filenamesWithLinebreaks.toString())
|
||||
.setPositiveButton(R.string.nc_yes) { v ->
|
||||
if (UploadAndShareFilesWorker.isStoragePermissionGranted(context!!)) {
|
||||
uploadFiles(filesToUpload, false)
|
||||
} else {
|
||||
UploadAndShareFilesWorker.requestStoragePermission(this)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.nc_no) {
|
||||
// unused atm
|
||||
}
|
||||
.show()
|
||||
} catch (e: IllegalStateException) {
|
||||
Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
||||
}
|
||||
} else if (requestCode == REQUEST_CODE_SELECT_CONTACT) {
|
||||
val contactUri = intent?.data ?: return
|
||||
val cursor: Cursor? = activity?.contentResolver!!.query(contactUri, null, null, null, null)
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
val id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
|
||||
val fileName = ContactUtils.getDisplayNameFromDeviceContact(context!!, id) + ".vcf"
|
||||
val file = File(context?.cacheDir, fileName)
|
||||
writeContactToVcfFile(cursor, file)
|
||||
|
||||
val shareUri = FileProvider.getUriForFile(
|
||||
activity!!,
|
||||
BuildConfig.APPLICATION_ID,
|
||||
File(file.absolutePath)
|
||||
)
|
||||
uploadFiles(mutableListOf(shareUri.toString()), false)
|
||||
}
|
||||
cursor?.close()
|
||||
} else if (requestCode == REQUEST_CODE_PICK_CAMERA) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
when (requestCode) {
|
||||
REQUEST_CODE_CHOOSE_FILE -> {
|
||||
try {
|
||||
checkNotNull(intent)
|
||||
filesToUpload.clear()
|
||||
run {
|
||||
intent.clipData?.let {
|
||||
for (index in 0 until it.itemCount) {
|
||||
filesToUpload.add(it.getItemAt(index).uri.toString())
|
||||
}
|
||||
} ?: run {
|
||||
checkNotNull(intent.data)
|
||||
intent.data.let {
|
||||
filesToUpload.add(intent.data.toString())
|
||||
@ -1437,11 +1368,37 @@ class ChatController(args: Bundle) :
|
||||
}
|
||||
require(filesToUpload.isNotEmpty())
|
||||
|
||||
if (UploadAndShareFilesWorker.isStoragePermissionGranted(context!!)) {
|
||||
uploadFiles(filesToUpload, false)
|
||||
} else {
|
||||
UploadAndShareFilesWorker.requestStoragePermission(this)
|
||||
val filenamesWithLinebreaks = StringBuilder("\n")
|
||||
|
||||
for (file in filesToUpload) {
|
||||
val filename = UriUtils.getFileName(Uri.parse(file), context)
|
||||
filenamesWithLinebreaks.append(filename).append("\n")
|
||||
}
|
||||
|
||||
val confirmationQuestion = when (filesToUpload.size) {
|
||||
1 -> context?.resources?.getString(R.string.nc_upload_confirm_send_single)?.let {
|
||||
String.format(it, title)
|
||||
}
|
||||
else -> context?.resources?.getString(R.string.nc_upload_confirm_send_multiple)?.let {
|
||||
String.format(it, title)
|
||||
}
|
||||
}
|
||||
|
||||
LovelyStandardDialog(activity)
|
||||
.setPositiveButtonColorRes(R.color.nc_darkGreen)
|
||||
.setTitle(confirmationQuestion)
|
||||
.setMessage(filenamesWithLinebreaks.toString())
|
||||
.setPositiveButton(R.string.nc_yes) { v ->
|
||||
if (UploadAndShareFilesWorker.isStoragePermissionGranted(context!!)) {
|
||||
uploadFiles(filesToUpload, false)
|
||||
} else {
|
||||
UploadAndShareFilesWorker.requestStoragePermission(this)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.nc_no) {
|
||||
// unused atm
|
||||
}
|
||||
.show()
|
||||
} catch (e: IllegalStateException) {
|
||||
Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
@ -1452,6 +1409,79 @@ class ChatController(args: Bundle) :
|
||||
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
||||
}
|
||||
}
|
||||
REQUEST_CODE_SELECT_CONTACT -> {
|
||||
val contactUri = intent?.data ?: return
|
||||
val cursor: Cursor? = activity?.contentResolver!!.query(contactUri, null, null, null, null)
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
val id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
|
||||
val fileName = ContactUtils.getDisplayNameFromDeviceContact(context!!, id) + ".vcf"
|
||||
val file = File(context?.cacheDir, fileName)
|
||||
writeContactToVcfFile(cursor, file)
|
||||
|
||||
val shareUri = FileProvider.getUriForFile(
|
||||
activity!!,
|
||||
BuildConfig.APPLICATION_ID,
|
||||
File(file.absolutePath)
|
||||
)
|
||||
uploadFiles(mutableListOf(shareUri.toString()), false)
|
||||
}
|
||||
cursor?.close()
|
||||
}
|
||||
REQUEST_CODE_PICK_CAMERA -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
try {
|
||||
checkNotNull(intent)
|
||||
filesToUpload.clear()
|
||||
run {
|
||||
checkNotNull(intent.data)
|
||||
intent.data.let {
|
||||
filesToUpload.add(intent.data.toString())
|
||||
}
|
||||
}
|
||||
require(filesToUpload.isNotEmpty())
|
||||
|
||||
if (UploadAndShareFilesWorker.isStoragePermissionGranted(context!!)) {
|
||||
uploadFiles(filesToUpload, false)
|
||||
} else {
|
||||
UploadAndShareFilesWorker.requestStoragePermission(this)
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context?.resources?.getString(R.string.nc_upload_failed),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context?.resources?.getString(R.string.nc_upload_failed),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
REQUEST_CODE_MESSAGE_SEARCH -> {
|
||||
val messageId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
|
||||
messageId?.let { id ->
|
||||
scrollToMessageWithId(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToMessageWithId(messageId: String) {
|
||||
val position = adapter?.items?.indexOfFirst {
|
||||
it.item is ChatMessage && (it.item as ChatMessage).id == messageId
|
||||
}
|
||||
if (position != null && position >= 0) {
|
||||
binding.messagesListView.smoothScrollToPosition(position)
|
||||
} else {
|
||||
// TODO show error that we don't have that message?
|
||||
}
|
||||
}
|
||||
|
||||
@ -2279,6 +2309,7 @@ class ChatController(args: Bundle) :
|
||||
if (adapter != null) {
|
||||
adapter?.addToEnd(chatMessageList, false)
|
||||
}
|
||||
scrollToRequestedMessageIfNeeded()
|
||||
} else {
|
||||
|
||||
var chatMessage: ChatMessage
|
||||
@ -2394,6 +2425,12 @@ class ChatController(args: Bundle) :
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToRequestedMessageIfNeeded() {
|
||||
args.getString(BundleKeys.KEY_MESSAGE_ID)?.let {
|
||||
scrollToMessageWithId(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSameDayNonSystemMessages(messageLeft: ChatMessage, messageRight: ChatMessage): Boolean {
|
||||
return TextUtils.isEmpty(messageLeft.systemMessage) &&
|
||||
TextUtils.isEmpty(messageRight.systemMessage) &&
|
||||
@ -2465,32 +2502,38 @@ class ChatController(args: Bundle) :
|
||||
if (CapabilitiesUtil.hasSpreedFeatureCapability(it, "read-only-rooms")) {
|
||||
checkShowCallButtons()
|
||||
}
|
||||
val searchItem = menu.findItem(R.id.conversation_search)
|
||||
searchItem.isVisible = CapabilitiesUtil.isUnifiedSearchAvailable(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
(activity as MainActivity).resetConversationsList()
|
||||
return true
|
||||
true
|
||||
}
|
||||
R.id.conversation_video_call -> {
|
||||
startACall(false, false)
|
||||
return true
|
||||
true
|
||||
}
|
||||
R.id.conversation_voice_call -> {
|
||||
startACall(true, false)
|
||||
return true
|
||||
true
|
||||
}
|
||||
R.id.conversation_info -> {
|
||||
showConversationInfoScreen()
|
||||
return true
|
||||
true
|
||||
}
|
||||
R.id.shared_items -> {
|
||||
showSharedItems()
|
||||
return true
|
||||
true
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
R.id.conversation_search -> {
|
||||
startMessageSearch()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2502,6 +2545,13 @@ class ChatController(args: Bundle) :
|
||||
activity!!.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun startMessageSearch() {
|
||||
val intent = Intent(activity, MessageSearchActivity::class.java)
|
||||
intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
|
||||
intent.putExtra(KEY_ROOM_TOKEN, roomToken)
|
||||
startActivityForResult(intent, REQUEST_CODE_MESSAGE_SEARCH)
|
||||
}
|
||||
|
||||
private fun handleSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
|
||||
val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
|
||||
val chatMessageIterator = chatMessageMap.iterator()
|
||||
@ -3087,6 +3137,7 @@ class ChatController(args: Bundle) :
|
||||
private const val AGE_THREHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
|
||||
private const val REQUEST_CODE_CHOOSE_FILE: Int = 555
|
||||
private const val REQUEST_CODE_SELECT_CONTACT: Int = 666
|
||||
private const val REQUEST_CODE_MESSAGE_SEARCH: Int = 777
|
||||
private const val REQUEST_RECORD_AUDIO_PERMISSION = 222
|
||||
private const val REQUEST_READ_CONTACT_PERMISSION = 234
|
||||
private const val REQUEST_CAMERA_PERMISSION = 223
|
||||
|
@ -68,6 +68,9 @@ import com.nextcloud.talk.R;
|
||||
import com.nextcloud.talk.activities.MainActivity;
|
||||
import com.nextcloud.talk.adapters.items.ConversationItem;
|
||||
import com.nextcloud.talk.adapters.items.GenericTextHeaderItem;
|
||||
import com.nextcloud.talk.adapters.items.LoadMoreResultsItem;
|
||||
import com.nextcloud.talk.adapters.items.MessageResultItem;
|
||||
import com.nextcloud.talk.adapters.items.MessagesTextHeaderItem;
|
||||
import com.nextcloud.talk.api.NcApi;
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication;
|
||||
import com.nextcloud.talk.controllers.base.BaseController;
|
||||
@ -78,11 +81,14 @@ import com.nextcloud.talk.jobs.AccountRemovalWorker;
|
||||
import com.nextcloud.talk.jobs.ContactAddressBookWorker;
|
||||
import com.nextcloud.talk.jobs.DeleteConversationWorker;
|
||||
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker;
|
||||
import com.nextcloud.talk.messagesearch.MessageSearchHelper;
|
||||
import com.nextcloud.talk.models.database.CapabilitiesUtil;
|
||||
import com.nextcloud.talk.models.database.UserEntity;
|
||||
import com.nextcloud.talk.models.domain.SearchMessageEntry;
|
||||
import com.nextcloud.talk.models.json.conversations.Conversation;
|
||||
import com.nextcloud.talk.models.json.status.Status;
|
||||
import com.nextcloud.talk.models.json.statuses.StatusesOverall;
|
||||
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository;
|
||||
import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment;
|
||||
import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog;
|
||||
import com.nextcloud.talk.utils.ApiUtils;
|
||||
@ -94,6 +100,7 @@ import com.nextcloud.talk.utils.UriUtils;
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys;
|
||||
import com.nextcloud.talk.utils.database.user.UserUtils;
|
||||
import com.nextcloud.talk.utils.preferences.AppPreferences;
|
||||
import com.nextcloud.talk.utils.rx.SearchViewObservable;
|
||||
import com.webianks.library.PopupBubble;
|
||||
import com.yarolegovich.lovelydialog.LovelySaveStateHandler;
|
||||
import com.yarolegovich.lovelydialog.LovelyStandardDialog;
|
||||
@ -110,6 +117,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ -131,6 +139,8 @@ import butterknife.BindView;
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager;
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
|
||||
import eu.davidea.flexibleadapter.items.IHeader;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Observer;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
@ -138,13 +148,16 @@ import io.reactivex.schedulers.Schedulers;
|
||||
import retrofit2.HttpException;
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication.class)
|
||||
public class ConversationsListController extends BaseController implements SearchView.OnQueryTextListener,
|
||||
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, ConversationMenuInterface {
|
||||
public class ConversationsListController extends BaseController implements FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, ConversationMenuInterface {
|
||||
|
||||
public static final String TAG = "ConvListController";
|
||||
public static final int ID_DELETE_CONVERSATION_DIALOG = 0;
|
||||
public static final int UNREAD_BUBBLE_DELAY = 2500;
|
||||
private static final String KEY_SEARCH_QUERY = "ContactsController.searchQuery";
|
||||
|
||||
public static final int SEARCH_DEBOUNCE_INTERVAL_MS = 300;
|
||||
public static final int SEARCH_MIN_CHARS = 2;
|
||||
|
||||
private final Bundle bundle;
|
||||
@Inject
|
||||
UserUtils userUtils;
|
||||
@ -161,6 +174,9 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
@Inject
|
||||
AppPreferences appPreferences;
|
||||
|
||||
@Inject
|
||||
UnifiedSearchRepository unifiedSearchRepository;
|
||||
|
||||
@BindView(R.id.recycler_view)
|
||||
RecyclerView recyclerView;
|
||||
|
||||
@ -207,6 +223,7 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
private Conversation selectedConversation;
|
||||
|
||||
private String textToPaste = "";
|
||||
private String selectedMessageId = null;
|
||||
|
||||
private boolean forwardMessage;
|
||||
|
||||
@ -220,6 +237,9 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
|
||||
private HashMap<String, Status> userStatuses = new HashMap<>();
|
||||
|
||||
private MessageSearchHelper searchHelper;
|
||||
private Disposable searchViewDisposable;
|
||||
|
||||
public ConversationsListController(Bundle bundle) {
|
||||
super();
|
||||
setHasOptionsMenu(true);
|
||||
@ -306,6 +326,10 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
return;
|
||||
}
|
||||
|
||||
if (CapabilitiesUtil.isUnifiedSearchAvailable(currentUser)) {
|
||||
searchHelper = new MessageSearchHelper(unifiedSearchRepository);
|
||||
}
|
||||
|
||||
credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken());
|
||||
if (getActivity() != null && getActivity() instanceof MainActivity) {
|
||||
loadUserAvatar(((MainActivity) getActivity()).binding.switchAccountButton);
|
||||
@ -339,7 +363,18 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
if (searchManager != null) {
|
||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(getActivity().getComponentName()));
|
||||
}
|
||||
searchView.setOnQueryTextListener(this);
|
||||
searchViewDisposable = SearchViewObservable.observeSearchView(searchView)
|
||||
.debounce(query -> {
|
||||
if (TextUtils.isEmpty(query)) {
|
||||
return Observable.empty();
|
||||
} else {
|
||||
return Observable.timer(SEARCH_DEBOUNCE_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
})
|
||||
.distinctUntilChanged()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::onQueryTextChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -419,6 +454,11 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
adapter.setHeadersShown(false);
|
||||
adapter.updateDataSet(conversationItems, false);
|
||||
adapter.hideAllHeaders();
|
||||
if (searchHelper != null) {
|
||||
// cancel any pending searches
|
||||
searchHelper.cancelSearch();
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
if (swipeRefreshLayout != null) {
|
||||
swipeRefreshLayout.setEnabled(true);
|
||||
}
|
||||
@ -427,8 +467,8 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
MainActivity activity = (MainActivity) getActivity();
|
||||
if (activity != null) {
|
||||
activity.binding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator(
|
||||
activity.binding.appBar.getContext(),
|
||||
R.animator.appbar_elevation_off)
|
||||
activity.binding.appBar.getContext(),
|
||||
R.animator.appbar_elevation_off)
|
||||
);
|
||||
activity.binding.toolbar.setVisibility(View.GONE);
|
||||
activity.binding.searchToolbar.setVisibility(View.VISIBLE);
|
||||
@ -842,28 +882,76 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
dispose(null);
|
||||
searchViewDisposable.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
if (adapter.hasNewFilter(newText) || !TextUtils.isEmpty(searchQuery)) {
|
||||
if (!TextUtils.isEmpty(searchQuery)) {
|
||||
adapter.setFilter(searchQuery);
|
||||
searchQuery = "";
|
||||
adapter.filterItems();
|
||||
} else {
|
||||
adapter.setFilter(newText);
|
||||
adapter.filterItems(300);
|
||||
}
|
||||
public void onQueryTextChange(final String newText) {
|
||||
if (!TextUtils.isEmpty(searchQuery)) {
|
||||
final String filter = searchQuery;
|
||||
searchQuery = "";
|
||||
performFilterAndSearch(filter);
|
||||
} else if (adapter.hasNewFilter(newText)) {
|
||||
performFilterAndSearch(newText);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
return onQueryTextChange(query);
|
||||
private void performFilterAndSearch(String filter) {
|
||||
if (filter.length() >= SEARCH_MIN_CHARS) {
|
||||
clearMessageSearchResults();
|
||||
adapter.setFilter(filter);
|
||||
adapter.filterItems();
|
||||
if (CapabilitiesUtil.isUnifiedSearchAvailable(currentUser)) {
|
||||
startMessageSearch(filter);
|
||||
}
|
||||
} else {
|
||||
resetSearchResults();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetSearchResults() {
|
||||
clearMessageSearchResults();
|
||||
adapter.setFilter("");
|
||||
adapter.filterItems();
|
||||
}
|
||||
|
||||
private void clearMessageSearchResults() {
|
||||
final IHeader firstHeader = adapter.getSectionHeader(0);
|
||||
if (firstHeader != null && firstHeader.getItemViewType() == MessagesTextHeaderItem.VIEW_TYPE) {
|
||||
adapter.removeSection(firstHeader);
|
||||
} else {
|
||||
adapter.removeItemsOfType(MessageResultItem.VIEW_TYPE);
|
||||
}
|
||||
adapter.removeItemsOfType(LoadMoreResultsItem.VIEW_TYPE);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult") // handled by helper
|
||||
private void startMessageSearch(final String search) {
|
||||
if (swipeRefreshLayout != null) {
|
||||
swipeRefreshLayout.setRefreshing(true);
|
||||
}
|
||||
searchHelper
|
||||
.startMessageSearch(search)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
this::onMessageSearchResult,
|
||||
this::onMessageSearchError);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult") // handled by helper
|
||||
private void loadMoreMessages() {
|
||||
swipeRefreshLayout.setRefreshing(true);
|
||||
final Observable<MessageSearchHelper.MessageSearchResults> observable = searchHelper.loadMore();
|
||||
if (observable != null) {
|
||||
observable
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
this::onMessageSearchResult,
|
||||
this::onMessageSearchError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String getTitle() {
|
||||
return getResources().getString(R.string.nc_app_product_name);
|
||||
@ -871,38 +959,59 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
|
||||
@Override
|
||||
public boolean onItemClick(View view, int position) {
|
||||
try {
|
||||
selectedConversation = ((ConversationItem) Objects.requireNonNull(adapter.getItem(position))).getModel();
|
||||
|
||||
if (selectedConversation != null && getActivity() != null) {
|
||||
boolean hasChatPermission =
|
||||
new AttendeePermissionsUtil(selectedConversation.getPermissions()).hasChatPermission(currentUser);
|
||||
|
||||
if (showShareToScreen) {
|
||||
if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) {
|
||||
handleSharedData();
|
||||
showShareToScreen = false;
|
||||
} else {
|
||||
Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else if (forwardMessage) {
|
||||
if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) {
|
||||
openConversation(bundle.getString(BundleKeys.INSTANCE.getKEY_FORWARD_MSG_TEXT()));
|
||||
forwardMessage = false;
|
||||
} else {
|
||||
Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else {
|
||||
openConversation();
|
||||
}
|
||||
final AbstractFlexibleItem item = adapter.getItem(position);
|
||||
if (item != null) {
|
||||
final int viewType = item.getItemViewType();
|
||||
if (viewType == MessageResultItem.VIEW_TYPE) {
|
||||
MessageResultItem messageItem = (MessageResultItem) item;
|
||||
String conversationToken = messageItem.getMessageEntry().getConversationToken();
|
||||
selectedMessageId = messageItem.getMessageEntry().getMessageId();
|
||||
showConversationByToken(conversationToken);
|
||||
} else if (viewType == LoadMoreResultsItem.VIEW_TYPE) {
|
||||
loadMoreMessages();
|
||||
} else if (viewType == ConversationItem.VIEW_TYPE) {
|
||||
showConversation(((ConversationItem) Objects.requireNonNull(item)).getModel());
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
Log.w(TAG, "failed to cast clicked item to ConversationItem. Most probably a heading was clicked. This is" +
|
||||
" just ignored.", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showConversationByToken(String conversationToken) {
|
||||
for (AbstractFlexibleItem absItem : conversationItems) {
|
||||
ConversationItem conversationItem = ((ConversationItem) absItem);
|
||||
if (conversationItem.getModel().getToken().equals(conversationToken)) {
|
||||
final Conversation conversation = conversationItem.getModel();
|
||||
showConversation(conversation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showConversation(@Nullable final Conversation conversation) {
|
||||
selectedConversation = conversation;
|
||||
if (selectedConversation != null && getActivity() != null) {
|
||||
boolean hasChatPermission =
|
||||
new AttendeePermissionsUtil(selectedConversation.getPermissions()).hasChatPermission(currentUser);
|
||||
|
||||
if (showShareToScreen) {
|
||||
if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) {
|
||||
handleSharedData();
|
||||
showShareToScreen = false;
|
||||
} else {
|
||||
Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else if (forwardMessage) {
|
||||
if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) {
|
||||
openConversation(bundle.getString(BundleKeys.INSTANCE.getKEY_FORWARD_MSG_TEXT()));
|
||||
forwardMessage = false;
|
||||
} else {
|
||||
Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else {
|
||||
openConversation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean isReadOnlyConversation(Conversation conversation) {
|
||||
return conversation.getConversationReadOnlyState() ==
|
||||
Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY;
|
||||
@ -1085,6 +1194,10 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), selectedConversation.getToken());
|
||||
bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), selectedConversation.getRoomId());
|
||||
bundle.putString(BundleKeys.INSTANCE.getKEY_SHARED_TEXT(), textToPaste);
|
||||
if (selectedMessageId != null) {
|
||||
bundle.putString(BundleKeys.KEY_MESSAGE_ID, selectedMessageId);
|
||||
selectedMessageId = null;
|
||||
}
|
||||
|
||||
ConductorRemapping.INSTANCE.remapChatController(getRouter(), currentUser.getId(),
|
||||
selectedConversation.getToken(), bundle, false);
|
||||
@ -1274,4 +1387,33 @@ public class ConversationsListController extends BaseController implements Searc
|
||||
public AppBarLayoutType getAppBarLayoutType() {
|
||||
return AppBarLayoutType.SEARCH_BAR;
|
||||
}
|
||||
|
||||
public void onMessageSearchResult(@NonNull MessageSearchHelper.MessageSearchResults results) {
|
||||
if (searchView.getQuery().length() > 0) {
|
||||
clearMessageSearchResults();
|
||||
final List<SearchMessageEntry> entries = results.getMessages();
|
||||
if (entries.size() > 0) {
|
||||
List<AbstractFlexibleItem> adapterItems = new ArrayList<>(entries.size() + 1);
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
final boolean showHeader = i == 0;
|
||||
adapterItems.add(new MessageResultItem(context, currentUser, entries.get(i), showHeader));
|
||||
}
|
||||
if (results.getHasMore()) {
|
||||
adapterItems.add(LoadMoreResultsItem.INSTANCE);
|
||||
}
|
||||
adapter.addItems(0, adapterItems);
|
||||
recyclerView.scrollToPosition(0);
|
||||
}
|
||||
}
|
||||
if (swipeRefreshLayout != null) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void onMessageSearchError(@NonNull Throwable throwable) {
|
||||
handleHttpExceptions(throwable);
|
||||
if (swipeRefreshLayout != null) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,11 @@
|
||||
package com.nextcloud.talk.dagger.modules
|
||||
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
|
||||
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepositoryImpl
|
||||
import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository
|
||||
import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl
|
||||
import com.nextcloud.talk.utils.database.user.CurrentUserProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
@ -33,4 +36,9 @@ class RepositoryModule {
|
||||
fun provideSharedItemsRepository(ncApi: NcApi): SharedItemsRepository {
|
||||
return SharedItemsRepositoryImpl(ncApi)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideUnifiedSearchRepository(ncApi: NcApi, userProvider: CurrentUserProvider): UnifiedSearchRepository {
|
||||
return UnifiedSearchRepositoryImpl(ncApi, userProvider)
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ package com.nextcloud.talk.dagger.modules
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.nextcloud.talk.messagesearch.MessageSearchViewModel
|
||||
import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
|
||||
import dagger.Binds
|
||||
import dagger.MapKey
|
||||
@ -53,4 +54,9 @@ abstract class ViewModelModule {
|
||||
@IntoMap
|
||||
@ViewModelKey(SharedItemsViewModel::class)
|
||||
abstract fun sharedItemsViewModel(viewModel: SharedItemsViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(MessageSearchViewModel::class)
|
||||
abstract fun messageSearchViewModel(viewModel: MessageSearchViewModel): ViewModel
|
||||
}
|
||||
|
@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.messagesearch
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import autodagger.AutoInjector
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.activities.BaseActivity
|
||||
import com.nextcloud.talk.adapters.items.LoadMoreResultsItem
|
||||
import com.nextcloud.talk.adapters.items.MessageResultItem
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
import com.nextcloud.talk.controllers.ConversationsListController
|
||||
import com.nextcloud.talk.databinding.ActivityMessageSearchBinding
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||
import com.nextcloud.talk.utils.database.user.CurrentUserProvider
|
||||
import com.nextcloud.talk.utils.rx.SearchViewObservable.Companion.observeSearchView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class MessageSearchActivity : BaseActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
@Inject
|
||||
lateinit var userProvider: CurrentUserProvider
|
||||
|
||||
private lateinit var binding: ActivityMessageSearchBinding
|
||||
private lateinit var searchView: SearchView
|
||||
|
||||
private lateinit var user: UserEntity
|
||||
|
||||
private lateinit var viewModel: MessageSearchViewModel
|
||||
|
||||
private var searchViewDisposable: Disposable? = null
|
||||
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
binding = ActivityMessageSearchBinding.inflate(layoutInflater)
|
||||
setupActionBar()
|
||||
setupSystemColors()
|
||||
setContentView(binding.root)
|
||||
|
||||
viewModel = ViewModelProvider(this, viewModelFactory)[MessageSearchViewModel::class.java]
|
||||
user = userProvider.currentUser!!
|
||||
val roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!!
|
||||
viewModel.initialize(roomToken)
|
||||
setupStateObserver()
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
viewModel.refresh(searchView.query?.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupActionBar() {
|
||||
setSupportActionBar(binding.messageSearchToolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
val conversationName = intent.getStringExtra(BundleKeys.KEY_CONVERSATION_NAME)
|
||||
supportActionBar?.title = conversationName
|
||||
}
|
||||
|
||||
private fun setupSystemColors() {
|
||||
DisplayUtils.applyColorToStatusBar(
|
||||
this,
|
||||
ResourcesCompat.getColor(
|
||||
resources, R.color.appbar, null
|
||||
)
|
||||
)
|
||||
DisplayUtils.applyColorToNavigationBar(
|
||||
this.window,
|
||||
ResourcesCompat.getColor(resources, R.color.bg_default, null)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupStateObserver() {
|
||||
viewModel.state.observe(this) { state ->
|
||||
when (state) {
|
||||
MessageSearchViewModel.InitialState -> showInitial()
|
||||
MessageSearchViewModel.EmptyState -> showEmpty()
|
||||
is MessageSearchViewModel.LoadedState -> showLoaded(state)
|
||||
MessageSearchViewModel.LoadingState -> showLoading()
|
||||
MessageSearchViewModel.ErrorState -> showError()
|
||||
is MessageSearchViewModel.FinishedState -> onFinish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError() {
|
||||
displayLoading(false)
|
||||
Toast.makeText(this, "Error while searching", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun showLoading() {
|
||||
displayLoading(true)
|
||||
}
|
||||
|
||||
private fun displayLoading(loading: Boolean) {
|
||||
binding.swipeRefreshLayout.isRefreshing = loading
|
||||
}
|
||||
|
||||
private fun showLoaded(state: MessageSearchViewModel.LoadedState) {
|
||||
displayLoading(false)
|
||||
binding.emptyContainer.emptyListView.visibility = View.GONE
|
||||
binding.messageSearchRecycler.visibility = View.VISIBLE
|
||||
setAdapterItems(state)
|
||||
}
|
||||
|
||||
private fun setAdapterItems(state: MessageSearchViewModel.LoadedState) {
|
||||
val loadMoreItems = if (state.hasMore) {
|
||||
listOf(LoadMoreResultsItem)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val newItems =
|
||||
state.results.map { MessageResultItem(this, user, it) } + loadMoreItems
|
||||
|
||||
if (adapter != null) {
|
||||
adapter!!.updateDataSet(newItems)
|
||||
} else {
|
||||
createAdapter(newItems)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAdapter(items: List<AbstractFlexibleItem<out FlexibleViewHolder>>) {
|
||||
adapter = FlexibleAdapter(items)
|
||||
binding.messageSearchRecycler.adapter = adapter
|
||||
adapter!!.addListener(object : FlexibleAdapter.OnItemClickListener {
|
||||
override fun onItemClick(view: View?, position: Int): Boolean {
|
||||
val item = adapter!!.getItem(position)
|
||||
when (item?.itemViewType) {
|
||||
LoadMoreResultsItem.VIEW_TYPE -> {
|
||||
viewModel.loadMore()
|
||||
}
|
||||
MessageResultItem.VIEW_TYPE -> {
|
||||
val messageItem = item as MessageResultItem
|
||||
viewModel.selectMessage(messageItem.messageEntry)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun onFinish() {
|
||||
val state = viewModel.state.value
|
||||
if (state is MessageSearchViewModel.FinishedState) {
|
||||
val resultIntent = Intent().apply {
|
||||
putExtra(RESULT_KEY_MESSAGE_ID, state.selectedMessageId)
|
||||
}
|
||||
setResult(Activity.RESULT_OK, resultIntent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showInitial() {
|
||||
displayLoading(false)
|
||||
binding.messageSearchRecycler.visibility = View.GONE
|
||||
binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_typing)
|
||||
binding.emptyContainer.emptyListView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showEmpty() {
|
||||
displayLoading(false)
|
||||
binding.messageSearchRecycler.visibility = View.GONE
|
||||
binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_empty)
|
||||
binding.emptyContainer.emptyListView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_search, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
val menuItem = menu!!.findItem(R.id.action_search)
|
||||
searchView = menuItem.actionView as SearchView
|
||||
setupSearchView()
|
||||
menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
|
||||
searchView.requestFocus()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
|
||||
onBackPressed()
|
||||
return false
|
||||
}
|
||||
})
|
||||
menuItem.expandActionView()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setupSearchView() {
|
||||
searchView.queryHint = getString(R.string.message_search_hint)
|
||||
searchViewDisposable = observeSearchView(searchView)
|
||||
.debounce { query ->
|
||||
when {
|
||||
TextUtils.isEmpty(query) -> Observable.empty()
|
||||
else -> Observable.timer(
|
||||
ConversationsListController.SEARCH_DEBOUNCE_INTERVAL_MS.toLong(),
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { newText -> viewModel.onQueryTextChange(newText) }
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
searchViewDisposable?.dispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val RESULT_KEY_MESSAGE_ID = "MessageSearchActivity.result.message"
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.messagesearch
|
||||
|
||||
import android.util.Log
|
||||
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
||||
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
class MessageSearchHelper @JvmOverloads constructor(
|
||||
private val unifiedSearchRepository: UnifiedSearchRepository,
|
||||
private val fromRoom: String? = null
|
||||
) {
|
||||
|
||||
data class MessageSearchResults(val messages: List<SearchMessageEntry>, val hasMore: Boolean)
|
||||
|
||||
private var unifiedSearchDisposable: Disposable? = null
|
||||
private var previousSearch: String? = null
|
||||
private var previousCursor: Int = 0
|
||||
private var previousResults: List<SearchMessageEntry> = emptyList()
|
||||
|
||||
fun startMessageSearch(search: String): Observable<MessageSearchResults> {
|
||||
resetCachedData()
|
||||
return doSearch(search)
|
||||
}
|
||||
|
||||
fun loadMore(): Observable<MessageSearchResults>? {
|
||||
previousSearch?.let {
|
||||
return doSearch(it, previousCursor)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun cancelSearch() {
|
||||
disposeIfPossible()
|
||||
}
|
||||
|
||||
private fun doSearch(search: String, cursor: Int = 0): Observable<MessageSearchResults> {
|
||||
disposeIfPossible()
|
||||
return searchCall(search, cursor)
|
||||
.map { results ->
|
||||
previousSearch = search
|
||||
previousCursor = results.cursor
|
||||
previousResults = previousResults + results.entries
|
||||
MessageSearchResults(previousResults, results.hasMore)
|
||||
}
|
||||
.doOnSubscribe {
|
||||
unifiedSearchDisposable = it
|
||||
}
|
||||
.doOnError { throwable ->
|
||||
Log.e(TAG, "message search - ERROR", throwable)
|
||||
resetCachedData()
|
||||
disposeIfPossible()
|
||||
}
|
||||
.doOnComplete(this::disposeIfPossible)
|
||||
}
|
||||
|
||||
private fun searchCall(
|
||||
search: String,
|
||||
cursor: Int
|
||||
): Observable<UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>> {
|
||||
return when {
|
||||
fromRoom != null -> {
|
||||
unifiedSearchRepository.searchInRoom(
|
||||
roomToken = fromRoom,
|
||||
searchTerm = search,
|
||||
cursor = cursor
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
unifiedSearchRepository.searchMessages(
|
||||
searchTerm = search,
|
||||
cursor = cursor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetCachedData() {
|
||||
previousSearch = null
|
||||
previousCursor = 0
|
||||
previousResults = emptyList()
|
||||
}
|
||||
|
||||
private fun disposeIfPossible() {
|
||||
unifiedSearchDisposable?.dispose()
|
||||
unifiedSearchDisposable = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MessageSearchHelper::class.simpleName
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.messagesearch
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
||||
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Install PlantUML plugin to render this state diagram
|
||||
* @startuml
|
||||
* hide empty description
|
||||
* [*] --> InitialState
|
||||
* InitialState --> LoadingState
|
||||
* LoadingState --> EmptyState
|
||||
* LoadingState --> LoadedState
|
||||
* LoadingState --> LoadingState
|
||||
* LoadedState --> LoadingState
|
||||
* EmptyState --> LoadingState
|
||||
* LoadingState --> ErrorState
|
||||
* ErrorState --> LoadingState
|
||||
* @enduml
|
||||
*/
|
||||
class MessageSearchViewModel @Inject constructor(private val unifiedSearchRepository: UnifiedSearchRepository) :
|
||||
ViewModel() {
|
||||
|
||||
sealed class ViewState
|
||||
object InitialState : ViewState()
|
||||
object LoadingState : ViewState()
|
||||
object EmptyState : ViewState()
|
||||
object ErrorState : ViewState()
|
||||
class LoadedState(val results: List<SearchMessageEntry>, val hasMore: Boolean) : ViewState()
|
||||
class FinishedState(val selectedMessageId: String) : ViewState()
|
||||
|
||||
private lateinit var messageSearchHelper: MessageSearchHelper
|
||||
|
||||
private val _state: MutableLiveData<ViewState> = MutableLiveData(InitialState)
|
||||
val state: LiveData<ViewState>
|
||||
get() = _state
|
||||
|
||||
fun initialize(roomToken: String) {
|
||||
messageSearchHelper = MessageSearchHelper(unifiedSearchRepository, roomToken)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult") // handled by helper
|
||||
fun onQueryTextChange(newText: String) {
|
||||
if (newText.length >= MIN_CHARS_FOR_SEARCH) {
|
||||
_state.value = LoadingState
|
||||
messageSearchHelper.cancelSearch()
|
||||
messageSearchHelper.startMessageSearch(newText)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::onReceiveResults, this::onError)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult") // handled by helper
|
||||
fun loadMore() {
|
||||
_state.value = LoadingState
|
||||
messageSearchHelper.cancelSearch()
|
||||
messageSearchHelper.loadMore()
|
||||
?.subscribeOn(Schedulers.io())
|
||||
?.observeOn(AndroidSchedulers.mainThread())
|
||||
?.subscribe(this::onReceiveResults)
|
||||
}
|
||||
|
||||
private fun onReceiveResults(results: MessageSearchHelper.MessageSearchResults) {
|
||||
if (results.messages.isEmpty()) {
|
||||
_state.value = EmptyState
|
||||
} else {
|
||||
_state.value = LoadedState(results.messages, results.hasMore)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError(throwable: Throwable) {
|
||||
Log.e(TAG, "onError:", throwable)
|
||||
messageSearchHelper.cancelSearch()
|
||||
_state.value = ErrorState
|
||||
}
|
||||
|
||||
fun refresh(query: String?) {
|
||||
query?.let { onQueryTextChange(it) }
|
||||
}
|
||||
|
||||
fun selectMessage(messageEntry: SearchMessageEntry) {
|
||||
_state.value = FinishedState(messageEntry.messageId!!)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = MessageSearchViewModel::class.simpleName
|
||||
private const val MIN_CHARS_FOR_SEARCH = 2
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public abstract class CapabilitiesUtil {
|
||||
@ -38,7 +39,7 @@ public abstract class CapabilitiesUtil {
|
||||
public static boolean hasNotificationsCapability(@Nullable UserEntity user, String capabilityName) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
try {
|
||||
Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
if (capabilities.getNotificationsCapability() != null &&
|
||||
capabilities.getNotificationsCapability().getFeatures() != null) {
|
||||
return capabilities.getSpreedCapability().getFeatures().contains(capabilityName);
|
||||
@ -53,7 +54,7 @@ public abstract class CapabilitiesUtil {
|
||||
public static boolean hasExternalCapability(@Nullable UserEntity user, String capabilityName) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
try {
|
||||
Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
if (capabilities.getExternalCapability() != null &&
|
||||
capabilities.getExternalCapability().containsKey("v1")) {
|
||||
return capabilities.getExternalCapability().get("v1").contains(capabilityName);
|
||||
@ -82,7 +83,7 @@ public abstract class CapabilitiesUtil {
|
||||
public static boolean hasSpreedFeatureCapability(@Nullable UserEntity user, String capabilityName) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
try {
|
||||
Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
if (capabilities != null && capabilities.getSpreedCapability() != null &&
|
||||
capabilities.getSpreedCapability().getFeatures() != null) {
|
||||
return capabilities.getSpreedCapability().getFeatures().contains(capabilityName);
|
||||
@ -97,7 +98,7 @@ public abstract class CapabilitiesUtil {
|
||||
public static Integer getMessageMaxLength(@Nullable UserEntity user) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
try {
|
||||
Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
if (capabilities != null &&
|
||||
capabilities.getSpreedCapability() != null &&
|
||||
capabilities.getSpreedCapability().getConfig() != null &&
|
||||
@ -125,7 +126,7 @@ public abstract class CapabilitiesUtil {
|
||||
public static boolean isPhoneBookIntegrationAvailable(@Nullable UserEntity user) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
try {
|
||||
Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
return capabilities != null &&
|
||||
capabilities.getSpreedCapability() != null &&
|
||||
capabilities.getSpreedCapability().getFeatures() != null &&
|
||||
@ -140,7 +141,7 @@ public abstract class CapabilitiesUtil {
|
||||
public static boolean isReadStatusAvailable(@Nullable UserEntity user) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
try {
|
||||
Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
if (capabilities != null &&
|
||||
capabilities.getSpreedCapability() != null &&
|
||||
capabilities.getSpreedCapability().getConfig() != null &&
|
||||
@ -158,7 +159,7 @@ public abstract class CapabilitiesUtil {
|
||||
public static boolean isReadStatusPrivate(@Nullable UserEntity user) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
try {
|
||||
Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
if (capabilities != null &&
|
||||
capabilities.getSpreedCapability() != null &&
|
||||
capabilities.getSpreedCapability().getConfig() != null &&
|
||||
@ -178,7 +179,7 @@ public abstract class CapabilitiesUtil {
|
||||
public static boolean isUserStatusAvailable(@Nullable UserEntity user) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
try {
|
||||
Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
if (capabilities.getUserStatusCapability() != null &&
|
||||
capabilities.getUserStatusCapability().getEnabled() &&
|
||||
capabilities.getUserStatusCapability().getSupportsEmoji()) {
|
||||
@ -194,7 +195,7 @@ public abstract class CapabilitiesUtil {
|
||||
public static String getAttachmentFolder(@Nullable UserEntity user) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
try {
|
||||
Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
if (capabilities != null &&
|
||||
capabilities.getSpreedCapability() != null &&
|
||||
capabilities.getSpreedCapability().getConfig() != null &&
|
||||
@ -213,9 +214,8 @@ public abstract class CapabilitiesUtil {
|
||||
|
||||
public static String getServerName(@Nullable UserEntity user) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
Capabilities capabilities;
|
||||
try {
|
||||
capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
if (capabilities != null && capabilities.getThemingCapability() != null) {
|
||||
return capabilities.getThemingCapability().getName();
|
||||
}
|
||||
@ -229,9 +229,8 @@ public abstract class CapabilitiesUtil {
|
||||
// TODO later avatar can also be checked via user fields, for now it is in Talk capability
|
||||
public static boolean isAvatarEndpointAvailable(@Nullable UserEntity user) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
Capabilities capabilities;
|
||||
try {
|
||||
capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
return (capabilities != null &&
|
||||
capabilities.getSpreedCapability() != null &&
|
||||
capabilities.getSpreedCapability().getFeatures() != null &&
|
||||
@ -245,9 +244,8 @@ public abstract class CapabilitiesUtil {
|
||||
|
||||
public static boolean canEditScopes(@Nullable UserEntity user) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
Capabilities capabilities;
|
||||
try {
|
||||
capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
return (capabilities != null &&
|
||||
capabilities.getProvisioningCapability() != null &&
|
||||
capabilities.getProvisioningCapability().getAccountPropertyScopesVersion() != null &&
|
||||
@ -262,7 +260,7 @@ public abstract class CapabilitiesUtil {
|
||||
public static boolean isAbleToCall(@Nullable UserEntity user) {
|
||||
if (user != null && user.getCapabilities() != null) {
|
||||
try {
|
||||
Capabilities capabilities = LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
Capabilities capabilities = parseUserCapabilities(user);
|
||||
if (capabilities != null &&
|
||||
capabilities.getSpreedCapability() != null &&
|
||||
capabilities.getSpreedCapability().getConfig() != null &&
|
||||
@ -281,4 +279,12 @@ public abstract class CapabilitiesUtil {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Capabilities parseUserCapabilities(@NonNull final UserEntity user) throws IOException {
|
||||
return LoganSquare.parse(user.getCapabilities(), Capabilities.class);
|
||||
}
|
||||
|
||||
public static boolean isUnifiedSearchAvailable(@Nullable final UserEntity user) {
|
||||
return hasSpreedFeatureCapability(user, "unified-search");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.models.domain
|
||||
|
||||
data class SearchMessageEntry(
|
||||
val searchTerm: String,
|
||||
val thumbnailURL: String?,
|
||||
val title: String,
|
||||
val messageExcerpt: String,
|
||||
val conversationToken: String,
|
||||
val messageId: String?
|
||||
)
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.models.json.unifiedsearch
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bluelinelabs.logansquare.annotation.JsonField
|
||||
import com.bluelinelabs.logansquare.annotation.JsonObject
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@JsonObject
|
||||
data class UnifiedSearchEntry(
|
||||
@JsonField(name = ["thumbnailUrl"])
|
||||
var thumbnailUrl: String?,
|
||||
@JsonField(name = ["title"])
|
||||
var title: String?,
|
||||
@JsonField(name = ["subline"])
|
||||
var subline: String?,
|
||||
@JsonField(name = ["resourceUrl"])
|
||||
var resourceUrl: String?,
|
||||
@JsonField(name = ["icon"])
|
||||
var icon: String?,
|
||||
@JsonField(name = ["rounded"])
|
||||
var rounded: Boolean?,
|
||||
@JsonField(name = ["attributes"])
|
||||
var attributes: Map<String, String>?,
|
||||
) : Parcelable {
|
||||
constructor() : this(null, null, null, null, null, null, null)
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Marcel Hibbe
|
||||
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
*
|
||||
* 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.models.json.unifiedsearch
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bluelinelabs.logansquare.annotation.JsonField
|
||||
import com.bluelinelabs.logansquare.annotation.JsonObject
|
||||
import com.nextcloud.talk.models.json.generic.GenericMeta
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@JsonObject
|
||||
data class UnifiedSearchOCS(
|
||||
@JsonField(name = ["meta"])
|
||||
var meta: GenericMeta?,
|
||||
@JsonField(name = ["data"])
|
||||
var data: UnifiedSearchResponseData?
|
||||
) : Parcelable {
|
||||
// Empty constructor needed for JsonObject
|
||||
constructor() : this(null, null)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Marcel Hibbe
|
||||
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
*
|
||||
* 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.models.json.unifiedsearch
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bluelinelabs.logansquare.annotation.JsonField
|
||||
import com.bluelinelabs.logansquare.annotation.JsonObject
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@JsonObject
|
||||
data class UnifiedSearchOverall(
|
||||
@JsonField(name = ["ocs"])
|
||||
var ocs: UnifiedSearchOCS?
|
||||
) : Parcelable {
|
||||
// Empty constructor needed for JsonObject
|
||||
constructor() : this(null)
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.models.json.unifiedsearch
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bluelinelabs.logansquare.annotation.JsonField
|
||||
import com.bluelinelabs.logansquare.annotation.JsonObject
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@JsonObject
|
||||
data class UnifiedSearchResponseData(
|
||||
@JsonField(name = ["name"])
|
||||
var name: String?,
|
||||
@JsonField(name = ["isPaginated"])
|
||||
var paginated: Boolean?,
|
||||
@JsonField(name = ["entries"])
|
||||
var entries: List<UnifiedSearchEntry>?,
|
||||
@JsonField(name = ["cursor"])
|
||||
var cursor: Int?
|
||||
) : Parcelable {
|
||||
// empty constructor needed for JsonObject
|
||||
constructor() : this(null, null, null, null)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.nextcloud.talk.repositories.unifiedsearch
|
||||
|
||||
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
||||
import io.reactivex.Observable
|
||||
|
||||
interface UnifiedSearchRepository {
|
||||
data class UnifiedSearchResults<T>(
|
||||
val cursor: Int,
|
||||
val hasMore: Boolean,
|
||||
val entries: List<T>
|
||||
)
|
||||
|
||||
fun searchMessages(
|
||||
searchTerm: String,
|
||||
cursor: Int = 0,
|
||||
limit: Int = DEFAULT_PAGE_SIZE
|
||||
): Observable<UnifiedSearchResults<SearchMessageEntry>>
|
||||
|
||||
fun searchInRoom(
|
||||
roomToken: String,
|
||||
searchTerm: String,
|
||||
cursor: Int = 0,
|
||||
limit: Int = DEFAULT_PAGE_SIZE
|
||||
): Observable<UnifiedSearchResults<SearchMessageEntry>>
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_PAGE_SIZE = 5
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.repositories.unifiedsearch
|
||||
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
||||
import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchEntry
|
||||
import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchResponseData
|
||||
import com.nextcloud.talk.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.database.user.CurrentUserProvider
|
||||
import io.reactivex.Observable
|
||||
|
||||
class UnifiedSearchRepositoryImpl(private val api: NcApi, private val userProvider: CurrentUserProvider) :
|
||||
UnifiedSearchRepository {
|
||||
|
||||
private val userEntity: UserEntity
|
||||
get() = userProvider.currentUser!!
|
||||
|
||||
private val credentials: String
|
||||
get() = ApiUtils.getCredentials(userEntity.username, userEntity.token)
|
||||
|
||||
override fun searchMessages(
|
||||
searchTerm: String,
|
||||
cursor: Int,
|
||||
limit: Int
|
||||
): Observable<UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>> {
|
||||
val apiObservable = api.performUnifiedSearch(
|
||||
credentials,
|
||||
ApiUtils.getUrlForUnifiedSearch(userEntity.baseUrl, PROVIDER_TALK_MESSAGE),
|
||||
searchTerm,
|
||||
null,
|
||||
limit,
|
||||
cursor
|
||||
)
|
||||
return apiObservable.map { mapToMessageResults(it.ocs?.data!!, searchTerm, limit) }
|
||||
}
|
||||
|
||||
override fun searchInRoom(
|
||||
roomToken: String,
|
||||
searchTerm: String,
|
||||
cursor: Int,
|
||||
limit: Int
|
||||
): Observable<UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>> {
|
||||
val apiObservable = api.performUnifiedSearch(
|
||||
credentials,
|
||||
ApiUtils.getUrlForUnifiedSearch(userEntity.baseUrl, PROVIDER_TALK_MESSAGE_CURRENT),
|
||||
searchTerm,
|
||||
fromUrlForRoom(roomToken),
|
||||
limit,
|
||||
cursor
|
||||
)
|
||||
return apiObservable.map { mapToMessageResults(it.ocs?.data!!, searchTerm, limit) }
|
||||
}
|
||||
|
||||
private fun fromUrlForRoom(roomToken: String) = "/call/$roomToken"
|
||||
|
||||
companion object {
|
||||
private const val PROVIDER_TALK_MESSAGE = "talk-message"
|
||||
private const val PROVIDER_TALK_MESSAGE_CURRENT = "talk-message-current"
|
||||
|
||||
private const val ATTRIBUTE_CONVERSATION = "conversation"
|
||||
private const val ATTRIBUTE_MESSAGE_ID = "messageId"
|
||||
|
||||
private fun mapToMessageResults(data: UnifiedSearchResponseData, searchTerm: String, limit: Int):
|
||||
UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry> {
|
||||
val entries = data.entries?.map { it -> mapToMessage(it, searchTerm) }
|
||||
val cursor = data.cursor ?: 0
|
||||
val hasMore = entries?.size == limit
|
||||
return UnifiedSearchRepository.UnifiedSearchResults(cursor, hasMore, entries ?: emptyList())
|
||||
}
|
||||
|
||||
private fun mapToMessage(unifiedSearchEntry: UnifiedSearchEntry, searchTerm: String): SearchMessageEntry {
|
||||
val conversation = unifiedSearchEntry.attributes?.get(ATTRIBUTE_CONVERSATION)!!
|
||||
val messageId = unifiedSearchEntry.attributes?.get(ATTRIBUTE_MESSAGE_ID)
|
||||
return SearchMessageEntry(
|
||||
searchTerm,
|
||||
unifiedSearchEntry.thumbnailUrl,
|
||||
unifiedSearchEntry.title!!,
|
||||
unifiedSearchEntry.subline!!,
|
||||
conversation,
|
||||
messageId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -32,10 +32,11 @@ import com.nextcloud.talk.models.RetrofitBucket;
|
||||
import com.nextcloud.talk.models.database.CapabilitiesUtil;
|
||||
import com.nextcloud.talk.models.database.UserEntity;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.DimenRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import okhttp3.Credentials;
|
||||
|
||||
@ -456,4 +457,8 @@ public class ApiUtils {
|
||||
return baseUrl + ocsApiVersion + spreedApiVersion + "/reaction/" + roomToken + "/" + messageId;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String getUrlForUnifiedSearch(@NotNull String baseUrl, @NotNull String providerId) {
|
||||
return baseUrl + ocsApiVersion + "/search/providers/" + providerId + "/search";
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.graphics.drawable.VectorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
@ -592,6 +593,30 @@ public class DisplayUtils {
|
||||
avatarImageView.setController(draweeController);
|
||||
}
|
||||
|
||||
public static void loadAvatarPlaceholder(final SimpleDraweeView targetView) {
|
||||
final Context context = targetView.getContext();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Drawable[] layers = new Drawable[2];
|
||||
layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background);
|
||||
layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground);
|
||||
LayerDrawable layerDrawable = new LayerDrawable(layers);
|
||||
|
||||
targetView.getHierarchy().setPlaceholderImage(
|
||||
DisplayUtils.getRoundedDrawable(layerDrawable));
|
||||
} else {
|
||||
targetView.getHierarchy().setPlaceholderImage(R.mipmap.ic_launcher);
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadImage(final SimpleDraweeView targetView, final ImageRequest imageRequest) {
|
||||
final DraweeController newController = Fresco.newDraweeControllerBuilder()
|
||||
.setOldController(targetView.getController())
|
||||
.setAutoPlayAnimations(true)
|
||||
.setImageRequest(imageRequest)
|
||||
.build();
|
||||
targetView.setController(newController);
|
||||
}
|
||||
|
||||
public static @StringRes
|
||||
int getSortOrderStringId(FileSortOrder sortOrder) {
|
||||
switch (sortOrder.name) {
|
||||
|
@ -73,4 +73,5 @@ object BundleKeys {
|
||||
val KEY_FORWARD_MSG_TEXT = "KEY_FORWARD_MSG_TEXT"
|
||||
val KEY_FORWARD_HIDE_SOURCE_ROOM = "KEY_FORWARD_HIDE_SOURCE_ROOM"
|
||||
val KEY_SYSTEM_NOTIFICATION_ID = "KEY_SYSTEM_NOTIFICATION_ID"
|
||||
const val KEY_MESSAGE_ID = "KEY_MESSAGE_ID"
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.nextcloud.talk.utils.database.user
|
||||
|
||||
import com.nextcloud.talk.models.database.UserEntity
|
||||
|
||||
interface CurrentUserProvider {
|
||||
val currentUser: UserEntity?
|
||||
}
|
@ -17,28 +17,25 @@
|
||||
* 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.utils.database.user;
|
||||
package com.nextcloud.talk.utils.database.user
|
||||
|
||||
import autodagger.AutoInjector;
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication;
|
||||
import com.nextcloud.talk.dagger.modules.DatabaseModule;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import io.requery.Persistable;
|
||||
import io.requery.reactivex.ReactiveEntityStore;
|
||||
import com.nextcloud.talk.dagger.modules.DatabaseModule
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.requery.Persistable
|
||||
import io.requery.reactivex.ReactiveEntityStore
|
||||
|
||||
import javax.inject.Inject;
|
||||
@Module(includes = [DatabaseModule::class])
|
||||
abstract class UserModule {
|
||||
|
||||
@Module(includes = DatabaseModule.class)
|
||||
@AutoInjector(NextcloudTalkApplication.class)
|
||||
public class UserModule {
|
||||
@Binds
|
||||
abstract fun bindCurrentUserProvider(userUtils: UserUtils): CurrentUserProvider
|
||||
|
||||
@Inject
|
||||
public UserModule() {
|
||||
}
|
||||
|
||||
@Provides
|
||||
public UserUtils provideUserUtils(ReactiveEntityStore<Persistable> dataStore) {
|
||||
return new UserUtils(dataStore);
|
||||
companion object {
|
||||
@Provides
|
||||
fun provideUserUtils(dataStore: ReactiveEntityStore<Persistable?>?): UserUtils {
|
||||
return UserUtils(dataStore)
|
||||
}
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ import io.requery.Persistable;
|
||||
import io.requery.query.Result;
|
||||
import io.requery.reactivex.ReactiveEntityStore;
|
||||
|
||||
public class UserUtils {
|
||||
public class UserUtils implements CurrentUserProvider {
|
||||
private ReactiveEntityStore<Persistable> dataStore;
|
||||
|
||||
UserUtils(ReactiveEntityStore<Persistable> dataStore) {
|
||||
@ -83,6 +83,7 @@ public class UserUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable UserEntity getCurrentUser() {
|
||||
Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.CURRENT.eq(Boolean.TRUE)
|
||||
.and(UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE)))
|
||||
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.utils.rx
|
||||
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
|
||||
class SearchViewObservable {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun observeSearchView(searchView: SearchView): Observable<String> {
|
||||
val subject: PublishSubject<String> = PublishSubject.create()
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
subject.onComplete()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
subject.onNext(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
return subject
|
||||
}
|
||||
}
|
||||
}
|
72
app/src/main/res/layout/activity_message_search.xml
Normal file
72
app/src/main/res/layout/activity_message_search.xml
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Nextcloud Talk application
|
||||
~
|
||||
~ @author Álvaro Brey
|
||||
~ Copyright (C) 2022 Álvaro Brey <alvaro.brey@nextcloud.com>
|
||||
~ Copyright (C) 2022 Nextcloud GmbH
|
||||
~
|
||||
~ 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/>.
|
||||
-->
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bg_default"
|
||||
tools:ignore="Overdraw"
|
||||
tools:context=".messagesearch.MessageSearchActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/message_search_appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/message_search_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/appbar"
|
||||
android:theme="?attr/actionBarPopupTheme"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:navigationIconTint="@color/fontAppbar"
|
||||
app:popupTheme="@style/appActionBarPopupMenu"
|
||||
app:titleTextColor="@color/fontAppbar"
|
||||
tools:title="@string/nc_app_product_name"></com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/emptyContainer"
|
||||
layout="@layout/empty_list"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.nextcloud.talk.utils.FABAwareScrollingViewBehavior">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/message_search_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:listitem="@layout/rv_item_search_message" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -20,6 +20,7 @@
|
||||
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/empty_list_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -60,5 +61,7 @@
|
||||
android:paddingTop="@dimen/standard_half_padding"
|
||||
android:paddingBottom="@dimen/standard_half_padding"
|
||||
android:text=""
|
||||
tools:visibility="visible"
|
||||
tools:text="Empty list view text"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
62
app/src/main/res/layout/rv_item_load_more.xml
Normal file
62
app/src/main/res/layout/rv_item_load_more.xml
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Nextcloud Talk application
|
||||
~
|
||||
~ @author Mario Danic
|
||||
~ @author Andy Scherzinger
|
||||
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
~ Copyright (C) 2017-2018 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/>.
|
||||
~
|
||||
~
|
||||
~
|
||||
~ Adapted from https://github.com/stfalcon-studio/ChatKit/blob/master/chatkit/src/main/res/layout/item_dialog.xml
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/double_margin_between_elements"
|
||||
tools:background="@color/white">
|
||||
|
||||
<Space
|
||||
android:id="@+id/load_more_spacer"
|
||||
android:layout_width="@dimen/small_item_height"
|
||||
android:layout_height="@dimen/small_item_height"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:roundAsCircle="true" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/double_margin_between_elements"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/textColorMaxContrast"
|
||||
android:textSize="@dimen/two_line_primary_text_size"
|
||||
android:text="@string/load_more_results"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/load_more_spacer"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
80
app/src/main/res/layout/rv_item_search_message.xml
Normal file
80
app/src/main/res/layout/rv_item_search_message.xml
Normal file
@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Nextcloud Talk application
|
||||
~
|
||||
~ @author Mario Danic
|
||||
~ @author Andy Scherzinger
|
||||
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
~ Copyright (C) 2017-2018 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/>.
|
||||
~
|
||||
~
|
||||
~
|
||||
~ Adapted from https://github.com/stfalcon-studio/ChatKit/blob/master/chatkit/src/main/res/layout/item_dialog.xml
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/double_margin_between_elements"
|
||||
tools:background="@color/white">
|
||||
|
||||
<com.facebook.drawee.view.SimpleDraweeView
|
||||
android:id="@+id/thumbnail"
|
||||
android:layout_width="@dimen/small_item_height"
|
||||
android:layout_height="@dimen/small_item_height"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:roundAsCircle="true" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/conversation_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/double_margin_between_elements"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/conversation_item_header"
|
||||
android:textSize="@dimen/two_line_primary_text_size"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/thumbnail"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Message title goes here" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/message_excerpt"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="start|top"
|
||||
android:lines="1"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/textColorMaxContrast"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="@id/conversation_title"
|
||||
app:layout_constraintStart_toStartOf="@+id/conversation_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/conversation_title"
|
||||
tools:text="...this is a message result from unified search, which includes ellipses..." />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -35,15 +35,22 @@
|
||||
android:title="@string/nc_conversation_menu_video_call"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/conversation_search"
|
||||
android:icon="@drawable/ic_search_white_24dp"
|
||||
android:orderInCategory="2"
|
||||
android:title="@string/nc_search"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/conversation_info"
|
||||
android:orderInCategory="1"
|
||||
android:orderInCategory="3"
|
||||
android:title="@string/nc_conversation_menu_conversation_info"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/shared_items"
|
||||
android:orderInCategory="1"
|
||||
android:orderInCategory="4"
|
||||
android:title="@string/nc_shared_items"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
|
30
app/src/main/res/menu/menu_search.xml
Normal file
30
app/src/main/res/menu/menu_search.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Nextcloud Talk application
|
||||
~
|
||||
~ @author Álvaro Brey
|
||||
~ Copyright (C) 2022 Álvaro Brey
|
||||
~ Copyright (C) 2022 Nextcloud GmbH
|
||||
~
|
||||
~ 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 <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
android:icon="@drawable/ic_search_white_24dp"
|
||||
android:title="@string/nc_search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:showAsAction="always|collapseActionView" />
|
||||
</menu>
|
@ -273,14 +273,14 @@
|
||||
<string name="dnd">Do not disturb</string>
|
||||
<string name="away">Away</string>
|
||||
<string name="invisible">Invisible</string>
|
||||
<string translatable="false" name="divider">—</string>
|
||||
<string translatable="false" name="default_emoji">😃</string>
|
||||
<string translatable="false" name="emoji_thumbsUp">👍</string>
|
||||
<string translatable="false" name="emoji_thumbsDown">👎</string>
|
||||
<string translatable="false" name="emoji_heart">❤️</string>
|
||||
<string translatable="false" name="emoji_confused">😯</string>
|
||||
<string translatable="false" name="emoji_sad">😢</string>
|
||||
<string translatable="false" name="emoji_more">More emojis</string>
|
||||
<string name="divider" translatable="false">—</string>
|
||||
<string name="default_emoji" translatable="false">😃</string>
|
||||
<string name="emoji_thumbsUp" translatable="false">👍</string>
|
||||
<string name="emoji_thumbsDown" translatable="false">👎</string>
|
||||
<string name="emoji_heart" translatable="false">❤️</string>
|
||||
<string name="emoji_confused" translatable="false">😯</string>
|
||||
<string name="emoji_sad" translatable="false">😢</string>
|
||||
<string name="emoji_more" translatable="false">More emojis</string>
|
||||
<string name="dontClear">Don\'t clear</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="thirtyMinutes">30 minutes</string>
|
||||
@ -521,6 +521,13 @@
|
||||
<string name="shared_items_voice">Voice</string>
|
||||
<string name="shared_items_other">Other</string>
|
||||
|
||||
<!-- Message search -->
|
||||
<string name="messages">Messages</string>
|
||||
<string name="load_more_results">Load more results</string>
|
||||
<string name="message_search_hint">Search…</string>
|
||||
<string name="message_search_begin_typing">Start typing to search…</string>
|
||||
<string name="message_search_begin_empty">No search results</string>
|
||||
|
||||
<string name="title_attachments">Attachments</string>
|
||||
|
||||
<string name="reactions_tab_all">All</string>
|
||||
|
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.messagesearch
|
||||
|
||||
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
||||
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
|
||||
import com.nextcloud.talk.test.fakes.FakeUnifiedSearchRepository
|
||||
import io.reactivex.Observable
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
class MessageSearchHelperTest {
|
||||
|
||||
val repository = FakeUnifiedSearchRepository()
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun createMessageEntry(
|
||||
searchTerm: String = "foo",
|
||||
thumbnailURL: String = "foo",
|
||||
title: String = "foo",
|
||||
messageExcerpt: String = "foo",
|
||||
conversationToken: String = "foo",
|
||||
messageId: String? = "foo"
|
||||
) = SearchMessageEntry(searchTerm, thumbnailURL, title, messageExcerpt, conversationToken, messageId)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.openMocks(this)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptySearch() {
|
||||
repository.response = UnifiedSearchRepository.UnifiedSearchResults(0, false, emptyList())
|
||||
|
||||
val sut = MessageSearchHelper(repository)
|
||||
|
||||
val testObserver = sut.startMessageSearch("foo").test()
|
||||
testObserver.assertComplete()
|
||||
testObserver.assertValueCount(1)
|
||||
val expected = MessageSearchHelper.MessageSearchResults(emptyList(), false)
|
||||
testObserver.assertValue(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySearch_withMoreResults() {
|
||||
val entries = (1..5).map { createMessageEntry() }
|
||||
repository.response = UnifiedSearchRepository.UnifiedSearchResults(5, true, entries)
|
||||
|
||||
val sut = MessageSearchHelper(repository)
|
||||
|
||||
val observable = sut.startMessageSearch("foo")
|
||||
val expected = MessageSearchHelper.MessageSearchResults(entries, true)
|
||||
testCall(observable, expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySearch_withNoMoreResults() {
|
||||
val entries = (1..2).map { createMessageEntry() }
|
||||
repository.response = UnifiedSearchRepository.UnifiedSearchResults(2, false, entries)
|
||||
|
||||
val sut = MessageSearchHelper(repository)
|
||||
|
||||
val observable = sut.startMessageSearch("foo")
|
||||
val expected = MessageSearchHelper.MessageSearchResults(entries, false)
|
||||
testCall(observable, expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySearch_consecutiveSearches_sameResult() {
|
||||
val entries = (1..2).map { createMessageEntry() }
|
||||
repository.response = UnifiedSearchRepository.UnifiedSearchResults(2, false, entries)
|
||||
|
||||
val sut = MessageSearchHelper(repository)
|
||||
|
||||
repeat(5) {
|
||||
val observable = sut.startMessageSearch("foo")
|
||||
val expected = MessageSearchHelper.MessageSearchResults(entries, false)
|
||||
testCall(observable, expected)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadMore_noPreviousResults() {
|
||||
val sut = MessageSearchHelper(repository)
|
||||
Assert.assertEquals(null, sut.loadMore())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadMore_previousResults_sameSearch() {
|
||||
val sut = MessageSearchHelper(repository)
|
||||
|
||||
val firstPageEntries = (1..5).map { createMessageEntry() }
|
||||
repository.response = UnifiedSearchRepository.UnifiedSearchResults(5, true, firstPageEntries)
|
||||
|
||||
val firstPageObservable = sut.startMessageSearch("foo")
|
||||
Assert.assertEquals(0, repository.lastRequestedCursor)
|
||||
val firstPageExpected = MessageSearchHelper.MessageSearchResults(firstPageEntries, true)
|
||||
testCall(firstPageObservable, firstPageExpected)
|
||||
|
||||
val secondPageEntries = (1..5).map { createMessageEntry(title = "bar") }
|
||||
repository.response = UnifiedSearchRepository.UnifiedSearchResults(10, false, secondPageEntries)
|
||||
|
||||
val secondPageObservable = sut.loadMore()
|
||||
Assert.assertEquals(5, repository.lastRequestedCursor)
|
||||
Assert.assertNotNull(secondPageObservable)
|
||||
val secondPageExpected = MessageSearchHelper.MessageSearchResults(firstPageEntries + secondPageEntries, false)
|
||||
testCall(secondPageObservable!!, secondPageExpected)
|
||||
}
|
||||
|
||||
private fun testCall(
|
||||
searchCall: Observable<MessageSearchHelper.MessageSearchResults>,
|
||||
expectedResult: MessageSearchHelper.MessageSearchResults
|
||||
) {
|
||||
val testObserver = searchCall.test()
|
||||
testObserver.assertComplete()
|
||||
testObserver.assertValueCount(1)
|
||||
testObserver.assertValue(expectedResult)
|
||||
testObserver.dispose()
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Álvaro Brey
|
||||
* Copyright (C) 2022 Álvaro Brey
|
||||
* Copyright (C) 2022 Nextcloud GmbH
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.test.fakes
|
||||
|
||||
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
||||
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
|
||||
import io.reactivex.Observable
|
||||
|
||||
class FakeUnifiedSearchRepository : UnifiedSearchRepository {
|
||||
|
||||
lateinit var response: UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>
|
||||
var lastRequestedCursor = -1
|
||||
|
||||
override fun searchMessages(
|
||||
searchTerm: String,
|
||||
cursor: Int,
|
||||
limit: Int
|
||||
): Observable<UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>> {
|
||||
lastRequestedCursor = cursor
|
||||
return Observable.just(response)
|
||||
}
|
||||
|
||||
override fun searchInRoom(
|
||||
roomToken: String,
|
||||
searchTerm: String,
|
||||
cursor: Int,
|
||||
limit: Int
|
||||
): Observable<UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>> {
|
||||
lastRequestedCursor = cursor
|
||||
return Observable.just(response)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user