diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchResult.kt index 4529bf6d13..59d1c4d46d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/search/SearchResult.kt @@ -23,10 +23,17 @@ import org.matrix.android.sdk.api.session.events.model.Event * Domain class to represent the response of a search request in a room. */ data class SearchResult( - // Token that can be used to get the next batch of results, by passing as the next_batch parameter to the next call. If this field is absent, there are no more results. + /** + * Token that can be used to get the next batch of results, by passing as the next_batch parameter to the next call. + * If this field is null, there are no more results. + */ val nextBatch: String? = null, - // List of words which should be highlighted, useful for stemming which may change the query terms. - var highlights: List? = null, - // List of results in the requested order. - var results: List? = null + /** + * List of words which should be highlighted, useful for stemming which may change the query terms. + */ + val highlights: List? = null, + /** + * List of results in the requested order. + */ + val results: List? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index 34b6be225b..d1c0844caa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -96,6 +96,7 @@ internal class DefaultSession @Inject constructor( private val pushRuleService: Lazy, private val pushersService: Lazy, private val termsService: Lazy, + private val searchService: Lazy, private val cryptoService: Lazy, private val defaultFileService: Lazy, private val permalinkService: Lazy, @@ -121,8 +122,7 @@ internal class DefaultSession @Inject constructor( private val taskExecutor: TaskExecutor, private val callSignalingService: Lazy, @UnauthenticatedWithCertificate - private val unauthenticatedWithCertificateOkHttpClient: Lazy, - private val searchService: Lazy + private val unauthenticatedWithCertificateOkHttpClient: Lazy ) : Session, RoomService by roomService.get(), RoomDirectoryService by roomDirectoryService.get(), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchAPI.kt index 3d5dbc306f..a903cd8cc8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchAPI.kt @@ -28,9 +28,9 @@ internal interface SearchAPI { /** * Performs a full text search across different categories. + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-search */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "search") - fun search( - @Query("next_batch") nextBatch: String?, - @Body body: SearchRequestBody): Call + fun search(@Query("next_batch") nextBatch: String?, + @Body body: SearchRequestBody): Call } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt index a2cad6bb2f..3bb65fb8da 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/SearchTask.kt @@ -69,14 +69,14 @@ internal class DefaultSearchTask @Inject constructor( ) ) apiCall = searchAPI.search(params.nextBatch, searchRequestBody) - }.toDomain().apply { results = results?.reversed() } + }.toDomain() } private fun SearchResponse.toDomain(): SearchResult { return SearchResult( nextBatch = searchCategories.roomEvents?.nextBatch, highlights = searchCategories.roomEvents?.highlights, - results = searchCategories.roomEvents?.results?.map { it.event } + results = searchCategories.roomEvents?.results?.map { it.event }?.reversed() ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestBody.kt index 000f89e751..7cf86457d6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestBody.kt @@ -22,6 +22,9 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class SearchRequestBody( + /** + * Required. Describes which categories to search in and their criteria. + */ @Json(name = "search_categories") val searchCategories: SearchRequestCategories ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestCategories.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestCategories.kt index 606b7320e5..9d87114237 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestCategories.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestCategories.kt @@ -22,7 +22,9 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class SearchRequestCategories( - // Mapping of category name to search criteria. + /** + * Mapping of category name to search criteria. + */ @Json(name = "room_events") val roomEvents: SearchRequestRoomEvents? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestRoomEvents.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestRoomEvents.kt index fef1baa990..c5234b1052 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestRoomEvents.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/request/SearchRequestRoomEvents.kt @@ -22,15 +22,44 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class SearchRequestRoomEvents( - // Required. The string to search events for. + /** + * Required. The string to search events for. + */ @Json(name = "search_term") val searchTerm: String, + + /** + * The keys to search. Defaults to all. One of: ["content.body", "content.name", "content.topic"] + */ + @Json(name = "keys") + val keys: Any? = null, + + /** + * This takes a filter. + */ @Json(name = "filter") val filter: SearchRequestFilter? = null, - // By default, this is "rank". One of: ["recent", "rank"] + + /** + * The order in which to search for results. By default, this is "rank". One of: ["recent", "rank"] + */ @Json(name = "order_by") val orderBy: SearchRequestOrder? = null, - // Configures whether any context for the events returned are included in the response. + + /** + * Configures whether any context for the events returned are included in the response. + */ @Json(name = "event_context") - val eventContext: SearchRequestEventContext? = null + val eventContext: SearchRequestEventContext? = null, + + /** + * Requests the server return the current state for each room returned. + */ + @Json(name = "include_state") + val include_state: Boolean? = null + + /** + * Requests that the server partitions the result set based on the provided list of keys. + */ + // val groupings: SearchRequestGroupings? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponse.kt index b7debb6b1e..8e11ba1468 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponse.kt @@ -21,8 +21,10 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class SearchResponse( - // Required. Describes which categories to search in and their criteria. +internal data class SearchResponse( + /** + * Required. Describes which categories to search in and their criteria. + */ @Json(name = "search_categories") val searchCategories: SearchResponseCategories ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseCategories.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseCategories.kt index c931b23826..8e46502e27 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseCategories.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseCategories.kt @@ -21,7 +21,10 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class SearchResponseCategories( +internal data class SearchResponseCategories( + /** + * Mapping of category name to search criteria. + */ @Json(name = "room_events") val roomEvents: SearchResponseRoomEvents? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseItem.kt index 7c675f0593..5b0806ce85 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseItem.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseItem.kt @@ -22,11 +22,22 @@ import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Event @JsonClass(generateAdapter = true) -data class SearchResponseItem( - // A number that describes how closely this result matches the search. Higher is closer. +internal data class SearchResponseItem( + /** + * A number that describes how closely this result matches the search. Higher is closer. + */ @Json(name = "rank") val rank: Double? = null, - // The event that matched. + + /** + * The event that matched. + */ @Json(name = "result") - val event: Event + val event: Event, + + /** + * Context for result, if requested. + */ + @Json(name = "context") + val context: SearchResponseEventContext? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseRoomEvents.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseRoomEvents.kt index 61ed2bf3cf..1078b9a9f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseRoomEvents.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/search/response/SearchResponseRoomEvents.kt @@ -21,7 +21,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -class SearchResponseRoomEvents( +internal data class SearchResponseRoomEvents( // List of results in the requested order. @Json(name = "results") val results: List? = null, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt index 9f024f50f3..ea7ba8f464 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchFragment.kt @@ -46,7 +46,7 @@ data class SearchArgs( class SearchFragment @Inject constructor( val viewModelFactory: SearchViewModel.Factory, - val controller: SearchResultController + private val controller: SearchResultController ) : VectorBaseFragment(), StateView.EventCallback, SearchResultController.Listener { private val fragmentArgs: SearchArgs by args() @@ -85,13 +85,13 @@ class SearchFragment @Inject constructor( } override fun invalidate() = withState(searchViewModel) { state -> - if (state.searchResult?.results.isNullOrEmpty()) { - when (state.asyncEventsRequest) { + if (state.searchResult.isNullOrEmpty()) { + when (state.asyncSearchRequest) { is Loading -> { stateView.state = StateView.State.Loading } is Fail -> { - stateView.state = StateView.State.Error(errorFormatter.toHumanReadable(state.asyncEventsRequest.error)) + stateView.state = StateView.State.Error(errorFormatter.toHumanReadable(state.asyncSearchRequest.error)) } is Success -> { stateView.state = StateView.State.Empty( @@ -100,7 +100,7 @@ class SearchFragment @Inject constructor( } } } else { - val lastBatchSize = state.lastBatch?.results?.size ?: 0 + val lastBatchSize = state.lastBatch?.size ?: 0 pendingScrollToPosition = if (lastBatchSize > 0) lastBatchSize - 1 else 0 stateView.state = StateView.State.Content diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt index cf79ed0ea6..8daa4f60e4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultController.kt @@ -48,7 +48,9 @@ class SearchResultController @Inject constructor( } override fun buildModels(data: SearchViewState?) { - if (!data?.searchResult?.nextBatch.isNullOrEmpty()) { + data ?: return + + if (data.hasMoreResult) { loadingItem { // Always use a different id, because we can be notified several times of visibility state changed id("loadMore${idx++}") @@ -60,7 +62,7 @@ class SearchResultController @Inject constructor( } } - buildSearchResultItems(data?.searchResult?.results.orEmpty()) + buildSearchResultItems(data.searchResult.orEmpty()) } private fun buildSearchResultItems(events: List) { @@ -83,12 +85,9 @@ class SearchResultController @Inject constructor( avatarRenderer(avatarRenderer) dateFormatter(dateFormatter) event(event) + // I think we should use the data returned by the server? sender(event.senderId?.let { session.getUser(it) }) - listener(object : SearchResultItem.Listener { - override fun onItemClicked() { - listener?.onItemClicked(event) - } - }) + listener { listener?.onItemClicked(event) } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt index fb0121cbcf..2d569c1c6a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchResultItem.kt @@ -23,8 +23,10 @@ import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.user.model.User @@ -33,21 +35,21 @@ import org.matrix.android.sdk.api.util.toMatrixItem @EpoxyModelClass(layout = R.layout.item_search_result) abstract class SearchResultItem : VectorEpoxyModel() { - @EpoxyAttribute var avatarRenderer: AvatarRenderer? = null + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute var dateFormatter: VectorDateFormatter? = null - @EpoxyAttribute var event: Event? = null + @EpoxyAttribute lateinit var event: Event @EpoxyAttribute var sender: User? = null - @EpoxyAttribute var listener: Listener? = null + @EpoxyAttribute var listener: ClickListener? = null override fun bind(holder: Holder) { super.bind(holder) - event ?: return - holder.view.setOnClickListener { listener?.onItemClicked() } - sender?.toMatrixItem()?.let { avatarRenderer?.render(it, holder.avatarImageView) } - holder.memberNameView.text = sender?.displayName - holder.timeView.text = dateFormatter?.format(event!!.originServerTs, DateFormatKind.MESSAGE_SIMPLE) - holder.contentView.text = event?.content?.get("body") as? String + holder.view.onClick(listener) + sender?.toMatrixItem()?.let { avatarRenderer.render(it, holder.avatarImageView) } + holder.memberNameView.text = sender?.getBestName() + holder.timeView.text = dateFormatter?.format(event.originServerTs, DateFormatKind.MESSAGE_SIMPLE) + // TODO Improve that (use formattedBody, etc.) + holder.contentView.text = event.content?.get("body") as? String } class Holder : VectorEpoxyHolder() { @@ -56,8 +58,4 @@ abstract class SearchResultItem : VectorEpoxyModel() { val timeView by bind(R.id.messageTimeView) val contentView by bind(R.id.messageContentView) } - - interface Listener { - fun onItemClicked() - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt index e9cca86e30..6703815f57 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewModel.kt @@ -37,15 +37,14 @@ import org.matrix.android.sdk.internal.util.awaitCallback class SearchViewModel @AssistedInject constructor( @Assisted private val initialState: SearchViewState, - private val session: Session + session: Session ) : VectorViewModel(initialState) { - private var room: Room? = null + private var room: Room? = session.getRoom(initialState.roomId) + private var currentTask: Cancelable? = null - init { - room = initialState.roomId?.let { session.getRoom(it) } - } + private var nextBatch: String? = null @AssistedInject.Factory interface Factory { @@ -64,8 +63,8 @@ class SearchViewModel @AssistedInject constructor( override fun handle(action: SearchAction) { when (action) { is SearchAction.SearchWith -> handleSearchWith(action) - is SearchAction.LoadMore -> handleLoadMore() - is SearchAction.Retry -> handleRetry() + is SearchAction.LoadMore -> handleLoadMore() + is SearchAction.Retry -> handleRetry() }.exhaustive } @@ -74,7 +73,7 @@ class SearchViewModel @AssistedInject constructor( setState { copy(searchTerm = action.searchTerm) } - startSearching() + startSearching(false) } } @@ -83,25 +82,25 @@ class SearchViewModel @AssistedInject constructor( } private fun handleRetry() { - startSearching() + startSearching(false) } - private fun startSearching(isNextBatch: Boolean = false) = withState { state -> - if (state.roomId == null || state.searchTerm == null) return@withState + private fun startSearching(isNextBatch: Boolean) = withState { state -> + if (state.searchTerm == null) return@withState // There is no batch to retrieve - if (isNextBatch && state.searchResult?.nextBatch == null) return@withState + if (isNextBatch && nextBatch == null) return@withState // Show full screen loading just for the clean search if (!isNextBatch) { setState { copy( - asyncEventsRequest = Loading() + asyncSearchRequest = Loading() ) } } - if (state.asyncEventsRequest is Loading) { + if (state.asyncSearchRequest is Loading) { currentTask?.cancel() } @@ -110,7 +109,7 @@ class SearchViewModel @AssistedInject constructor( val result = awaitCallback { currentTask = room?.search( searchTerm = state.searchTerm, - nextBatch = state.searchResult?.nextBatch, + nextBatch = nextBatch, orderByRecent = true, beforeLimit = 0, afterLimit = 0, @@ -126,7 +125,7 @@ class SearchViewModel @AssistedInject constructor( _viewEvents.post(SearchViewEvents.Failure(failure)) setState { copy( - asyncEventsRequest = Fail(failure), + asyncSearchRequest = Fail(failure), searchResult = null ) } @@ -135,27 +134,19 @@ class SearchViewModel @AssistedInject constructor( } private fun onSearchResultSuccess(searchResult: SearchResult, isNextBatch: Boolean) = withState { state -> - val accumulatedResult = SearchResult( - nextBatch = searchResult.nextBatch, - results = searchResult.results, - highlights = searchResult.highlights - ) - // Accumulate results if it is the next batch - if (isNextBatch) { - if (state.searchResult != null) { - accumulatedResult.results = accumulatedResult.results?.plus(state.searchResult.results!!) - } - if (state.searchResult?.highlights != null) { - accumulatedResult.highlights = accumulatedResult.highlights?.plus(state.searchResult.highlights!!) - } - } + val accumulatedResult = searchResult.results.orEmpty().plus(state.searchResult?.takeIf { isNextBatch }.orEmpty()) + + // Note: We do not care about the highlights for the moment, but it will be the same algorithm + + nextBatch = searchResult.nextBatch setState { copy( searchResult = accumulatedResult, - lastBatch = searchResult, - asyncEventsRequest = Success(Unit) + hasMoreResult = !nextBatch.isNullOrEmpty(), + lastBatch = searchResult.results, + asyncSearchRequest = Success(Unit) ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt index 3873769c44..72da1ca940 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/search/SearchViewState.kt @@ -19,17 +19,18 @@ package im.vector.app.features.home.room.detail.search import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized -import org.matrix.android.sdk.api.session.search.SearchResult +import org.matrix.android.sdk.api.session.events.model.Event data class SearchViewState( // Accumulated search result - val searchResult: SearchResult? = null, + val searchResult: List? = null, + val hasMoreResult: Boolean = false, // Last batch result will help RecyclerView to position itself - val lastBatch: SearchResult? = null, + val lastBatch: List? = null, val searchTerm: String? = null, - val roomId: String? = null, + val roomId: String = "", // Current pagination request - val asyncEventsRequest: Async = Uninitialized + val asyncSearchRequest: Async = Uninitialized ) : MvRxState { constructor(args: SearchArgs) : this(roomId = args.roomId) diff --git a/vector/src/main/res/layout/activity_search.xml b/vector/src/main/res/layout/activity_search.xml index a3aa21eecc..2268bf932c 100644 --- a/vector/src/main/res/layout/activity_search.xml +++ b/vector/src/main/res/layout/activity_search.xml @@ -24,8 +24,8 @@ style="@style/VectorSearchView" android:layout_width="match_parent" android:layout_height="wrap_content" - app:queryHint="@string/search_hint" - android:backgroundTint="@color/base_color" /> + android:backgroundTint="@color/base_color" + app:queryHint="@string/search_hint" /> diff --git a/vector/src/main/res/layout/fragment_search.xml b/vector/src/main/res/layout/fragment_search.xml index 757168850b..330e70d86b 100644 --- a/vector/src/main/res/layout/fragment_search.xml +++ b/vector/src/main/res/layout/fragment_search.xml @@ -1,5 +1,6 @@ @@ -8,6 +9,7 @@ android:id="@+id/searchResultRecycler" android:layout_width="match_parent" android:layout_height="match_parent" - android:overScrollMode="always" /> + android:overScrollMode="always" + tools:listitem="@layout/item_search_result" /> \ No newline at end of file