Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
Release 2.4.0
  • Loading branch information
aallam committed Jun 4, 2020
2 parents 6fcab13 + c36523b commit 25415a8
Show file tree
Hide file tree
Showing 25 changed files with 371 additions and 21 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
# 2.4.0

## Added
- Related items widget
- Connecting `SortByViewModel` to `PagedList`

### Changed
- Kotlin `1.3.72`
- Android Gradle Plugin `3.6.3`
- Algolia Kotlin Client `1.4.0` (#198)

### Fixed
- Apply correct spans in `toSpannedString` extension function

# 2.3.1

- Fix PagedList Bug when typing too fast #194

# 2.3.0

- Use Kotlin client `1.3.1`
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @q-litzler @PLNech
* @spinach @Aallam
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ Have a look at our [widget showcase][showcase-url] to see concrete examples of a

You can add InstantSearch to your Android application by adding the following line to your `build.gradle`'s dependencies.
```groovy
implementation "com.algolia:instantsearch-android:2.0.1"
implementation "com.algolia:instantsearch-android:$instantsearch_version"
```
<!--TODO Document using helper-jvm / using core directly -->

⚠️ Important: starting from version `2.4.0`, the library is compatible only with kotlin version `1.3.70` or higher; for previous versions of kotlin, please use version `2.3.1` of the library.

See the [documentation][doc]. You can start with the [Getting Started Guide][getting-started].

# Contributing
Expand Down
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ buildscript {
}
dependencies {
classpath(dependency.script.AndroidTools())
classpath(kotlin("gradle-plugin", version = "1.3.60"))
classpath(kotlin("serialization", version = "1.3.60"))
classpath(kotlin("gradle-plugin", version = "1.3.72"))
classpath(kotlin("serialization", version = "1.3.72"))
classpath("com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4")
}
}
Expand Down
4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/dependency/Library.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ object Library: Dependency {

override val group = "com.algolia"
override val artifact = "instantsearch"
override val version = "2.3.1"
override val version = "2.4.0"

val packageName = "$group:$artifact-android"

Expand All @@ -17,4 +17,4 @@ object Library: Dependency {
val artifactHelperCommon = "$artifactHelper-common"
val artifactHelperJvm = "$artifactHelper-jvm"
val artifactHelperAndroid = "$artifact-android"
}
}
4 changes: 2 additions & 2 deletions buildSrc/src/main/kotlin/dependency/network/AlgoliaClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ object AlgoliaClient : Dependency {

override val group = "com.algolia"
override val artifact = "algoliasearch-client-kotlin"
override val version = "1.3.1"
}
override val version = "1.4.0"
}
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/dependency/script/AndroidTools.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ object AndroidTools : Dependency {

override val group = "com.android.tools.build"
override val artifact = "gradle"
override val version = "3.5.2"
override val version = "3.6.3"
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,27 @@ package com.algolia.instantsearch.helper.android.highlighting
import android.graphics.Typeface
import android.text.ParcelableSpan
import android.text.SpannedString
import android.text.style.CharacterStyle
import android.text.style.StyleSpan
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import com.algolia.instantsearch.core.highlighting.HighlightedString


public fun HighlightedString.toSpannedString(
span: ParcelableSpan = StyleSpan(Typeface.BOLD)
): SpannedString = buildSpannedString {
tokens.forEach { (part, isHighlighted) ->
if (isHighlighted) inSpans(span) { append(part) }
if (isHighlighted) inSpans(span.wrap()) { append(part) }
else append(part)
}
}

/**
* A given [CharacterStyle] can only applied to a single region of a given Spanned.
* This method wraps a [ParcelableSpan] with a new object (if it is a [CharacterStyle]) that will have the same effect.
*/
internal fun ParcelableSpan.wrap(): Any = if (this is CharacterStyle) CharacterStyle.wrap(this) else this

public fun List<HighlightedString>.toSpannedString(
span: ParcelableSpan = StyleSpan(Typeface.BOLD)
): SpannedString {
Expand All @@ -27,4 +33,4 @@ public fun List<HighlightedString>.toSpannedString(
append(spanned)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.algolia.instantsearch.helper.android.sortby

import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.algolia.instantsearch.core.connection.Connection
import com.algolia.instantsearch.helper.sortby.SortByConnector
import com.algolia.instantsearch.helper.sortby.SortByViewModel

public fun <T> SortByViewModel.connectPagedList(pagedList: LiveData<PagedList<T>>): Connection {
return SortByConnectionPagedList(this, pagedList)
}

public fun <T> SortByConnector.connectPagedList(pagedList: LiveData<PagedList<T>>): Connection {
return viewModel.connectPagedList(pagedList)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.algolia.instantsearch.helper.android.sortby

import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.algolia.instantsearch.core.Callback
import com.algolia.instantsearch.core.connection.ConnectionImpl
import com.algolia.instantsearch.helper.sortby.SortByViewModel

internal class SortByConnectionPagedList<T>(
private val viewModel: SortByViewModel,
private val pagedList: LiveData<PagedList<T>>
) : ConnectionImpl() {

private val onSelection: Callback<Int?> = {
pagedList.value?.dataSource?.invalidate()
}

override fun connect() {
super.connect()
viewModel.eventSelection.subscribe(onSelection)
}

override fun disconnect() {
super.disconnect()
viewModel.eventSelection.unsubscribe(onSelection)
}
}
13 changes: 7 additions & 6 deletions helper/src/androidTest/kotlin/highlighting/TestExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.algolia.instantsearch.core.highlighting.HighlightTokenizer
import com.algolia.instantsearch.helper.android.highlighting.toSpannedString
import com.algolia.instantsearch.helper.android.highlighting.wrap
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
Expand All @@ -26,7 +27,7 @@ import shouldEqual
class TestExtensions {

private val tokenizer = HighlightTokenizer("[", "]")
private val highlightStrings = listOf("foo[ba]r", "foo[ba]r[ba]z")
private val highlightStrings = listOf("foo[ba]r", "foo[ba]r[ba]z") // 3 spans
private val highlights = highlightStrings.map(tokenizer)
private val defaultSpan = StyleSpan(Typeface.BOLD)
private val customSpan = ForegroundColorSpan(Color.RED)
Expand All @@ -35,13 +36,13 @@ class TestExtensions {
return listOf(
buildSpannedString {
append("foo")
inSpans(span) { append("ba") }
inSpans(span.wrap()) { append("ba") }
append("r")
}, buildSpannedString {
append("foo")
inSpans(span) { append("ba") }
inSpans(span.wrap()) { append("ba") }
append("r")
inSpans(span) { append("ba") }
inSpans(span.wrap()) { append("ba") }
append("z")
}
)
Expand Down Expand Up @@ -81,10 +82,10 @@ class TestExtensions {
val expectedSpannedStrings = expectedSpannedStrings(customSpan)

tested.toString() shouldEqual expectedSpannedStrings.joinToString() // Built strings are the same
tested.getSpans<Any>().size shouldEqual 1 // and tested does still have its span
tested.getSpans<Any>().size shouldEqual 3 // and tested does still have its span
}

private inline fun <reified T : Any> SpannedString.getSpans(): Array<out T> {
return getSpans(0, length)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.algolia.instantsearch.helper.relateditems

import com.algolia.search.model.Attribute
import kotlin.reflect.KProperty1

/**
* Representation of a scored filter based on a hit attribute.
*
* @param attribute hit's attribute
* @param score filter score
* @param property hit's property
*/
public data class MatchingPattern<T>(
val attribute: Attribute,
val score: Int,
val property: KProperty1<T, *>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.algolia.instantsearch.helper.relateditems

import com.algolia.instantsearch.core.Presenter
import com.algolia.instantsearch.core.connection.Connection
import com.algolia.instantsearch.core.hits.HitsView
import com.algolia.instantsearch.helper.relateditems.internal.RelatedItemsConnectionView
import com.algolia.instantsearch.helper.searcher.SearcherSingleIndex
import com.algolia.search.model.indexing.Indexable
import com.algolia.search.model.response.ResponseSearch

/**
* Connects [SearcherSingleIndex] to [HitsView] to display related items.
*
* @param adapter hits views adapter
* @param hit hit to get its related items
* @param matchingPatterns list of matching patterns that create scored filters based on the hit’s attributes
* @param presenter presentation output and format
*/
public fun <T> SearcherSingleIndex.connectRelatedHitsView(
adapter: HitsView<T>,
hit: T,
matchingPatterns: List<MatchingPattern<T>>,
presenter: Presenter<ResponseSearch, List<T>>
): Connection where T : Indexable {
return RelatedItemsConnectionView(this, adapter, hit, matchingPatterns, presenter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.algolia.instantsearch.helper.relateditems.internal

import com.algolia.instantsearch.helper.filter.state.FilterGroupID
import com.algolia.search.model.filter.Filter

internal class FilterFacetAndID(
val filterGroupID: FilterGroupID,
val filterFacets: Array<Filter.Facet>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.algolia.instantsearch.helper.relateditems.internal

import com.algolia.instantsearch.core.Callback
import com.algolia.instantsearch.core.Presenter
import com.algolia.instantsearch.core.connection.ConnectionImpl
import com.algolia.instantsearch.core.hits.HitsView
import com.algolia.instantsearch.helper.relateditems.MatchingPattern
import com.algolia.instantsearch.helper.relateditems.internal.extensions.configureRelatedItems
import com.algolia.instantsearch.helper.searcher.SearcherSingleIndex
import com.algolia.search.model.indexing.Indexable
import com.algolia.search.model.response.ResponseSearch

internal data class RelatedItemsConnectionView<T>(
private val searcher: SearcherSingleIndex,
private val view: HitsView<T>,
private val hit: T,
private val matchingPatterns: List<MatchingPattern<T>>,
private val presenter: Presenter<ResponseSearch, List<T>>
) : ConnectionImpl() where T : Indexable {

init {
searcher.configureRelatedItems(hit, matchingPatterns)
}

private val callback: Callback<ResponseSearch?> = { response ->
if (response != null) {
view.setHits(presenter(response))
}
}

override fun connect() {
super.connect()
searcher.response.subscribe(callback)
}

override fun disconnect() {
super.disconnect()
searcher.response.unsubscribe(callback)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.algolia.instantsearch.helper.relateditems.internal.extensions

import com.algolia.instantsearch.helper.filter.state.FilterOperator
import com.algolia.instantsearch.helper.filter.state.FilterState
import com.algolia.instantsearch.helper.relateditems.MatchingPattern
import com.algolia.search.model.filter.Filter
import com.algolia.search.model.filter.FilterGroup

internal fun <T> FilterState.addMatchingPattern(hit: T, matchingPattern: MatchingPattern<T>) {
val optionalFilter = matchingPattern.toOptionalFilter(hit)
optionalFilter?.let {
add(it.filterGroupID, *it.filterFacets)
}
}

internal fun FilterState.toFilterFacetGroup(): Set<FilterGroup<Filter.Facet>> {
return getFacetGroups().map { (key, value) ->
when (key.operator) {
FilterOperator.And -> FilterGroup.And.Facet(value, key.name)
FilterOperator.Or -> FilterGroup.Or.Facet(value, key.name)
}
}.toSet()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.algolia.instantsearch.helper.relateditems.internal.extensions

import com.algolia.search.model.Attribute
import com.algolia.search.model.filter.Filter
import com.algolia.search.model.filter.FilterGroup
import com.algolia.search.model.filter.FilterGroupsConverter
import com.algolia.search.model.indexing.Indexable

internal fun Indexable.toFacetFilter(isNegated: Boolean = false): List<List<String>> {
val filter = Filter.Facet(Attribute("objectID"), objectID.toString(), isNegated = isNegated)
val filterGroups = setOf<FilterGroup<Filter.Facet>>(FilterGroup.And.Facet(filter))
return FilterGroupsConverter.Legacy.Facet(filterGroups).unquote()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.algolia.instantsearch.helper.relateditems.internal.extensions

import com.algolia.instantsearch.helper.filter.state.FilterState
import com.algolia.instantsearch.helper.filter.state.groupAnd
import com.algolia.instantsearch.helper.filter.state.groupOr
import com.algolia.instantsearch.helper.relateditems.MatchingPattern
import com.algolia.instantsearch.helper.relateditems.internal.FilterFacetAndID
import com.algolia.search.model.filter.Filter
import com.algolia.search.model.filter.FilterGroupsConverter

internal fun <T> List<MatchingPattern<T>>.toOptionalFilters(hit: T): List<List<String>>? {
val filterState = FilterState()
forEach { filterState.addMatchingPattern(hit, it) }
return FilterGroupsConverter.Legacy.Facet(filterState.toFilterFacetGroup()).unquote()
}

/**
* Create an [FilterFacetAndID] from a [MatchingPattern].
*/
internal fun <T> MatchingPattern<T>.toOptionalFilter(hit: T): FilterFacetAndID? {
val property = property.get(hit) ?: return null
return when (property) {
is Iterable<*> -> {
val groupOr = groupOr()
val list = property.map { value -> Filter.Facet(attribute, value.toString(), score) }.toTypedArray()
FilterFacetAndID(groupOr, list)
}
else -> FilterFacetAndID(
groupAnd(),
arrayOf(Filter.Facet(attribute, property.toString(), score))
)
}
}
Loading

0 comments on commit 25415a8

Please sign in to comment.